From 10f29f702ea526869ff40a61c29e66ede2ca387d Mon Sep 17 00:00:00 2001 From: Kajetan Ramsza Date: Wed, 13 May 2026 17:43:24 +0200 Subject: [PATCH 1/6] feat: add example files api --- oioioi/problems/urls.py | 10 +++++++ oioioi/problems/views.py | 58 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/oioioi/problems/urls.py b/oioioi/problems/urls.py index 5b62e94f2..b085822c4 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/", + views.problem_site_example_tests_view, + name="problem_site_example_tests", + ), + path( + "example_tests/", + views.problem_site_example_test_file_view, + name="problem_site_example_test_file", + ), ] urlpatterns = [ diff --git a/oioioi/problems/views.py b/oioioi/problems/views.py index 73577e21b..a143e05d9 100644 --- a/oioioi/problems/views.py +++ b/oioioi/problems/views.py @@ -26,7 +26,7 @@ When, ) from django.db.models.functions import Coalesce -from django.http import Http404, HttpResponse, HttpResponseBadRequest +from django.http import Http404, HttpResponse, HttpResponseBadRequest, JsonResponse from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse from django.urls import reverse @@ -92,7 +92,7 @@ query_statement, show_proposal_form, ) -from oioioi.programs.models import ModelSolution +from oioioi.programs.models import ModelSolution, Test def show_statement_view(request, statement_id): @@ -569,6 +569,60 @@ def problem_site_external_attachment_view(request, site_key, attachment_id): return stream_file(attachment.content, attachment.download_name) +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) + + +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) + + def problemset_add_to_contest_view(request, site_key): problem_name = request.GET.get("problem_name") if not problem_name: From 787315f98d74cd45b7c8e656e1494eafb58c31f2 Mon Sep 17 00:00:00 2001 From: Kajetan Ramsza Date: Wed, 20 May 2026 16:12:27 +0200 Subject: [PATCH 2/6] fix: format --- oioioi/problems/views.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/oioioi/problems/views.py b/oioioi/problems/views.py index a143e05d9..35541f0a8 100644 --- a/oioioi/problems/views.py +++ b/oioioi/problems/views.py @@ -579,17 +579,23 @@ def problem_site_example_tests_view(request, site_key): 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, - }) + 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) @@ -604,7 +610,7 @@ def problem_site_example_test_file_view(request, site_key, filename): short_name = problem.short_name if not base.startswith(short_name): raise Http404 - test_name = base[len(short_name):] + test_name = base[len(short_name) :] test = get_object_or_404( Test, From f14bb4470dc01e621b6e1c75bfcf68a1da7e6de0 Mon Sep 17 00:00:00 2001 From: Kajetan Ramsza Date: Wed, 20 May 2026 16:19:08 +0200 Subject: [PATCH 3/6] fix: require_safe --- oioioi/problems/views.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/oioioi/problems/views.py b/oioioi/problems/views.py index 35541f0a8..c11cf73df 100644 --- a/oioioi/problems/views.py +++ b/oioioi/problems/views.py @@ -34,7 +34,7 @@ from django.utils.safestring import mark_safe from django.utils.translation import get_language from django.utils.translation import gettext_lazy as _ -from django.views.decorators.http import require_http_methods +from django.views.decorators.http import require_http_methods, require_safe from unidecode import unidecode from oioioi.base.permissions import enforce_condition, not_anonymous @@ -569,6 +569,7 @@ def problem_site_external_attachment_view(request, site_key, attachment_id): return stream_file(attachment.content, attachment.download_name) +@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( @@ -600,6 +601,7 @@ def problem_site_example_tests_view(request, site_key): 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) From 0c98098ace72e34028119cfea7a0b87b867873f3 Mon Sep 17 00:00:00 2001 From: Kajetan Ramsza Date: Wed, 20 May 2026 16:33:18 +0200 Subject: [PATCH 4/6] feat: add unit tests --- oioioi/problems/tests/test_problem.py | 54 +++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) 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 = [ From 6e0b16deef8c7ae47e23c2bffb6e603459add3fe Mon Sep 17 00:00:00 2001 From: Kajetan Ramsza Date: Wed, 27 May 2026 16:34:32 +0200 Subject: [PATCH 5/6] fix: move new endpoints to api --- oioioi/problems/api.py | 69 ++++++++++++++++++++++++++++++++++++++++ oioioi/problems/urls.py | 4 +-- oioioi/problems/views.py | 68 ++------------------------------------- 3 files changed, 74 insertions(+), 67 deletions(-) diff --git a/oioioi/problems/api.py b/oioioi/problems/api.py index 5fdfa30bc..0f1b67c7c 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/urls.py b/oioioi/problems/urls.py index b085822c4..cab9839bd 100644 --- a/oioioi/problems/urls.py +++ b/oioioi/problems/urls.py @@ -24,12 +24,12 @@ ), path( "example_tests/", - views.problem_site_example_tests_view, + api.problem_site_example_tests_view, name="problem_site_example_tests", ), path( "example_tests/", - views.problem_site_example_test_file_view, + api.problem_site_example_test_file_view, name="problem_site_example_test_file", ), ] diff --git a/oioioi/problems/views.py b/oioioi/problems/views.py index c11cf73df..73577e21b 100644 --- a/oioioi/problems/views.py +++ b/oioioi/problems/views.py @@ -26,7 +26,7 @@ When, ) from django.db.models.functions import Coalesce -from django.http import Http404, HttpResponse, HttpResponseBadRequest, JsonResponse +from django.http import Http404, HttpResponse, HttpResponseBadRequest from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse from django.urls import reverse @@ -34,7 +34,7 @@ from django.utils.safestring import mark_safe from django.utils.translation import get_language from django.utils.translation import gettext_lazy as _ -from django.views.decorators.http import require_http_methods, require_safe +from django.views.decorators.http import require_http_methods from unidecode import unidecode from oioioi.base.permissions import enforce_condition, not_anonymous @@ -92,7 +92,7 @@ query_statement, show_proposal_form, ) -from oioioi.programs.models import ModelSolution, Test +from oioioi.programs.models import ModelSolution def show_statement_view(request, statement_id): @@ -569,68 +569,6 @@ def problem_site_external_attachment_view(request, site_key, attachment_id): return stream_file(attachment.content, attachment.download_name) -@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) - - def problemset_add_to_contest_view(request, site_key): problem_name = request.GET.get("problem_name") if not problem_name: From ea0a7260e48c47cc2167096f22ae733d1fa7b15a Mon Sep 17 00:00:00 2001 From: Kajetan Ramsza Date: Wed, 27 May 2026 17:01:07 +0200 Subject: [PATCH 6/6] fix: format --- oioioi/problems/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oioioi/problems/api.py b/oioioi/problems/api.py index 0f1b67c7c..28baf8358 100644 --- a/oioioi/problems/api.py +++ b/oioioi/problems/api.py @@ -228,7 +228,7 @@ def problem_site_example_test_file_view(request, site_key, filename): short_name = problem.short_name if not base.startswith(short_name): raise Http404 - test_name = base[len(short_name):] + test_name = base[len(short_name) :] test = get_object_or_404( Test,