diff --git a/oioioi/problems/api.py b/oioioi/problems/api.py index 5fdfa30bc..28baf8358 100644 --- a/oioioi/problems/api.py +++ b/oioioi/problems/api.py @@ -1,7 +1,12 @@ +import os + from django.db import transaction +from django.http import Http404, JsonResponse from django.shortcuts import get_object_or_404 +from django.urls import reverse from django.utils.decorators import method_decorator from django.utils.translation import gettext as _ +from django.views.decorators.http import require_safe from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import OpenApiParameter, extend_schema from rest_framework import status @@ -12,6 +17,7 @@ from oioioi.contests.models import Contest from oioioi.contests.utils import can_admin_contest +from oioioi.filetracker.utils import stream_file from oioioi.problems.forms import PackageUploadForm from oioioi.problems.models import Problem, ProblemPackage from oioioi.problems.problem_sources import UploadedPackageSource @@ -20,6 +26,7 @@ PackageUploadSerializer, ) from oioioi.problems.utils import can_admin_problem +from oioioi.programs.models import Test def _check_permissions(request, contest=None, existing_problem=None): @@ -176,3 +183,65 @@ def prepare_data(dictionary): } return data + + +@require_safe +def problem_site_example_tests_view(request, site_key): + problem = get_object_or_404(Problem, problemsite__url_key=site_key) + tests = Test.objects.filter( + problem_instance=problem.main_problem_instance, + kind="EXAMPLE", + ).order_by("order", "name") + + result = [] + for test in tests: + basename = f"{problem.short_name}{test.name}" + result.append( + { + "name": test.name, + "in_url": reverse( + "problem_site_example_test_file", + kwargs={"site_key": site_key, "filename": f"{basename}.in"}, + ) + if test.input_file + else None, + "out_url": reverse( + "problem_site_example_test_file", + kwargs={"site_key": site_key, "filename": f"{basename}.out"}, + ) + if test.output_file + else None, + } + ) + + return JsonResponse(result, safe=False) + + +@require_safe +def problem_site_example_test_file_view(request, site_key, filename): + problem = get_object_or_404(Problem, problemsite__url_key=site_key) + + base, ext = os.path.splitext(filename) + if ext not in (".in", ".out"): + raise Http404 + + short_name = problem.short_name + if not base.startswith(short_name): + raise Http404 + test_name = base[len(short_name) :] + + test = get_object_or_404( + Test, + problem_instance=problem.main_problem_instance, + kind="EXAMPLE", + name=test_name, + ) + + if ext == ".in": + if not test.input_file: + raise Http404 + return stream_file(test.input_file, filename) + else: + if not test.output_file: + raise Http404 + return stream_file(test.output_file, filename) diff --git a/oioioi/problems/tests/test_problem.py b/oioioi/problems/tests/test_problem.py index f6fd3cae9..661805a41 100644 --- a/oioioi/problems/tests/test_problem.py +++ b/oioioi/problems/tests/test_problem.py @@ -1,3 +1,5 @@ +import json + from django.conf import settings from django.contrib.auth.models import AnonymousUser, Permission, User from django.contrib.contenttypes.models import ContentType @@ -542,6 +544,58 @@ def test_form_accessibility(self): self.assertNotContains(response, 'id="open-form"') +@override_settings(CONTEST_MODE=ContestMode.neutral) +class TestProblemSiteExampleTests(TestCase, TestStreamingMixin): + fixtures = [ + "test_users", + "test_full_package", + "test_problem_instance_with_no_contest", + "test_problem_site", + ] + + def _list_url(self): + return reverse("problem_site_example_tests", kwargs={"site_key": "123"}) + + def _file_url(self, filename): + return reverse("problem_site_example_test_file", kwargs={"site_key": "123", "filename": filename}) + + def test_list_returns_example_tests(self): + # No auth required — same access model as the statement view + response = self.client.get(self._list_url()) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content) + self.assertEqual({e["name"] for e in data}, {"0", "1ocen"}) + entry = next(e for e in data if e["name"] == "0") + self.assertEqual(entry["in_url"], self._file_url("sum0.in")) + self.assertEqual(entry["out_url"], self._file_url("sum0.out")) + + def test_list_wrong_site_key_returns_404(self): + url = reverse("problem_site_example_tests", kwargs={"site_key": "nonexistent"}) + self.assertEqual(self.client.get(url).status_code, 404) + + def test_file_input(self): + # No auth required + response = self.client.get(self._file_url("sum0.in")) + self.assertStreamingEqual(response, b"1 2\n") + + def test_file_output(self): + response = self.client.get(self._file_url("sum0.out")) + self.assertStreamingEqual(response, b"3\n") + + def test_file_wrong_extension_returns_404(self): + self.assertEqual(self.client.get(self._file_url("sum0.txt")).status_code, 404) + + def test_file_wrong_test_name_returns_404(self): + self.assertEqual(self.client.get(self._file_url("sum99.in")).status_code, 404) + + def test_file_wrong_short_name_returns_404(self): + self.assertEqual(self.client.get(self._file_url("wrong0.in")).status_code, 404) + + def test_unsafe_methods_not_allowed(self): + self.assertEqual(self.client.post(self._list_url()).status_code, 405) + self.assertEqual(self.client.post(self._file_url("sum0.in")).status_code, 405) + + @override_settings(LANGUAGE_CODE="en") class TestProblemChangeForm(TestCase): fixtures = [ diff --git a/oioioi/problems/urls.py b/oioioi/problems/urls.py index 5b62e94f2..cab9839bd 100644 --- a/oioioi/problems/urls.py +++ b/oioioi/problems/urls.py @@ -22,6 +22,16 @@ views.problem_site_external_attachment_view, name="problem_site_external_attachment", ), + path( + "example_tests/", + api.problem_site_example_tests_view, + name="problem_site_example_tests", + ), + path( + "example_tests/", + api.problem_site_example_test_file_view, + name="problem_site_example_test_file", + ), ] urlpatterns = [