From 4c56b19b565f8dbbd1bc390e59b245bea06ef98c Mon Sep 17 00:00:00 2001 From: Mateusz Kolpa Date: Wed, 20 Mar 2024 16:55:58 +0100 Subject: [PATCH 01/34] Add api_issue_md --- oioioi/contests/api_issue.md | 163 +++++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 oioioi/contests/api_issue.md diff --git a/oioioi/contests/api_issue.md b/oioioi/contests/api_issue.md new file mode 100644 index 000000000..04f32f804 --- /dev/null +++ b/oioioi/contests/api_issue.md @@ -0,0 +1,163 @@ +Improve the OIOIOI API + +Hi everyone, we are starting to work on improving the OIOIOI's API. Currently, it doesn't provide much functionality and we'd like to make it more useful. + + +### Endpoints + +Here is the specification of endpoints we are planning on adding + +### `/api/version` + +Return the current version of the api. + +#### Input + +| parameter | type | description | +|:---------:|:----:|:-----------:| +| n/a | n/a | n/a | + +#### Output + +```json +{ + "major" : {major}, + "minor" : {minor}, + "patch" : {patch} +} +``` + +| parameter | type | description | +|:---------:|:----:|:-----------:| +| major | number | MAJOR version | +| minor | number | MINOR version | +| patch | number | PATCH version | + + +### `/api/contest_list` + +Return a list of contests that user is signed into. + +#### Input + +| arguments | description | +|:---------:|:-----------:| +| n/a | n/a | + +#### Output + +```json +{ + "{contest_slug}": "{contest_name}", + ... +} +``` + +| parameter | type | description | +|:---------:|:----:|:-----------:| +| contest_slug | string | Short, unique name of the contest, typically, eg. `oi31-1` | +| contest_name | string | Long, unique? name of the contest, typically, eg. `XXXI Olimpiada Informatyczna` | + +### `/api/user_info` + +Return all available user information **unrelated** to contests. + +#### Input + +| parameter | type | description | +|:---------:|:----:|:-----------:| +| n/a | n/a | n/a | + +#### Output + +```json +{ + "username" : {username}, + ??? +} +``` + +| parameter | type | description | +|:---------:|:----:|:-----------:| +| username | string | Username of the user ;D | + +### `/api/contest/{contest_slug}/problem_list` + +Return the available problems inside a contest. + +#### Input + +| parameter | type | description | +|:-------------:|:----:|:-----------:| +| contest_slug | string | Contest slug, any returned by `/api/contest_list`. | + +#### Output + +```json +{ + "{contest_slug}": { + "{problem_slug}" : { + "problem_name": {problem_name}, + "content_link": { + "type": "pdf" / "other", + "link": {link}, + } + }, + ... + } +} +``` + +| parameter | type | description | +|:---------:|:----:|:-----------:| +| contest_slug | string | Contest id, any returned by `/api/contest_list`. | +| problem_slug | string | Problem id, usually a 3 letter-long short name. | +| problem_name | string | Full name of the problem | +| link | string | In case of `pdf`, a link to a PDF, else a regular link. | + +### `/api/contest/{contest_slug}/problem/{problem_slug}/` + +Return ???????? + +#### Input + +| parameter | type | description | +|:-------------:|:----:|:-----------:| +| contest_slug | string | Contest slug, any returned by `/api/contest_list`. | + +#### Output + +```json +{ + "{contest_slug}": { + "{problem_slug}" : { + "problem_name": {problem_name}, + "content_link": { + "type": "pdf" / "other", + "link": {link}, + } + }, + ... + } +} +``` + +| parameter | type | description | +|:---------:|:----:|:-----------:| +| contest_slug | string | Contest id, any returned by `/api/contest_list`. | +| problem_slug | string | Problem id, usually a 3 letter-long short name. | +| problem_name | string | Full name of the problem | +| link | string | In case of `pdf`, a link to a PDF, else a regular link. | + +- Dla problemu: + - Nazwa + - Link do pdfa + - Aktualny wynik + - Liczba podzadań ? + - Limity czasowe ? + - SUBMIT + - Lista submitów - z punktami itd... + - Lista zgłoszeń: + - id zgłoszenia + - status + - link From 1f21272bd8e58b55ab1d476a0250941f4011b992 Mon Sep 17 00:00:00 2001 From: Mateusz Kolpa Date: Wed, 20 Mar 2024 17:52:05 +0100 Subject: [PATCH 02/34] Add contest_list --- oioioi/contests/api.py | 17 +++++++++++++++++ oioioi/contests/api_issue.md | 11 ++++++++--- oioioi/contests/urls.py | 4 ++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/oioioi/contests/api.py b/oioioi/contests/api.py index 6af22f277..efad46ec1 100644 --- a/oioioi/contests/api.py +++ b/oioioi/contests/api.py @@ -6,11 +6,28 @@ from oioioi.contests.serializers import SubmissionSerializer from oioioi.contests.utils import can_enter_contest from oioioi.problems.models import Problem +from oioioi.base.permissions import enforce_condition, not_anonymous + from rest_framework import permissions, status, views from rest_framework.parsers import MultiPartParser from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.schemas import AutoSchema +from rest_framework import serializers +from rest_framework.decorators import api_view + +class ContestSerializer(serializers.ModelSerializer): + class Meta: + model = Contest + fields = ['id', 'name'] + + +@api_view(['GET']) +@enforce_condition(not_anonymous, login_redirect=False) +def contest_list(request): + contests = [x.contest for x in request.user.contestview_set.all()] + serializer = ContestSerializer(contests, many=True) + return Response(serializer.data) class CanEnterContest(permissions.BasePermission): diff --git a/oioioi/contests/api_issue.md b/oioioi/contests/api_issue.md index 04f32f804..c0cacda21 100644 --- a/oioioi/contests/api_issue.md +++ b/oioioi/contests/api_issue.md @@ -47,10 +47,15 @@ Return a list of contests that user is signed into. #### Output ```json -{ - "{contest_slug}": "{contest_name}", + +[ + { + slug: "{contest_slug}": + name: "{contest_name}", + }, ... -} +], + ``` | parameter | type | description | diff --git a/oioioi/contests/urls.py b/oioioi/contests/urls.py index 0fb889774..a5cfd3c5e 100644 --- a/oioioi/contests/urls.py +++ b/oioioi/contests/urls.py @@ -189,6 +189,10 @@ def glob_namespaced_patterns(namespace): if settings.USE_API: nonc_patterns += [ # the contest information is managed manually and added after api prefix + re_path( + r'^api/contest_list', + api.contest_list + ), re_path( r'^api/c/(?P[a-z0-9_-]+)/submit/(?P[a-z0-9_-]+)$', api.SubmitContestSolutionView.as_view(), From d6e0b0221b066168512b6ba887fa38080a7b4419 Mon Sep 17 00:00:00 2001 From: Mateusz Kolpa Date: Wed, 20 Mar 2024 19:06:48 +0100 Subject: [PATCH 03/34] Round listing might work, not tested yet, cuz gotta go hit the gym --- oioioi/contests/api.py | 84 ++++++++++++++++++++++++++++++++++-- oioioi/contests/api_issue.md | 39 ++++++++--------- oioioi/contests/urls.py | 1 + 3 files changed, 101 insertions(+), 23 deletions(-) diff --git a/oioioi/contests/api.py b/oioioi/contests/api.py index efad46ec1..e34217a69 100644 --- a/oioioi/contests/api.py +++ b/oioioi/contests/api.py @@ -4,8 +4,9 @@ from oioioi.contests.forms import SubmissionFormForProblemInstance from oioioi.contests.models import Contest, ProblemInstance from oioioi.contests.serializers import SubmissionSerializer -from oioioi.contests.utils import can_enter_contest -from oioioi.problems.models import Problem +from oioioi.contests.utils import contest_exists, can_enter_contest, visible_contests, visible_rounds +from oioioi.problems.models import Problem, ProblemInstance +from oioioi.contests.models import Round from oioioi.base.permissions import enforce_condition, not_anonymous from rest_framework import permissions, status, views @@ -25,10 +26,87 @@ class Meta: @api_view(['GET']) @enforce_condition(not_anonymous, login_redirect=False) def contest_list(request): - contests = [x.contest for x in request.user.contestview_set.all()] + contests = visible_contests(request) serializer = ContestSerializer(contests, many=True) return Response(serializer.data) +class RoundSerializer(serializers.ModelSerializer): + class Meta: + model = Round + fields = ['__all__'] + + +@api_view(['GET']) +@enforce_condition(not_anonymous & contest_exists & can_enter_contest) +def round_list(request): + rounds = visible_rounds(request) + serializer = RoundSerializer(rounds, many=True) + return Response(serializer.data) + +# # Problem statements in order +# # 1) problem instance +# # 2) statement_visible +# # 3) round end time +# # 4) user result +# # 5) number of submissions left +# # 6) submissions_limit +# # 7) can_submit +# # Sorted by (start_date, end_date, round name, problem name) +# problems_statements = sorted( +# [ +# ( +# pi, +# controller.can_see_statement(request, pi), +# controller.get_round_times(request, pi.round), +# # Because this view can be accessed by an anynomous user we can't +# # use `user=request.user` (it would cause TypeError). Surprisingly +# # using request.user.id is ok since for AnynomousUser id is set +# # to None. +# next( +# ( +# r +# for r in UserResultForProblem.objects.filter( +# user__id=request.user.id, problem_instance=pi +# ) +# if r +# and r.submission_report +# and controller.can_see_submission_score( +# request, r.submission_report.submission +# ) +# ), +# None, +# ), +# pi.controller.get_submissions_left(request, pi), +# pi.controller.get_submissions_limit(request, pi), +# controller.can_submit(request, pi) and not is_contest_archived(request), +# ) +# for pi in problem_instances +# ], +# key=lambda p: (p[2].get_key_for_comparison(), p[0].round.name, p[0].short_name), +# ) + +# show_submissions_limit = any([p[5] for p in problems_statements]) +# show_submit_button = any([p[6] for p in problems_statements]) +# show_rounds = len(frozenset(pi.round_id for pi in problem_instances)) > 1 +# table_columns = 3 + int(show_submissions_limit) + int(show_submit_button) + +# return TemplateResponse( +# request, +# 'contests/problems_list.html', +# { +# 'problem_instances': problems_statements, +# 'show_rounds': show_rounds, +# 'show_scores': request.user.is_authenticated, +# 'show_submissions_limit': show_submissions_limit, +# 'show_submit_button': show_submit_button, +# 'table_columns': table_columns, +# 'problems_on_page': getattr(settings, 'PROBLEMS_ON_PAGE', 100), +# }, +# ) +# rounds = [x.contest for x in request.user.contestview_set.all()] +# serializer = RoundSerializer(rounds, many=True) +# return Response(serializer.data) + class CanEnterContest(permissions.BasePermission): def has_object_permission(self, request, view, obj): diff --git a/oioioi/contests/api_issue.md b/oioioi/contests/api_issue.md index c0cacda21..3a4d987c6 100644 --- a/oioioi/contests/api_issue.md +++ b/oioioi/contests/api_issue.md @@ -50,7 +50,7 @@ Return a list of contests that user is signed into. [ { - slug: "{contest_slug}": + id: "{contest_id}": name: "{contest_name}", }, ... @@ -60,8 +60,8 @@ Return a list of contests that user is signed into. | parameter | type | description | |:---------:|:----:|:-----------:| -| contest_slug | string | Short, unique name of the contest, typically, eg. `oi31-1` | -| contest_name | string | Long, unique? name of the contest, typically, eg. `XXXI Olimpiada Informatyczna` | +| contest_id | string | Id is a short, unique name of the contest, eg. `oi31-1` | +| contest_name | string | Long, unique? name of the contest, eg. `XXXI Olimpiada Informatyczna` | ### `/api/user_info` @@ -86,7 +86,7 @@ Return all available user information **unrelated** to contests. |:---------:|:----:|:-----------:| | username | string | Username of the user ;D | -### `/api/contest/{contest_slug}/problem_list` +### `/api/c/{contest_id}/problem_list` Return the available problems inside a contest. @@ -94,33 +94,32 @@ Return the available problems inside a contest. | parameter | type | description | |:-------------:|:----:|:-----------:| -| contest_slug | string | Contest slug, any returned by `/api/contest_list`. | +| contest_id | string | Contest id, any returned by `/api/contest_list`. | #### Output ```json -{ - "{contest_slug}": { - "{problem_slug}" : { - "problem_name": {problem_name}, - "content_link": { - "type": "pdf" / "other", - "link": {link}, - } - }, - ... - } -} +[ + { + "problem_id":"{problem_id}", + "problem_name": {problem_name}, + "content_link": { + "type": "pdf" / "other", + "link": {link}, + } + }, + ... +] ``` | parameter | type | description | |:---------:|:----:|:-----------:| -| contest_slug | string | Contest id, any returned by `/api/contest_list`. | -| problem_slug | string | Problem id, usually a 3 letter-long short name. | +| contest_id | string | Contest id, any returned by `/api/contest_list`. | +| problem_id | string | Problem id, usually a 3 letter-long short name. | | problem_name | string | Full name of the problem | | link | string | In case of `pdf`, a link to a PDF, else a regular link. | -### `/api/contest/{contest_slug}/problem/{problem_slug}/` +### `/api/c/{contest_id}/problem/{problem_slug}/` Return ???????? diff --git a/oioioi/contests/urls.py b/oioioi/contests/urls.py index dbabb1acb..a52af2497 100644 --- a/oioioi/contests/urls.py +++ b/oioioi/contests/urls.py @@ -205,6 +205,7 @@ def glob_namespaced_patterns(namespace): api.GetProblemIdView.as_view(), name='api_contest_get_problem_id', ), + re_path(r'^api/c/(?P[a-z0-9_-]+)/round_list/$', api.round_list), re_path( r'^api/problemset/submit/(?P[0-9a-zA-Z-_=]+)$', api.SubmitProblemsetSolutionView.as_view(), From 87583ebec9d9d1f86c88e0230b456ae0d87d5ce0 Mon Sep 17 00:00:00 2001 From: Mateusz Kolpa Date: Wed, 3 Apr 2024 17:57:56 +0200 Subject: [PATCH 04/34] Add round_list, problem_list --- oioioi/contests/api.py | 140 +++++++++++++-------------------- oioioi/contests/serializers.py | 32 ++++++++ oioioi/contests/urls.py | 7 +- 3 files changed, 94 insertions(+), 85 deletions(-) diff --git a/oioioi/contests/api.py b/oioioi/contests/api.py index e34217a69..e9034f206 100644 --- a/oioioi/contests/api.py +++ b/oioioi/contests/api.py @@ -1,12 +1,14 @@ from django.http import Http404 from django.shortcuts import get_object_or_404 +from django.utils.timezone import now + +from oioioi.base.utils import request_cached from oioioi.base.utils.api import make_path_coreapi_schema from oioioi.contests.forms import SubmissionFormForProblemInstance from oioioi.contests.models import Contest, ProblemInstance -from oioioi.contests.serializers import SubmissionSerializer -from oioioi.contests.utils import contest_exists, can_enter_contest, visible_contests, visible_rounds +from oioioi.contests.serializers import ContestSerializer, ProblemSerializer, RoundSerializer, SubmissionSerializer +from oioioi.contests.utils import can_enter_contest, visible_contests from oioioi.problems.models import Problem, ProblemInstance -from oioioi.contests.models import Round from oioioi.base.permissions import enforce_condition, not_anonymous from rest_framework import permissions, status, views @@ -17,11 +19,6 @@ from rest_framework import serializers from rest_framework.decorators import api_view -class ContestSerializer(serializers.ModelSerializer): - class Meta: - model = Contest - fields = ['id', 'name'] - @api_view(['GET']) @enforce_condition(not_anonymous, login_redirect=False) @@ -30,87 +27,62 @@ def contest_list(request): serializer = ContestSerializer(contests, many=True) return Response(serializer.data) -class RoundSerializer(serializers.ModelSerializer): - class Meta: - model = Round - fields = ['__all__'] +class CanEnterContest(permissions.BasePermission): + def has_object_permission(self, request, view, obj): + return can_enter_contest(request) -@api_view(['GET']) -@enforce_condition(not_anonymous & contest_exists & can_enter_contest) -def round_list(request): - rounds = visible_rounds(request) - serializer = RoundSerializer(rounds, many=True) - return Response(serializer.data) -# # Problem statements in order -# # 1) problem instance -# # 2) statement_visible -# # 3) round end time -# # 4) user result -# # 5) number of submissions left -# # 6) submissions_limit -# # 7) can_submit -# # Sorted by (start_date, end_date, round name, problem name) -# problems_statements = sorted( -# [ -# ( -# pi, -# controller.can_see_statement(request, pi), -# controller.get_round_times(request, pi.round), -# # Because this view can be accessed by an anynomous user we can't -# # use `user=request.user` (it would cause TypeError). Surprisingly -# # using request.user.id is ok since for AnynomousUser id is set -# # to None. -# next( -# ( -# r -# for r in UserResultForProblem.objects.filter( -# user__id=request.user.id, problem_instance=pi -# ) -# if r -# and r.submission_report -# and controller.can_see_submission_score( -# request, r.submission_report.submission -# ) -# ), -# None, -# ), -# pi.controller.get_submissions_left(request, pi), -# pi.controller.get_submissions_limit(request, pi), -# controller.can_submit(request, pi) and not is_contest_archived(request), -# ) -# for pi in problem_instances -# ], -# key=lambda p: (p[2].get_key_for_comparison(), p[0].round.name, p[0].short_name), -# ) - -# show_submissions_limit = any([p[5] for p in problems_statements]) -# show_submit_button = any([p[6] for p in problems_statements]) -# show_rounds = len(frozenset(pi.round_id for pi in problem_instances)) > 1 -# table_columns = 3 + int(show_submissions_limit) + int(show_submit_button) - -# return TemplateResponse( -# request, -# 'contests/problems_list.html', -# { -# 'problem_instances': problems_statements, -# 'show_rounds': show_rounds, -# 'show_scores': request.user.is_authenticated, -# 'show_submissions_limit': show_submissions_limit, -# 'show_submit_button': show_submit_button, -# 'table_columns': table_columns, -# 'problems_on_page': getattr(settings, 'PROBLEMS_ON_PAGE', 100), -# }, -# ) -# rounds = [x.contest for x in request.user.contestview_set.all()] -# serializer = RoundSerializer(rounds, many=True) -# return Response(serializer.data) +class GetContestRounds(views.APIView): + permission_classes = ( + IsAuthenticated, + CanEnterContest, + ) + schema = AutoSchema( + [ + make_path_coreapi_schema( + name='contest_id', + title="Contest id", + description="Id of the contest from contest_list endpoint" + ), + ] + ) -class CanEnterContest(permissions.BasePermission): - def has_object_permission(self, request, view, obj): - return can_enter_contest(request) + def get(self, request, contest_id): + contest = get_object_or_404(Contest, id=contest_id) + rounds = contest.round_set.all() + serializer = RoundSerializer(rounds, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + +class GetContestProblems(views.APIView): + permission_classes = ( + IsAuthenticated, + CanEnterContest, + ) + + schema = AutoSchema( + [ + make_path_coreapi_schema( + name='contest_id', + title="Contest id", + description="Id of the contest from contest_list endpoint" + ), + ] + ) + + def get(self, request, contest_id): + contest: Contest = get_object_or_404(Contest, id=contest_id) + controller = contest.controller + queryset = ( + ProblemInstance.objects.filter(contest=request.contest) + .select_related('problem') + .prefetch_related('round') + ) + + problems = [pi for pi in queryset if controller.can_see_problem(request, pi)] + serializer = ProblemSerializer(problems, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) class GetProblemIdView(views.APIView): diff --git a/oioioi/contests/serializers.py b/oioioi/contests/serializers.py index c7c46b3ee..849a2fb9c 100644 --- a/oioioi/contests/serializers.py +++ b/oioioi/contests/serializers.py @@ -1,4 +1,6 @@ +from oioioi.contests.models import Contest, ProblemInstance, Round from rest_framework import serializers +from django.utils.timezone import now class SubmissionSerializer(serializers.Serializer): @@ -30,3 +32,33 @@ def validate(self, data): class Meta: fields = ('file', 'kind', 'problem_instance_id') + + +class ContestSerializer(serializers.ModelSerializer): + class Meta: + model = Contest + fields = ['id', 'name'] + + +class RoundSerializer(serializers.ModelSerializer): + is_active = serializers.SerializerMethodField() + + class Meta: + model = Round + fields = [ + "name", "start_date", "end_date", "is_active", + "results_date", "public_results_date", "is_trial", + ] + + def get_is_active(self, obj: Round): + if obj.end_date: + return now() < obj.end_date + return True + + +class ProblemSerializer(serializers.ModelSerializer): + + class Meta: + model = ProblemInstance + fields = "__all__" + diff --git a/oioioi/contests/urls.py b/oioioi/contests/urls.py index a52af2497..46d26c4b8 100644 --- a/oioioi/contests/urls.py +++ b/oioioi/contests/urls.py @@ -205,7 +205,12 @@ def glob_namespaced_patterns(namespace): api.GetProblemIdView.as_view(), name='api_contest_get_problem_id', ), - re_path(r'^api/c/(?P[a-z0-9_-]+)/round_list/$', api.round_list), + re_path(r'^api/c/(?P[a-z0-9_-]+)/round_list/$', + api.GetContestRounds.as_view() + ), + re_path(r'^api/c/(?P[a-z0-9_-]+)/problem_list/$', + api.GetContestProblems.as_view() + ), re_path( r'^api/problemset/submit/(?P[0-9a-zA-Z-_=]+)$', api.SubmitProblemsetSolutionView.as_view(), From 42929bb22c65021362310fa624c3ce3b52b0d82e Mon Sep 17 00:00:00 2001 From: Mateusz Kolpa Date: Wed, 17 Apr 2024 17:28:19 +0200 Subject: [PATCH 05/34] Improve problem_list --- oioioi/contests/api.py | 55 ++++++++++++++++++++++++++++------ oioioi/contests/serializers.py | 21 +++++++++---- oioioi/contests/utils.py | 50 ++++++++++++++++++++++++++++--- 3 files changed, 108 insertions(+), 18 deletions(-) diff --git a/oioioi/contests/api.py b/oioioi/contests/api.py index e9034f206..517619a63 100644 --- a/oioioi/contests/api.py +++ b/oioioi/contests/api.py @@ -6,8 +6,18 @@ from oioioi.base.utils.api import make_path_coreapi_schema from oioioi.contests.forms import SubmissionFormForProblemInstance from oioioi.contests.models import Contest, ProblemInstance -from oioioi.contests.serializers import ContestSerializer, ProblemSerializer, RoundSerializer, SubmissionSerializer -from oioioi.contests.utils import can_enter_contest, visible_contests +from oioioi.contests.serializers import ( + ContestSerializer, + ProblemSerializer, + RoundSerializer, + SubmissionSerializer, + UserResultForProblemSerializer, +) +from oioioi.contests.utils import ( + can_enter_contest, + get_problem_statements, + visible_contests, +) from oioioi.problems.models import Problem, ProblemInstance from oioioi.base.permissions import enforce_condition, not_anonymous @@ -44,7 +54,7 @@ class GetContestRounds(views.APIView): make_path_coreapi_schema( name='contest_id', title="Contest id", - description="Id of the contest from contest_list endpoint" + description="Id of the contest from contest_list endpoint", ), ] ) @@ -53,7 +63,8 @@ def get(self, request, contest_id): contest = get_object_or_404(Contest, id=contest_id) rounds = contest.round_set.all() serializer = RoundSerializer(rounds, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.data) + class GetContestProblems(views.APIView): permission_classes = ( @@ -66,7 +77,7 @@ class GetContestProblems(views.APIView): make_path_coreapi_schema( name='contest_id', title="Contest id", - description="Id of the contest from contest_list endpoint" + description="Id of the contest from contest_list endpoint", ), ] ) @@ -74,15 +85,41 @@ class GetContestProblems(views.APIView): def get(self, request, contest_id): contest: Contest = get_object_or_404(Contest, id=contest_id) controller = contest.controller - queryset = ( + problem_instances = ( ProblemInstance.objects.filter(contest=request.contest) .select_related('problem') .prefetch_related('round') ) - problems = [pi for pi in queryset if controller.can_see_problem(request, pi)] - serializer = ProblemSerializer(problems, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) + # problems = [pi for pi in problem_instances if controller.can_see_problem(request, pi)] + # serializer = ProblemSerializer(problems, many=True) + # return Response(serializer.data,) + + # Problem statements in order + # 0) problem instance + # 1) statement_visible + # 2) round end time + # 3) user result + # 4) number of submissions left + # 5) submissions_limit + # 6) can_submit + # Sorted by (start_date, end_date, round name, problem name) + problem_statements = get_problem_statements( + request, controller, problem_instances + ) + + data = [] + for problem_stmt in problem_statements: + serialized = dict(ProblemSerializer(problem_stmt[0], many=False).data) + serialized["full_name"] = problem_stmt[0].problem.legacy_name + serialized["user_result"] = UserResultForProblemSerializer( + problem_stmt[3], many=False + ).data + serialized["submissions_left"] = problem_stmt[4] + serialized["can_submit"] = problem_stmt[6] + data.append(serialized) + + return Response(data) class GetProblemIdView(views.APIView): diff --git a/oioioi/contests/serializers.py b/oioioi/contests/serializers.py index 849a2fb9c..17524c706 100644 --- a/oioioi/contests/serializers.py +++ b/oioioi/contests/serializers.py @@ -1,4 +1,4 @@ -from oioioi.contests.models import Contest, ProblemInstance, Round +from oioioi.contests.models import Contest, ProblemInstance, Round, UserResultForProblem from rest_framework import serializers from django.utils.timezone import now @@ -46,9 +46,14 @@ class RoundSerializer(serializers.ModelSerializer): class Meta: model = Round fields = [ - "name", "start_date", "end_date", "is_active", - "results_date", "public_results_date", "is_trial", - ] + "name", + "start_date", + "end_date", + "is_active", + "results_date", + "public_results_date", + "is_trial", + ] def get_is_active(self, obj: Round): if obj.end_date: @@ -60,5 +65,11 @@ class ProblemSerializer(serializers.ModelSerializer): class Meta: model = ProblemInstance - fields = "__all__" + exclude = ['needs_rejudge', 'problem', 'contest'] + +class UserResultForProblemSerializer(serializers.ModelSerializer): + + class Meta: + model = UserResultForProblem + fields = ['score', 'status'] diff --git a/oioioi/contests/utils.py b/oioioi/contests/utils.py index 59d5a2dd3..2cb1cace0 100644 --- a/oioioi/contests/utils.py +++ b/oioioi/contests/utils.py @@ -20,6 +20,7 @@ FilesMessage, SubmissionsMessage, SubmitMessage, + UserResultForProblem, ) @@ -474,10 +475,7 @@ def get_submit_message(request): @make_request_condition @request_cached def is_contest_archived(request): - return ( - hasattr(request, 'contest') - and request.contest.is_archived - ) + return hasattr(request, 'contest') and request.contest.is_archived def get_inline_for_contest(inline, contest): @@ -508,3 +506,47 @@ def has_view_permission(self, request, obj=None): return True return ArchivedInlineWrapper + + +def get_problem_statements(request, controller, problem_instances): + # Problem statements in order + # 1) problem instance + # 2) statement_visible + # 3) round end time + # 4) user result + # 5) number of submissions left + # 6) submissions_limit + # 7) can_submit + # Sorted by (start_date, end_date, round name, problem name) + return sorted( + [ + ( + pi, + controller.can_see_statement(request, pi), + controller.get_round_times(request, pi.round), + # Because this view can be accessed by an anynomous user we can't + # use `user=request.user` (it would cause TypeError). Surprisingly + # using request.user.id is ok since for AnynomousUser id is set + # to None. + next( + ( + r + for r in UserResultForProblem.objects.filter( + user__id=request.user.id, problem_instance=pi + ) + if r + and r.submission_report + and controller.can_see_submission_score( + request, r.submission_report.submission + ) + ), + None, + ), + pi.controller.get_submissions_left(request, pi), + pi.controller.get_submissions_limit(request, pi), + controller.can_submit(request, pi) and not is_contest_archived(request), + ) + for pi in problem_instances + ], + key=lambda p: (p[2].get_key_for_comparison(), p[0].round.name, p[0].short_name), + ) From a852fa94848381d5e66485d990b45aa3eaecb9c1 Mon Sep 17 00:00:00 2001 From: Mateusz Kolpa Date: Wed, 17 Apr 2024 17:39:48 +0200 Subject: [PATCH 06/34] Add problem statement extension to api response --- oioioi/contests/api.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/oioioi/contests/api.py b/oioioi/contests/api.py index 517619a63..e9486ef89 100644 --- a/oioioi/contests/api.py +++ b/oioioi/contests/api.py @@ -21,6 +21,7 @@ from oioioi.problems.models import Problem, ProblemInstance from oioioi.base.permissions import enforce_condition, not_anonymous +from oioioi.problems.utils import query_statement from rest_framework import permissions, status, views from rest_framework.parsers import MultiPartParser from rest_framework.permissions import IsAuthenticated @@ -117,6 +118,9 @@ def get(self, request, contest_id): ).data serialized["submissions_left"] = problem_stmt[4] serialized["can_submit"] = problem_stmt[6] + serialized["statement_ext"] = query_statement( + problem_stmt[0].problem + ).extension data.append(serialized) return Response(data) From 28c1c3e41ce6e7f2b00683f9701c593090cf95a1 Mon Sep 17 00:00:00 2001 From: Mateusz Kolpa Date: Wed, 17 Apr 2024 18:01:00 +0200 Subject: [PATCH 07/34] Do not show invisible problems --- oioioi/contests/api.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/oioioi/contests/api.py b/oioioi/contests/api.py index e9486ef89..ac78b007f 100644 --- a/oioioi/contests/api.py +++ b/oioioi/contests/api.py @@ -111,17 +111,18 @@ def get(self, request, contest_id): data = [] for problem_stmt in problem_statements: - serialized = dict(ProblemSerializer(problem_stmt[0], many=False).data) - serialized["full_name"] = problem_stmt[0].problem.legacy_name - serialized["user_result"] = UserResultForProblemSerializer( - problem_stmt[3], many=False - ).data - serialized["submissions_left"] = problem_stmt[4] - serialized["can_submit"] = problem_stmt[6] - serialized["statement_ext"] = query_statement( - problem_stmt[0].problem - ).extension - data.append(serialized) + if problem_stmt[1]: + serialized = dict(ProblemSerializer(problem_stmt[0], many=False).data) + serialized["full_name"] = problem_stmt[0].problem.legacy_name + serialized["user_result"] = UserResultForProblemSerializer( + problem_stmt[3], many=False + ).data + serialized["submissions_left"] = problem_stmt[4] + serialized["can_submit"] = problem_stmt[6] + serialized["statement_ext"] = query_statement( + problem_stmt[0].problem + ).extension + data.append(serialized) return Response(data) From 7699811f3f5d8a5ed0ffca2e6325e151f870a08c Mon Sep 17 00:00:00 2001 From: Filip Krawczyk Date: Wed, 17 Apr 2024 19:34:24 +0200 Subject: [PATCH 08/34] Fix None extension in GetContestProblems --- oioioi/contests/api.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/oioioi/contests/api.py b/oioioi/contests/api.py index ac78b007f..d50571e6e 100644 --- a/oioioi/contests/api.py +++ b/oioioi/contests/api.py @@ -119,9 +119,7 @@ def get(self, request, contest_id): ).data serialized["submissions_left"] = problem_stmt[4] serialized["can_submit"] = problem_stmt[6] - serialized["statement_ext"] = query_statement( - problem_stmt[0].problem - ).extension + serialized["statement_ext"] = st.extension if (st := query_statement(problem_stmt[0].problem)) else None data.append(serialized) return Response(data) From cf35bc70656f19d5bdbe34fde9ea5c7aa69ebabb Mon Sep 17 00:00:00 2001 From: Mateusz Kolpa Date: Wed, 24 Apr 2024 16:33:24 +0200 Subject: [PATCH 09/34] Rename statement_ext to statement_extension --- oioioi/contests/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oioioi/contests/api.py b/oioioi/contests/api.py index ac78b007f..c0a21ee6b 100644 --- a/oioioi/contests/api.py +++ b/oioioi/contests/api.py @@ -119,7 +119,7 @@ def get(self, request, contest_id): ).data serialized["submissions_left"] = problem_stmt[4] serialized["can_submit"] = problem_stmt[6] - serialized["statement_ext"] = query_statement( + serialized["statement_extension"] = query_statement( problem_stmt[0].problem ).extension data.append(serialized) From 4a84e6ee06bbcf5bd02dc759efff3c2e4fe44579 Mon Sep 17 00:00:00 2001 From: Mateusz Kolpa Date: Wed, 24 Apr 2024 17:52:15 +0200 Subject: [PATCH 10/34] Prepare for initial PR without tests --- oioioi/contests/api.py | 16 +++++++--------- oioioi/contests/urls.py | 3 ++- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/oioioi/contests/api.py b/oioioi/contests/api.py index 7cf14347e..2dcb821ac 100644 --- a/oioioi/contests/api.py +++ b/oioioi/contests/api.py @@ -92,10 +92,6 @@ def get(self, request, contest_id): .prefetch_related('round') ) - # problems = [pi for pi in problem_instances if controller.can_see_problem(request, pi)] - # serializer = ProblemSerializer(problems, many=True) - # return Response(serializer.data,) - # Problem statements in order # 0) problem instance # 1) statement_visible @@ -171,8 +167,14 @@ def get(self, request, contest_id, problem_short_name): return Response(response_data, status=status.HTTP_200_OK) +# This is a base class for submitting a solution for contests and problemsets. +# It lacks get_problem_instance, as it's specific to problem source. class SubmitSolutionView(views.APIView): - permission_classes = (IsAuthenticated,) + permission_classes = ( + IsAuthenticated, + CanEnterContest, + ) + parser_classes = (MultiPartParser,) def get_problem_instance(self, **kwargs): @@ -201,10 +203,6 @@ def post(self, request, **kwargs): class SubmitContestSolutionView(SubmitSolutionView): - permission_classes = ( - IsAuthenticated, - CanEnterContest, - ) schema = AutoSchema( [ make_path_coreapi_schema( diff --git a/oioioi/contests/urls.py b/oioioi/contests/urls.py index 46d26c4b8..119e99f42 100644 --- a/oioioi/contests/urls.py +++ b/oioioi/contests/urls.py @@ -193,7 +193,8 @@ def glob_namespaced_patterns(namespace): # the contest information is managed manually and added after api prefix re_path( r'^api/contest_list', - api.contest_list + api.contest_list, + name="api_contest_list" ), re_path( r'^api/c/(?P[a-z0-9_-]+)/submit/(?P[a-z0-9_-]+)$', From 3069447a4bd5bdea495e0fe9da4facb71ce8324e Mon Sep 17 00:00:00 2001 From: Mateusz Kolpa Date: Wed, 24 Apr 2024 18:00:03 +0200 Subject: [PATCH 11/34] Rm a redundant file --- oioioi/contests/api_issue.md | 167 ----------------------------------- 1 file changed, 167 deletions(-) delete mode 100644 oioioi/contests/api_issue.md diff --git a/oioioi/contests/api_issue.md b/oioioi/contests/api_issue.md deleted file mode 100644 index 3a4d987c6..000000000 --- a/oioioi/contests/api_issue.md +++ /dev/null @@ -1,167 +0,0 @@ -Improve the OIOIOI API - -Hi everyone, we are starting to work on improving the OIOIOI's API. Currently, it doesn't provide much functionality and we'd like to make it more useful. - - -### Endpoints - -Here is the specification of endpoints we are planning on adding - -### `/api/version` - -Return the current version of the api. - -#### Input - -| parameter | type | description | -|:---------:|:----:|:-----------:| -| n/a | n/a | n/a | - -#### Output - -```json -{ - "major" : {major}, - "minor" : {minor}, - "patch" : {patch} -} -``` - -| parameter | type | description | -|:---------:|:----:|:-----------:| -| major | number | MAJOR version | -| minor | number | MINOR version | -| patch | number | PATCH version | - - -### `/api/contest_list` - -Return a list of contests that user is signed into. - -#### Input - -| arguments | description | -|:---------:|:-----------:| -| n/a | n/a | - -#### Output - -```json - -[ - { - id: "{contest_id}": - name: "{contest_name}", - }, - ... -], - -``` - -| parameter | type | description | -|:---------:|:----:|:-----------:| -| contest_id | string | Id is a short, unique name of the contest, eg. `oi31-1` | -| contest_name | string | Long, unique? name of the contest, eg. `XXXI Olimpiada Informatyczna` | - -### `/api/user_info` - -Return all available user information **unrelated** to contests. - -#### Input - -| parameter | type | description | -|:---------:|:----:|:-----------:| -| n/a | n/a | n/a | - -#### Output - -```json -{ - "username" : {username}, - ??? -} -``` - -| parameter | type | description | -|:---------:|:----:|:-----------:| -| username | string | Username of the user ;D | - -### `/api/c/{contest_id}/problem_list` - -Return the available problems inside a contest. - -#### Input - -| parameter | type | description | -|:-------------:|:----:|:-----------:| -| contest_id | string | Contest id, any returned by `/api/contest_list`. | - -#### Output - -```json -[ - { - "problem_id":"{problem_id}", - "problem_name": {problem_name}, - "content_link": { - "type": "pdf" / "other", - "link": {link}, - } - }, - ... -] -``` - -| parameter | type | description | -|:---------:|:----:|:-----------:| -| contest_id | string | Contest id, any returned by `/api/contest_list`. | -| problem_id | string | Problem id, usually a 3 letter-long short name. | -| problem_name | string | Full name of the problem | -| link | string | In case of `pdf`, a link to a PDF, else a regular link. | - -### `/api/c/{contest_id}/problem/{problem_slug}/` - -Return ???????? - -#### Input - -| parameter | type | description | -|:-------------:|:----:|:-----------:| -| contest_slug | string | Contest slug, any returned by `/api/contest_list`. | - -#### Output - -```json -{ - "{contest_slug}": { - "{problem_slug}" : { - "problem_name": {problem_name}, - "content_link": { - "type": "pdf" / "other", - "link": {link}, - } - }, - ... - } -} -``` - -| parameter | type | description | -|:---------:|:----:|:-----------:| -| contest_slug | string | Contest id, any returned by `/api/contest_list`. | -| problem_slug | string | Problem id, usually a 3 letter-long short name. | -| problem_name | string | Full name of the problem | -| link | string | In case of `pdf`, a link to a PDF, else a regular link. | - -- Dla problemu: - - Nazwa - - Link do pdfa - - Aktualny wynik - - Liczba podzadań ? - - Limity czasowe ? - - SUBMIT - - Lista submitów - z punktami itd... - - Lista zgłoszeń: - - id zgłoszenia - - status - - link From cb08e99a6c82760c2283bb420f5fbb9cc6fee985 Mon Sep 17 00:00:00 2001 From: Mateusz Kolpa Date: Wed, 8 May 2024 18:27:16 +0200 Subject: [PATCH 12/34] Add rate limiting --- oioioi/default_settings.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/oioioi/default_settings.py b/oioioi/default_settings.py index 24bda6547..07f60cf0f 100755 --- a/oioioi/default_settings.py +++ b/oioioi/default_settings.py @@ -869,3 +869,14 @@ # Experimental USE_ACE_EDITOR = False + +REST_FRAMEWORK = { + 'DEFAULT_THROTTLE_CLASSES': [ + 'rest_framework.throttling.AnonRateThrottle', + 'rest_framework.throttling.UserRateThrottle' + ], + 'DEFAULT_THROTTLE_RATES': { + 'anon': '100/day', + 'user': '1000/day' + } +} From d18af575ce20d1d3fef61ea4911c9752d0b03b18 Mon Sep 17 00:00:00 2001 From: Mateusz Kolpa Date: Wed, 8 May 2024 19:01:12 +0200 Subject: [PATCH 13/34] Start writing tests --- oioioi/contests/serializers.py | 1 + oioioi/contests/tests/tests.py | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/oioioi/contests/serializers.py b/oioioi/contests/serializers.py index 17524c706..68fe2d34b 100644 --- a/oioioi/contests/serializers.py +++ b/oioioi/contests/serializers.py @@ -61,6 +61,7 @@ def get_is_active(self, obj: Round): return True +# This is a partial serializer and it serves as a base for the API response. class ProblemSerializer(serializers.ModelSerializer): class Meta: diff --git a/oioioi/contests/tests/tests.py b/oioioi/contests/tests/tests.py index 14d37a24a..ac11a8b10 100644 --- a/oioioi/contests/tests/tests.py +++ b/oioioi/contests/tests/tests.py @@ -3242,6 +3242,18 @@ def test_problemset_submission(self): self._assertSubmitted(response, 2) +class TestAPIContestList(TestCase): + fixtures = ['test_users', 'test_contest', 'test_submission'] + + def test_rights(self): + contest_list = reverse('api_contest_list') + request_anon = self.client.get(contest_list) + self.assertEqual(403, request_anon.status_code) + + self.client.force_authenticate(user=User.objects.get(username='test_user2')) + + + class TestManyRoundsNoEnd(TestCase): fixtures = [ 'test_users', From 6db7e0d30abe14fac8abf9aad0e6e0cd366dd2cc Mon Sep 17 00:00:00 2001 From: Mateusz Kolpa Date: Wed, 15 May 2024 18:12:35 +0200 Subject: [PATCH 14/34] Add submission list --- oioioi/contests/api.py | 64 +++++++++++++++++++++++++++++++++++++- oioioi/contests/urls.py | 7 ++++- oioioi/default_settings.py | 7 +++-- oioioi/problems/views.py | 4 +-- 4 files changed, 75 insertions(+), 7 deletions(-) diff --git a/oioioi/contests/api.py b/oioioi/contests/api.py index 2dcb821ac..a66917717 100644 --- a/oioioi/contests/api.py +++ b/oioioi/contests/api.py @@ -4,8 +4,9 @@ from oioioi.base.utils import request_cached from oioioi.base.utils.api import make_path_coreapi_schema +from oioioi.contests.controllers import submission_template_context from oioioi.contests.forms import SubmissionFormForProblemInstance -from oioioi.contests.models import Contest, ProblemInstance +from oioioi.contests.models import Contest, ProblemInstance, Submission from oioioi.contests.serializers import ( ContestSerializer, ProblemSerializer, @@ -124,6 +125,67 @@ def get(self, request, contest_id): return Response(data) +# id submita +# liczba pkt +# data +# is_truncated max 20 i info czy jest więcej na stronie + +class GetUserProblemSubmissions(views.APIView): + permission_classes = ( + IsAuthenticated, + CanEnterContest, + ) + + schema = AutoSchema( + [ + make_path_coreapi_schema( + name='contest_id', + title="Contest id", + description="Id of the contest to which the problem you want to " + "query belongs. You can find this id after /c/ in urls " + "when using SIO 2 web interface.", + ), + make_path_coreapi_schema( + name='problem_short_name', + title="Problem short name", + description="Short name of the problem you want to query. " + "You can find it for example the in first column " + "of the problem list when using SIO 2 web interface.", + ), + ] + ) + + def get(self, request, contest_id, problem_short_name): + contest = get_object_or_404(Contest, id=contest_id) + problem_instance = get_object_or_404( + ProblemInstance, contest=contest, problem__short_name=problem_short_name + ) + problem = problem_instance.problem + + last_20_submits = ( + Submission.objects.filter(user=request.user) + .order_by('-date') + .select_related( + 'problem_instance', + 'problem_instance__contest', + 'problem_instance__round', + 'problem_instance__problem', + )[:20] + ) + submissions = [submission_template_context(request, s) for s in last_20_submits] + submissions_data = [] + for submission in submissions: + score = submission['submission'].score + submissions_data.append({ + 'id': submission['submission'].id, + 'score': score.to_int() if score else None, + 'date': submission['submission'].date, + }) + + + + return Response(submissions_data, status=status.HTTP_200_OK) + class GetProblemIdView(views.APIView): permission_classes = ( diff --git a/oioioi/contests/urls.py b/oioioi/contests/urls.py index 1b26c2e72..22b2e4eab 100644 --- a/oioioi/contests/urls.py +++ b/oioioi/contests/urls.py @@ -204,10 +204,15 @@ def glob_namespaced_patterns(namespace): name='api_contest_submit', ), re_path( - r'^api/c/(?P[a-z0-9_-]+)/problems/(?P[a-z0-9_-]+)$', + r'^api/c/(?P[a-z0-9_-]+)/problems/(?P[a-z0-9_-]+)/$', api.GetProblemIdView.as_view(), name='api_contest_get_problem_id', ), + re_path( + r'^api/c/(?P[a-z0-9_-]+)/problem_submissions/(?P[a-z0-9_-]+)/$', + api.GetUserProblemSubmissions.as_view(), + name='api_contest_get_user_problem_submissions', + ), re_path(r'^api/c/(?P[a-z0-9_-]+)/round_list/$', api.GetContestRounds.as_view() ), diff --git a/oioioi/default_settings.py b/oioioi/default_settings.py index fae0235d3..5fce163bc 100755 --- a/oioioi/default_settings.py +++ b/oioioi/default_settings.py @@ -877,7 +877,8 @@ 'rest_framework.throttling.UserRateThrottle' ], 'DEFAULT_THROTTLE_RATES': { - 'anon': '100/day', - 'user': '1000/day' - } + 'anon': '1000/day', + 'user': '1000/hour' + }, + 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema' } diff --git a/oioioi/problems/views.py b/oioioi/problems/views.py index 3aab16ba6..d03b4d3ae 100644 --- a/oioioi/problems/views.py +++ b/oioioi/problems/views.py @@ -141,7 +141,7 @@ def download_package_traceback_view(request, package_id): def add_or_update_problem(request, contest, template): if contest and contest.is_archived: raise PermissionDenied - + if 'problem' in request.GET: existing_problem = get_object_or_404(Problem, id=request.GET['problem']) if ( @@ -669,7 +669,7 @@ def get_report_row_begin_HTML_view(request, submission_id): return TemplateResponse( request, 'contests/my_submission_table_base_row_begin.html', - { + { 'record': submission_template_context(request, submission), 'show_scores': json.loads(request.POST.get('show_scores', "false")), 'can_admin': can_admin_problem_instance(request, submission.problem_instance) and From e7d8cecf097f6809d6dc134bbda7a520410586a3 Mon Sep 17 00:00:00 2001 From: Mateusz Kolpa Date: Wed, 22 May 2024 16:40:28 +0200 Subject: [PATCH 15/34] Add stuff --- oioioi/contests/api.py | 85 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 70 insertions(+), 15 deletions(-) diff --git a/oioioi/contests/api.py b/oioioi/contests/api.py index a66917717..ffa440f4b 100644 --- a/oioioi/contests/api.py +++ b/oioioi/contests/api.py @@ -125,10 +125,6 @@ def get(self, request, contest_id): return Response(data) -# id submita -# liczba pkt -# data -# is_truncated max 20 i info czy jest więcej na stronie class GetUserProblemSubmissions(views.APIView): permission_classes = ( @@ -162,7 +158,7 @@ def get(self, request, contest_id, problem_short_name): ) problem = problem_instance.problem - last_20_submits = ( + user_problem_submits = ( Submission.objects.filter(user=request.user) .order_by('-date') .select_related( @@ -170,21 +166,80 @@ def get(self, request, contest_id, problem_short_name): 'problem_instance__contest', 'problem_instance__round', 'problem_instance__problem', - )[:20] + ) ) + last_20_submits = user_problem_submits[:20] submissions = [submission_template_context(request, s) for s in last_20_submits] - submissions_data = [] - for submission in submissions: - score = submission['submission'].score - submissions_data.append({ - 'id': submission['submission'].id, - 'score': score.to_int() if score else None, - 'date': submission['submission'].date, - }) + submissions_data = {'submissions': []} + for submission_entry in submissions: + score = submission_entry['submission'].score + submissions_data['submissions'].append( + { + 'id': submission_entry['submission'].id, + 'score': score.to_int() if score else None, + 'date': submission_entry['submission'].date, + } + ) + submissions_data['is_truncated_to_20'] = len(user_problem_submits) > 20 + return Response(submissions_data, status=status.HTTP_200_OK) +class GetUserProblemSubmissionCode(views.APIView): + permission_classes = ( + IsAuthenticated, + CanEnterContest, + ) - return Response(submissions_data, status=status.HTTP_200_OK) + schema = AutoSchema( + [ + make_path_coreapi_schema( + name='contest_id', + title="Contest id", + description="Id of the contest to which the problem you want to " + "query belongs. You can find this id after /c/ in urls " + "when using SIO 2 web interface.", + ), + make_path_coreapi_schema( + name='problem_short_name', + title="Problem short name", + description="Short name of the problem you want to query. " + "You can find it for example the in first column " + "of the problem list when using SIO 2 web interface.", + ), + ] + ) + + def get(self, request, contest_id, problem_short_name): + contest = get_object_or_404(Contest, id=contest_id) + problem_instance = get_object_or_404( + ProblemInstance, contest=contest, problem__short_name=problem_short_name + ) + problem = problem_instance.problem + + # user_problem_submits = ( + # Submission.objects.filter(user=request.user) + # .order_by('-date') + # .select_related( + # 'problem_instance', + # 'problem_instance__contest', + # 'problem_instance__round', + # 'problem_instance__problem', + # ) + # ) + # last_20_submits = user_problem_submits[:20] + # submissions = [submission_template_context(request, s) for s in last_20_submits] + # submissions_data = {'submissions': []} + # for submission_entry in submissions: + # score = submission_entry['submission'].score + # submissions_data['submissions'].append( + # { + # 'id': submission_entry['submission'].id, + # 'score': score.to_int() if score else None, + # 'date': submission_entry['submission'].date, + # } + # ) + # submissions_data['is_truncated_to_20'] = len(user_problem_submits) > 20 + # return Response(submissions_data, status=status.HTTP_200_OK) class GetProblemIdView(views.APIView): From 487baf650441c8a1a727ee2c6c366688eeb107de Mon Sep 17 00:00:00 2001 From: Mateusz Kolpa Date: Wed, 22 May 2024 16:51:02 +0200 Subject: [PATCH 16/34] Fix stuff --- oioioi/default_settings.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/oioioi/default_settings.py b/oioioi/default_settings.py index 5fce163bc..bfeaed4a6 100755 --- a/oioioi/default_settings.py +++ b/oioioi/default_settings.py @@ -880,5 +880,9 @@ 'anon': '1000/day', 'user': '1000/hour' }, - 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema' + 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema', + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework.authentication.BasicAuthentication', + 'rest_framework.authentication.SessionAuthentication', + ] } From 2833c98eda563d04eceae1052d73d9f44e7489fb Mon Sep 17 00:00:00 2001 From: Mateusz Kolpa Date: Wed, 22 May 2024 16:52:54 +0200 Subject: [PATCH 17/34] Better fix stuff --- oioioi/default_settings.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/oioioi/default_settings.py b/oioioi/default_settings.py index bfeaed4a6..6c588c746 100755 --- a/oioioi/default_settings.py +++ b/oioioi/default_settings.py @@ -871,18 +871,15 @@ # Experimental USE_ACE_EDITOR = False -REST_FRAMEWORK = { - 'DEFAULT_THROTTLE_CLASSES': [ - 'rest_framework.throttling.AnonRateThrottle', - 'rest_framework.throttling.UserRateThrottle' - ], - 'DEFAULT_THROTTLE_RATES': { - 'anon': '1000/day', - 'user': '1000/hour' - }, - 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema', - 'DEFAULT_AUTHENTICATION_CLASSES': [ - 'rest_framework.authentication.BasicAuthentication', - 'rest_framework.authentication.SessionAuthentication', - ] +REST_FRAMEWORK['DEFAULT_THROTTLE_CLASSES'] = [ + 'rest_framework.throttling.AnonRateThrottle', + 'rest_framework.throttling.UserRateThrottle' +] +REST_FRAMEWORK['DEFAULT_THROTTLE_RATES'] = { + 'anon': '1000/day', + 'user': '1000/hour' } +REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] = [ + 'rest_framework.authentication.BasicAuthentication', + 'rest_framework.authentication.SessionAuthentication', +] From 237a94c10efaef6e1860abbfdada6f245ade1f66 Mon Sep 17 00:00:00 2001 From: Mateusz Kolpa Date: Wed, 22 May 2024 16:54:33 +0200 Subject: [PATCH 18/34] Even better fix stuff --- oioioi/default_settings.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/oioioi/default_settings.py b/oioioi/default_settings.py index 6c588c746..b84aa2093 100755 --- a/oioioi/default_settings.py +++ b/oioioi/default_settings.py @@ -879,7 +879,3 @@ 'anon': '1000/day', 'user': '1000/hour' } -REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] = [ - 'rest_framework.authentication.BasicAuthentication', - 'rest_framework.authentication.SessionAuthentication', -] From 1c35b78f807ac4aab887259b14fe3f05950b69d0 Mon Sep 17 00:00:00 2001 From: Mateusz Kolpa Date: Wed, 22 May 2024 17:19:37 +0200 Subject: [PATCH 19/34] Fix getProblemSubmission --- oioioi/contests/api.py | 16 +++++++++------- oioioi/contests/urls.py | 5 +++++ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/oioioi/contests/api.py b/oioioi/contests/api.py index ffa440f4b..ed1b7bc43 100644 --- a/oioioi/contests/api.py +++ b/oioioi/contests/api.py @@ -156,10 +156,11 @@ def get(self, request, contest_id, problem_short_name): problem_instance = get_object_or_404( ProblemInstance, contest=contest, problem__short_name=problem_short_name ) - problem = problem_instance.problem user_problem_submits = ( - Submission.objects.filter(user=request.user) + Submission.objects.filter( + user=request.user, problem_instance=problem_instance + ) .order_by('-date') .select_related( 'problem_instance', @@ -210,11 +211,12 @@ class GetUserProblemSubmissionCode(views.APIView): ) def get(self, request, contest_id, problem_short_name): - contest = get_object_or_404(Contest, id=contest_id) - problem_instance = get_object_or_404( - ProblemInstance, contest=contest, problem__short_name=problem_short_name - ) - problem = problem_instance.problem + pass + # contest = get_object_or_404(Contest, id=contest_id) + # problem_instance = get_object_or_404( + # ProblemInstance, contest=contest, problem__short_name=problem_short_name + # ) + # problem = problem_instance.problem # user_problem_submits = ( # Submission.objects.filter(user=request.user) diff --git a/oioioi/contests/urls.py b/oioioi/contests/urls.py index 22b2e4eab..b582e2f38 100644 --- a/oioioi/contests/urls.py +++ b/oioioi/contests/urls.py @@ -213,6 +213,11 @@ def glob_namespaced_patterns(namespace): api.GetUserProblemSubmissions.as_view(), name='api_contest_get_user_problem_submissions', ), + re_path( + r'^api/c/(?P[a-z0-9_-]+)/problem_submission_code/(?P[a-z0-9_-]+)/$', + api.GetUserProblemSubmissionCode.as_view(), + name='api_contest_get_user_problem_submissions', + ), re_path(r'^api/c/(?P[a-z0-9_-]+)/round_list/$', api.GetContestRounds.as_view() ), From 20e26f8b51323d8e32ddab6e20e0f7cd0959f7ae Mon Sep 17 00:00:00 2001 From: Mateusz Kolpa Date: Wed, 22 May 2024 17:56:33 +0200 Subject: [PATCH 20/34] Add stuff (like code) --- oioioi/contests/api.py | 91 ++++++++++++++++++----------------------- oioioi/contests/urls.py | 24 ++++++----- 2 files changed, 52 insertions(+), 63 deletions(-) diff --git a/oioioi/contests/api.py b/oioioi/contests/api.py index ed1b7bc43..c317e9156 100644 --- a/oioioi/contests/api.py +++ b/oioioi/contests/api.py @@ -23,6 +23,7 @@ from oioioi.base.permissions import enforce_condition, not_anonymous from oioioi.problems.utils import query_statement +from oioioi.programs.utils import decode_str, get_submission_source_file_or_error from rest_framework import permissions, status, views from rest_framework.parsers import MultiPartParser from rest_framework.permissions import IsAuthenticated @@ -173,12 +174,23 @@ def get(self, request, contest_id, problem_short_name): submissions = [submission_template_context(request, s) for s in last_20_submits] submissions_data = {'submissions': []} for submission_entry in submissions: - score = submission_entry['submission'].score + print(">>> CODE", submission_entry['submission'].comment) + score = ( + submission_entry['submission'].score + if submission_entry['can_see_score'] + else None + ) + status = ( + submission_entry['submission'].status + if submission_entry['can_see_status'] + else None + ) submissions_data['submissions'].append( { 'id': submission_entry['submission'].id, - 'score': score.to_int() if score else None, 'date': submission_entry['submission'].date, + 'score': score.to_int() if score else None, + 'status': status, } ) submissions_data['is_truncated_to_20'] = len(user_problem_submits) > 20 @@ -191,57 +203,32 @@ class GetUserProblemSubmissionCode(views.APIView): CanEnterContest, ) - schema = AutoSchema( - [ - make_path_coreapi_schema( - name='contest_id', - title="Contest id", - description="Id of the contest to which the problem you want to " - "query belongs. You can find this id after /c/ in urls " - "when using SIO 2 web interface.", - ), - make_path_coreapi_schema( - name='problem_short_name', - title="Problem short name", - description="Short name of the problem you want to query. " - "You can find it for example the in first column " - "of the problem list when using SIO 2 web interface.", - ), - ] - ) - - def get(self, request, contest_id, problem_short_name): + # schema = AutoSchema( + # [ + # make_path_coreapi_schema( + # name='contest_id', + # title="Contest id", + # description="Id of the contest to which the problem you want to " + # "query belongs. You can find this id after /c/ in urls " + # "when using SIO 2 web interface.", + # ), + # # make_path_coreapi_schema( + # # name='problem_short_name', + # # title="Problem short name", + # # description="Short name of the problem you want to query. " + # # "You can find it for example the in first column " + # # "of the problem list when using SIO 2 web interface.", + # # ), + # ] + # ) + + def get(self, request, contest_id, submission_id): pass - # contest = get_object_or_404(Contest, id=contest_id) - # problem_instance = get_object_or_404( - # ProblemInstance, contest=contest, problem__short_name=problem_short_name - # ) - # problem = problem_instance.problem - - # user_problem_submits = ( - # Submission.objects.filter(user=request.user) - # .order_by('-date') - # .select_related( - # 'problem_instance', - # 'problem_instance__contest', - # 'problem_instance__round', - # 'problem_instance__problem', - # ) - # ) - # last_20_submits = user_problem_submits[:20] - # submissions = [submission_template_context(request, s) for s in last_20_submits] - # submissions_data = {'submissions': []} - # for submission_entry in submissions: - # score = submission_entry['submission'].score - # submissions_data['submissions'].append( - # { - # 'id': submission_entry['submission'].id, - # 'score': score.to_int() if score else None, - # 'date': submission_entry['submission'].date, - # } - # ) - # submissions_data['is_truncated_to_20'] = len(user_problem_submits) > 20 - # return Response(submissions_data, status=status.HTTP_200_OK) + contest = get_object_or_404(Contest, id=contest_id) + submission = get_object_or_404(Submission, user=request.user, id=submission_id) + source_file = get_submission_source_file_or_error(request, int(submission_id)) + raw_source, decode_error = decode_str(source_file.read()) + return Response(raw_source, status=status.HTTP_200_OK) class GetProblemIdView(views.APIView): diff --git a/oioioi/contests/urls.py b/oioioi/contests/urls.py index b582e2f38..e25fa3dba 100644 --- a/oioioi/contests/urls.py +++ b/oioioi/contests/urls.py @@ -150,7 +150,11 @@ def glob_namespaced_patterns(namespace): name='user_info_redirect', ), re_path(r'^admin/', admin.contest_site.urls), - re_path(r'^archive/confirm$', views.confirm_archive_contest, name='confirm_archive_contest'), + re_path( + r'^archive/confirm$', + views.confirm_archive_contest, + name='confirm_archive_contest', + ), re_path(r'^unarchive/$', views.unarchive_contest, name='unarchive_contest'), ] @@ -193,11 +197,7 @@ def glob_namespaced_patterns(namespace): if settings.USE_API: nonc_patterns += [ # the contest information is managed manually and added after api prefix - re_path( - r'^api/contest_list', - api.contest_list, - name="api_contest_list" - ), + re_path(r'^api/contest_list', api.contest_list, name="api_contest_list"), re_path( r'^api/c/(?P[a-z0-9_-]+)/submit/(?P[a-z0-9_-]+)$', api.SubmitContestSolutionView.as_view(), @@ -216,13 +216,15 @@ def glob_namespaced_patterns(namespace): re_path( r'^api/c/(?P[a-z0-9_-]+)/problem_submission_code/(?P[a-z0-9_-]+)/$', api.GetUserProblemSubmissionCode.as_view(), - name='api_contest_get_user_problem_submissions', + name='api_contest_get_user_problem_submission_code', ), - re_path(r'^api/c/(?P[a-z0-9_-]+)/round_list/$', - api.GetContestRounds.as_view() + re_path( + r'^api/c/(?P[a-z0-9_-]+)/round_list/$', + api.GetContestRounds.as_view(), ), - re_path(r'^api/c/(?P[a-z0-9_-]+)/problem_list/$', - api.GetContestProblems.as_view() + re_path( + r'^api/c/(?P[a-z0-9_-]+)/problem_list/$', + api.GetContestProblems.as_view(), ), re_path( r'^api/problemset/submit/(?P[0-9a-zA-Z-_=]+)$', From 736f2b7cccea633a242ccc2ebbe8288e757e7018 Mon Sep 17 00:00:00 2001 From: Mateusz Kolpa Date: Wed, 5 Jun 2024 17:10:20 +0200 Subject: [PATCH 21/34] Update stuff --- oioioi/contests/api.py | 37 ++++++++++++++++--------------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/oioioi/contests/api.py b/oioioi/contests/api.py index c317e9156..0193d702c 100644 --- a/oioioi/contests/api.py +++ b/oioioi/contests/api.py @@ -203,29 +203,24 @@ class GetUserProblemSubmissionCode(views.APIView): CanEnterContest, ) - # schema = AutoSchema( - # [ - # make_path_coreapi_schema( - # name='contest_id', - # title="Contest id", - # description="Id of the contest to which the problem you want to " - # "query belongs. You can find this id after /c/ in urls " - # "when using SIO 2 web interface.", - # ), - # # make_path_coreapi_schema( - # # name='problem_short_name', - # # title="Problem short name", - # # description="Short name of the problem you want to query. " - # # "You can find it for example the in first column " - # # "of the problem list when using SIO 2 web interface.", - # # ), - # ] - # ) + schema = AutoSchema( + [ + make_path_coreapi_schema( + name='contest_id', + title="Name of the contest", + description="Id of the contest to which the problem you want to " + "query belongs. You can find this id after /c/ in urls " + "when using SIO 2 web interface.", + ), + make_path_coreapi_schema( + name='submission_id', + title="Submission id", + description="You can query submission ID list at problem_submissions endpoint.", + ), + ] + ) def get(self, request, contest_id, submission_id): - pass - contest = get_object_or_404(Contest, id=contest_id) - submission = get_object_or_404(Submission, user=request.user, id=submission_id) source_file = get_submission_source_file_or_error(request, int(submission_id)) raw_source, decode_error = decode_str(source_file.read()) return Response(raw_source, status=status.HTTP_200_OK) From cc2b53daaa53e33d0cb02cfe0cd8e0b8c9ee6f70 Mon Sep 17 00:00:00 2001 From: Mateusz Kolpa Date: Wed, 5 Jun 2024 18:25:50 +0200 Subject: [PATCH 22/34] Fix stuff --- oioioi/contests/api.py | 15 +- oioioi/contests/tests/tests.py | 307 +++++++++++++++++++++------------ oioioi/contests/urls.py | 2 + 3 files changed, 215 insertions(+), 109 deletions(-) diff --git a/oioioi/contests/api.py b/oioioi/contests/api.py index 0193d702c..7982cea74 100644 --- a/oioioi/contests/api.py +++ b/oioioi/contests/api.py @@ -23,6 +23,7 @@ from oioioi.base.permissions import enforce_condition, not_anonymous from oioioi.problems.utils import query_statement +from oioioi.programs.models import ProgramSubmission from oioioi.programs.utils import decode_str, get_submission_source_file_or_error from rest_framework import permissions, status, views from rest_framework.parsers import MultiPartParser @@ -180,7 +181,7 @@ def get(self, request, contest_id, problem_short_name): if submission_entry['can_see_score'] else None ) - status = ( + submission_status = ( submission_entry['submission'].status if submission_entry['can_see_status'] else None @@ -190,7 +191,7 @@ def get(self, request, contest_id, problem_short_name): 'id': submission_entry['submission'].id, 'date': submission_entry['submission'].date, 'score': score.to_int() if score else None, - 'status': status, + 'status': submission_status, } ) submissions_data['is_truncated_to_20'] = len(user_problem_submits) > 20 @@ -221,9 +222,17 @@ class GetUserProblemSubmissionCode(views.APIView): ) def get(self, request, contest_id, submission_id): + # Make sure user made this submission, not somebody else. + submission = get_object_or_404(ProgramSubmission, id=submission_id) + if submission.user != request.user: + raise Http404("Submission not found.") + source_file = get_submission_source_file_or_error(request, int(submission_id)) raw_source, decode_error = decode_str(source_file.read()) - return Response(raw_source, status=status.HTTP_200_OK) + return Response( + {"code_language": source_file.name.split('.')[-1], "code": raw_source}, + status=status.HTTP_200_OK, + ) class GetProblemIdView(views.APIView): diff --git a/oioioi/contests/tests/tests.py b/oioioi/contests/tests/tests.py index 4fb165454..bf44baa79 100755 --- a/oioioi/contests/tests/tests.py +++ b/oioioi/contests/tests/tests.py @@ -254,7 +254,6 @@ def check_order_in_response(self, response, order, error_msg): self.assertTrue(test_first_index < test_second_index, error_msg) - # TODO: expand this TestCase @override_settings(CONTEST_MODE=ContestMode.neutral) class TestSubmissionListFilters(TestCase): @@ -317,9 +316,13 @@ def test_search(self): self.assertContains(response, '0 submissions') response = self.client.get(self.url, {'pi': 'Zadanie z tłumaczeniami'}) self.assertContains(response, '1 submission') - response = self.client.get(self.url, {'pi': 'Zadanie z tłumaczeniami', 'q': 'Zadanie'}) + response = self.client.get( + self.url, {'pi': 'Zadanie z tłumaczeniami', 'q': 'Zadanie'} + ) self.assertContains(response, '1 submission') - response = self.client.get(self.url, {'pi': 'Zadanie z tłumaczeniami', 'q': 'Sumżyce'}) + response = self.client.get( + self.url, {'pi': 'Zadanie z tłumaczeniami', 'q': 'Sumżyce'} + ) self.assertContains(response, '0 submissions') @@ -1510,7 +1513,9 @@ def get_time(hour): ContestAttachment.objects.create( contest=contest, description='round-attachment-pub-date-before', - content=ContentFile(b'content-of-roundatt-before', name='roundatt-before.txt'), + content=ContentFile( + b'content-of-roundatt-before', name='roundatt-before.txt' + ), round=round, pub_date=get_time(19), ), @@ -1521,7 +1526,9 @@ def get_time(hour): ContestAttachment.objects.create( contest=contest, description='round-attachment-pub-date-after', - content=ContentFile(b'content-of-roundatt-after', name='roundatt-after.txt'), + content=ContentFile( + b'content-of-roundatt-after', name='roundatt-after.txt' + ), round=round, pub_date=get_time(22), ), @@ -1542,17 +1549,17 @@ def check(visible, invisible): # File list response = self.client.get(list_url) self.assertEqual(response.status_code, 200) - for (att, content, name) in visible: + for att, content, name in visible: self.assertContains(response, name) self.assertContains(response, att.description) - for (att, content, name) in invisible: + for att, content, name in invisible: self.assertNotContains(response, name) self.assertNotContains(response, att.description) for f in response.context['files']: self.assertEqual(f['admin_only'], False) # Actual accessibility - for (att, content, name) in visible: + for att, content, name in visible: response = self.client.get( reverse( get_attachment_urlpattern_name(att), @@ -1560,7 +1567,7 @@ def check(visible, invisible): ) ) self.assertStreamingEqual(response, content) - for (att, content, name) in invisible: + for att, content, name in invisible: check_not_accessible( self, get_attachment_urlpattern_name(att), @@ -1571,7 +1578,7 @@ def check(visible, invisible): self.assertTrue(self.client.login(username='test_admin')) response = self.client.get(list_url) self.assertEqual(response.status_code, 200) - for (att, content, name) in visible + invisible: + for att, content, name in visible + invisible: self.assertContains(response, name) self.assertContains(response, att.description) invisible_names = set([f[2] for f in invisible]) @@ -1579,7 +1586,7 @@ def check(visible, invisible): self.assertEqual(f['admin_only'], f['name'] in invisible_names) # Actual accessibility as an admin - for (att, content, name) in visible + invisible: + for att, content, name in visible + invisible: response = self.client.get( reverse( get_attachment_urlpattern_name(att), @@ -1592,7 +1599,12 @@ def check(visible, invisible): check([ca, cb, pa, ra, rb, rc], []) # Before all dates with fake_time(get_time(18)): - check([ca,], [cb, pa, ra, rb, rc]) + check( + [ + ca, + ], + [cb, pa, ra, rb, rc], + ) # Before the round start, but after the pub_dates before it with fake_time(get_time(20)): check([ca, cb], [pa, ra, rb, rc]) @@ -1696,7 +1708,7 @@ def factory(self, user, timestamp=None): request = factory.request() request.contest = self.contest request.user = user - request.timestamp = timestamp or self.during + request.timestamp = timestamp or self.during return request def setUp(self): @@ -1746,15 +1758,22 @@ def check_perms(user, perms): self.assertEqual(f(request), f in perms) check_perms(self.superuser, all_funlist) - check_perms(self.cowner, [ - is_contest_owner, - is_contest_admin, - is_contest_basicadmin, - can_enter_contest, - ]) + check_perms( + self.cowner, + [ + is_contest_owner, + is_contest_admin, + is_contest_basicadmin, + can_enter_contest, + ], + ) check_perms( self.cadmin, - [is_contest_admin, is_contest_basicadmin, can_enter_contest,], + [ + is_contest_admin, + is_contest_basicadmin, + can_enter_contest, + ], ) check_perms(self.badmin, [is_contest_basicadmin, can_enter_contest]) check_perms(self.observer, [is_contest_observer, can_enter_contest]) @@ -1767,11 +1786,14 @@ def check_perms(user, perms): self.assertEqual(user.has_perm(p, self.contest), p in perms) check_perms(self.superuser, self.perms_list) - check_perms(self.cowner, [ - 'contests.contest_owner', - 'contests.contest_admin', - 'contests.contest_basicadmin', - ]) + check_perms( + self.cowner, + [ + 'contests.contest_owner', + 'contests.contest_admin', + 'contests.contest_basicadmin', + ], + ) check_perms( self.cadmin, ['contests.contest_admin', 'contests.contest_basicadmin'], @@ -1837,11 +1859,18 @@ def test_contestpermission_admin(self): ) # Only superusers and contest owners should see these pages for u in ( - self.observer, self.cadmin, self.badmin, self.pdata, self.user, + self.observer, + self.cadmin, + self.badmin, + self.pdata, + self.user, ): self.client.force_login(u) for url in ( - list_url, add_url, list_url_nocontest, add_url_nocontest, + list_url, + add_url, + list_url_nocontest, + add_url_nocontest, ): self.assertEqual(self.client.get(url).status_code, 403) @@ -1939,7 +1968,9 @@ def test_contestpermission_admin(self): tmp_perm.save() change_url = reverse( 'noncontest:oioioiadmin:contests_contestpermission_change', - kwargs={'object_id': tmp_perm.id,}, + kwargs={ + 'object_id': tmp_perm.id, + }, ) self.try_post_perm( change_url, @@ -1956,13 +1987,15 @@ def test_contestpermission_admin(self): 'post': 'yes', 'action': 'delete_selected', '_selected_action': ContestPermission.objects.get( - permission='contests.contest_owner').id, + permission='contests.contest_owner' + ).id, } data_admin = { 'post': 'yes', 'action': 'delete_selected', '_selected_action': ContestPermission.objects.get( - permission='contests.contest_admin').id, + permission='contests.contest_admin' + ).id, } data_different_contest = { 'post': 'yes', @@ -1991,42 +2024,54 @@ def test_contestpermission_admin(self): follow=True, ) self.assertEqual(resp.status_code, 403) - self.assertEqual(ContestPermission.objects.filter( - id=perm_different_contest.id).count(), 1) + self.assertEqual( + ContestPermission.objects.filter(id=perm_different_contest.id).count(), 1 + ) - self.assertEqual(ContestPermission.objects.filter( - permission='contests.contest_admin').count(), 1) + self.assertEqual( + ContestPermission.objects.filter( + permission='contests.contest_admin' + ).count(), + 1, + ) resp = self.client.post(list_url, data_admin) self.assertEqual(resp.status_code, 302) - self.assertEqual(ContestPermission.objects.filter( - permission='contests.contest_admin').count(), 0) + self.assertEqual( + ContestPermission.objects.filter( + permission='contests.contest_admin' + ).count(), + 0, + ) self.client.force_login(self.superuser) resp = self.client.post(list_url, data_different_contest, follow=True) self.assertEqual(resp.status_code, 200) - self.assertEqual(ContestPermission.objects.filter( - id=perm_different_contest.id).count(), 1) + self.assertEqual( + ContestPermission.objects.filter(id=perm_different_contest.id).count(), 1 + ) resp = self.client.post( list_url_another_contest, data_different_contest, ) self.assertEqual(resp.status_code, 302) - self.assertEqual(ContestPermission.objects.filter( - id=perm_different_contest.id).count(), 0) + self.assertEqual( + ContestPermission.objects.filter(id=perm_different_contest.id).count(), 0 + ) resp = self.client.post(list_url, data_owner) self.assertEqual(resp.status_code, 302) - self.assertEqual(ContestPermission.objects.filter( - permission='contests.contest_owner').count(), 0) + self.assertEqual( + ContestPermission.objects.filter( + permission='contests.contest_owner' + ).count(), + 0, + ) def test_menu(self): unregister_contest_dashboard_view(simpleui_contest_dashboard) unregister_contest_dashboard_view(teachers_contest_dashboard) - url = reverse( - 'default_contest_view', - kwargs={'contest_id': self.contest.id} - ) + url = reverse('default_contest_view', kwargs={'contest_id': self.contest.id}) self.client.force_login(self.cadmin) response = self.client.get(url, follow=True) @@ -3466,7 +3511,7 @@ def contest_submit(self, contest, pi, *args, **kwargs): 'api_contest_submit', {'contest_name': contest.id, 'problem_short_name': pi.short_name}, *args, - **kwargs + **kwargs, ) def test_simple_submission(self): @@ -3559,15 +3604,41 @@ def test_problemset_submission(self): class TestAPIContestList(TestCase): - fixtures = ['test_users', 'test_contest', 'test_submission'] + fixtures = [ + 'test_users', + 'test_contest', + 'test_submission', + 'test_problem_instance', + ] - def test_rights(self): + def test_rights_anon(self): contest_list = reverse('api_contest_list') request_anon = self.client.get(contest_list) self.assertEqual(403, request_anon.status_code) self.client.force_authenticate(user=User.objects.get(username='test_user2')) + def test_rights_logged_in(self): + contest_list = reverse('api_contest_list') + self.client.force_authenticate(user=User.objects.get(username='test_user1')) + request_auth = self.client.get(contest_list) + self.assertEqual(200, request_auth.status_code) + + +class TestAPIRoundList(TestCase): + fixtures = [ + 'test_users', + 'test_contest', + 'test_full_package', + 'test_problem_instance', + 'test_submission', + ] + + def test_rights_anon(self): + contest_id = Contest.objects.get(pk='c').id + round_list = reverse('api_round_list', kwargs={'contest_id': contest_id}) + request = self.client.get(round_list) + print(request) class TestManyRoundsNoEnd(TestCase): @@ -3689,7 +3760,9 @@ def set_registration_availability(rvc, enabled, available_from=None, available_t rvc.save() -def check_registration(self, expected_status_code, availability, available_from=None, available_to=None): +def check_registration( + self, expected_status_code, availability, available_from=None, available_to=None +): contest = Contest.objects.get() contest.controller_name = 'oioioi.oi.controllers.OIContestController' contest.save() @@ -3705,10 +3778,7 @@ def check_registration(self, expected_status_code, availability, available_from= class TestOpenRegistration(TestCase): - fixtures = [ - 'test_users', - 'test_contest' - ] + fixtures = ['test_users', 'test_contest'] def test_open_registration(self): check_registration(self, 200, 'YES') @@ -3734,13 +3804,14 @@ def test_configured_registration_closed_after(self): available_to = now - timedelta(minutes=5) check_registration(self, 403, 'CONFIG', available_from, available_to) + class TestRulesVisibility(TestCase): fixtures = [ 'test_users', 'test_participant', 'test_contest', 'test_full_package', - 'test_three_problem_instances.json' + 'test_three_problem_instances.json', ] controller_names = [ @@ -3755,7 +3826,7 @@ class TestRulesVisibility(TestCase): 'oioioi.oi.controllers.BOIOnlineContestController', 'oioioi.pa.controllers.PAContestController', 'oioioi.pa.controllers.PAFinalsContestController', - 'oioioi.programs.controllers.ProgrammingContestController' + 'oioioi.programs.controllers.ProgrammingContestController', ] # left to fill in when added, in order of the controllers above @@ -3767,7 +3838,6 @@ class TestRulesVisibility(TestCase): "The lower the total time, the higher the rank.
" "Compilation errors and system errors are not considered as an incorrect submission.
" "The ranking is frozen 60 minutes before the end of the round.", - "The solutions are judged on real-time. " "The submission is correct if it passes all the test cases.
" "Participants are ranked by the number of solved problems. " @@ -3775,7 +3845,6 @@ class TestRulesVisibility(TestCase): "The lower the total time, the higher the rank.
" "Compilation errors and system errors are not considered as an incorrect submission.
" "The ranking is frozen 60 minutes before the end of the round.", - "The solutions are judged on real-time. " "The submission is correct if it passes all the test cases.
" "Participants are ranked by the number of solved problems. " @@ -3783,39 +3852,31 @@ class TestRulesVisibility(TestCase): "The lower the total time, the higher the rank.
" "Compilation errors and system errors are not considered as an incorrect submission.
" "The ranking is frozen 15 minutes before the end of the trial rounds and 60 minutes before the end of the normal rounds.", - "The submissions are scored from 0 to 100 points.
" "The participant can submit to finished rounds, but a multiplier is applied to the score of such submissions.", - "The solutions are judged with sio2jail. They can be scored from 0 to 100 points. " "If the submission runs for longer than half of the time limit, the points for this test are linearly decreased to 0.
" "The score for a group of test cases is the minimum score for any of the test cases.
" "The ranking is determined by the total score.
" "Until the end of the contest, participants can only see scoring of their submissions on example test cases. " "Full scoring is available after the end of the contest.", - "The solutions are judged with sio2jail. They can be scored from 0 to 100 points. " "If the submission runs for longer than half of the time limit, the points for this test are linearly decreased to 0.
" "The score for a group of test cases is the minimum score for any of the test cases.
" "The ranking is determined by the total score.
" "Until the end of the contest, participants can only see scoring of their submissions on example test cases. " "Full scoring is available after the end of the contest.", - "The solutions are judged with sio2jail. They can be scored from 0 to 100 points. " "If the submission runs for longer than half of the time limit, the points for this test are linearly decreased to 0.
" "The score for a group of test cases is the minimum score for any of the test cases
." "The ranking is determined by the total score.
" "Full scoring of the submissions can be revealed during the contest.", - '', - '', - "The submissions are judged on real-time. All problems have 10 test groups, each worth 1 point. " "If any of the tests in a group fails, the group is worth 0 points.
" "The full scoring is available after the end of the round." "The ranking is determined by the total score and number of 10-score submissions, 9-score, 8-score etc.", - "The solutions are judged on real-time. " "The submission is correct if it passes all the test cases.
" "Participants are ranked by the number of solved problems. " @@ -3823,38 +3884,31 @@ class TestRulesVisibility(TestCase): "The lower the total time, the higher the rank.
" "Compilation errors and system errors are not considered as an incorrect submission.
" "The ranking is frozen 15 minutes before the end of the trial rounds and 60 minutes before the end of the normal rounds.", - "The submissions are scored on a set of groups of test cases. Each group is worth a certain number of points.
" "The score is a sum of the scores of all groups. The ranking is determined by the total score.
" - "The full scoring is available after the results date for the round." + "The full scoring is available after the results date for the round.", ] visibility_dates = [ + [datetime(2012, 8, 15, 20, 27, 58, tzinfo=timezone.utc), None], [ datetime(2012, 8, 15, 20, 27, 58, tzinfo=timezone.utc), - None + datetime(2013, 4, 20, 21, 37, 13, tzinfo=timezone.utc), ], - [ - datetime(2012, 8, 15, 20, 27, 58, tzinfo=timezone.utc), - datetime(2013, 4, 20, 21, 37, 13, tzinfo=timezone.utc) - ], - [ - None, - None - ] + [None, None], ] def _set_problem_limits(self, url, limits_list): for i in range(len(limits_list)): - problem = ProblemInstance.objects.get(pk=i+1) + problem = ProblemInstance.objects.get(pk=i + 1) problem.submissions_limit = limits_list[i] problem.save() - + response = self.client.get(url, follow=True) self.assertEqual(response.status_code, 200) return response - + def _set_results_dates(self, url, dates): round = Round.objects.get() round.results_date = dates[0] @@ -3877,7 +3931,7 @@ def _change_controller(self, public_results=False): 'oioioi.programs.controllers.ProgrammingContestController' ) contest.save() - + def test_dashboard_view(self): for c in self.controller_names: contest = Contest.objects.get() @@ -3888,7 +3942,7 @@ def test_dashboard_view(self): response = self.client.get(url, follow=True) self.assertEqual(response.status_code, 200) self.assertContains(response, "Rules") - + def test_contest_type(self): for c, d in zip(self.controller_names, self.scoring_descriptions): contest = Contest.objects.get() @@ -3922,21 +3976,31 @@ def test_problem_limits(self): self.assertTrue(self.client.login(username='test_user')) url = reverse('contest_rules', kwargs={'contest_id': 'c'}) response = self._set_problem_limits(url, [0, 0, 0]) - self.assertContains(response, "There is a limit of infinity submissions for each problem.") + self.assertContains( + response, "There is a limit of infinity submissions for each problem." + ) response = self._set_problem_limits(url, [0, 10, 0]) - self.assertContains(response, "There is a limit of 10 to infinity submissions, depending on a problem.") + self.assertContains( + response, + "There is a limit of 10 to infinity submissions, depending on a problem.", + ) response = self._set_problem_limits(url, [20, 10, 0]) - self.assertContains(response, "There is a limit of 10 to infinity submissions, depending on a problem.") + self.assertContains( + response, + "There is a limit of 10 to infinity submissions, depending on a problem.", + ) response = self._set_problem_limits(url, [10, 10, 10]) - self.assertContains(response, "There is a limit of 10 submissions for each problem.") + self.assertContains( + response, "There is a limit of 10 submissions for each problem." + ) def test_contest_dates(self): times = [ fake_time(datetime(2012, 8, 5, 12, 37, 45, tzinfo=timezone.utc)), - fake_time(datetime(2012, 8, 15, 15, 16, 18, tzinfo=timezone.utc)) + fake_time(datetime(2012, 8, 15, 15, 16, 18, tzinfo=timezone.utc)), ] for t in times: @@ -3945,7 +4009,7 @@ def test_contest_dates(self): contest = Contest.objects.get() contest.controller_name = c contest.save() - + round = Round.objects.get() round.end_date = None round.save() @@ -3954,13 +4018,18 @@ def test_contest_dates(self): url = reverse('contest_rules', kwargs={'contest_id': 'c'}) response = self.client.get(url, follow=True) self.assertEqual(response.status_code, 200) - self.assertContains(response, "The contest starts on 2011-07-31 20:27:58.") + self.assertContains( + response, "The contest starts on 2011-07-31 20:27:58." + ) round.end_date = datetime(2012, 8, 10, 0, 0, tzinfo=timezone.utc) round.save() response = self.client.get(url, follow=True) self.assertEqual(response.status_code, 200) - self.assertContains(response, "The contest starts on 2011-07-31 20:27:58 and ends on 2012-08-10 00:00:00.") + self.assertContains( + response, + "The contest starts on 2011-07-31 20:27:58 and ends on 2012-08-10 00:00:00.", + ) def test_ranking_visibility(self): # here we don't check for individual contests, as it would be hard to get the separate_public_results() @@ -3970,39 +4039,66 @@ def test_ranking_visibility(self): with fake_time(datetime(2012, 8, 4, 13, 46, 37, tzinfo=timezone.utc)): response = self._set_results_dates(url, self.visibility_dates[0]) - self.assertContains(response, "In round Round 1, your results as well as " \ - "public ranking will be visible after 2012-08-15 20:27:58.") - + self.assertContains( + response, + "In round Round 1, your results as well as " + "public ranking will be visible after 2012-08-15 20:27:58.", + ) + self._change_controller(public_results=True) response = self._set_results_dates(url, self.visibility_dates[1]) - self.assertContains(response, "In round Round 1, your results will be visible after 2012-08-15 20:27:58" \ - " and the public ranking will be visible after 2013-04-20 21:37:13.") + self.assertContains( + response, + "In round Round 1, your results will be visible after 2012-08-15 20:27:58" + " and the public ranking will be visible after 2013-04-20 21:37:13.", + ) response = self._set_results_dates(url, self.visibility_dates[2]) - self.assertContains(response, "In round Round 1, your results as well as public ranking will be visible immediately.") + self.assertContains( + response, + "In round Round 1, your results as well as public ranking will be visible immediately.", + ) with fake_time(datetime(2012, 12, 24, 11, 23, 56, tzinfo=timezone.utc)): response = self._set_results_dates(url, self.visibility_dates[0]) - self.assertContains(response, "In round Round 1, your results as well as public ranking will be visible immediately.") - + self.assertContains( + response, + "In round Round 1, your results as well as public ranking will be visible immediately.", + ) + self._change_controller(public_results=True) response = self._set_results_dates(url, self.visibility_dates[1]) - self.assertContains(response, "In round Round 1, your results will be visible immediately" \ - " and the public ranking will be visible after 2013-04-20 21:37:13.") + self.assertContains( + response, + "In round Round 1, your results will be visible immediately" + " and the public ranking will be visible after 2013-04-20 21:37:13.", + ) response = self._set_results_dates(url, self.visibility_dates[2]) - self.assertContains(response, "In round Round 1, your results as well as public ranking will be visible immediately.") + self.assertContains( + response, + "In round Round 1, your results as well as public ranking will be visible immediately.", + ) with fake_time(datetime(2014, 8, 26, 11, 23, 56, tzinfo=timezone.utc)): response = self._set_results_dates(url, self.visibility_dates[0]) - self.assertContains(response, "In round Round 1, your results as well as public ranking will be visible immediately.") - + self.assertContains( + response, + "In round Round 1, your results as well as public ranking will be visible immediately.", + ) + self._change_controller(public_results=True) response = self._set_results_dates(url, self.visibility_dates[1]) - self.assertContains(response, "In round Round 1, your results as well as public ranking will be visible immediately.") + self.assertContains( + response, + "In round Round 1, your results as well as public ranking will be visible immediately.", + ) response = self._set_results_dates(url, self.visibility_dates[2]) - self.assertContains(response, "In round Round 1, your results as well as public ranking will be visible immediately.") + self.assertContains( + response, + "In round Round 1, your results as well as public ranking will be visible immediately.", + ) class PublicMessageContestController(ProgrammingContestController): @@ -4041,7 +4137,6 @@ class TestSubmitMessage(TestPublicMessage): controller_name = 'oioioi.contests.tests.tests.PublicMessageContestController' - class TestContestArchived(TestCase): fixtures = [ 'test_users', diff --git a/oioioi/contests/urls.py b/oioioi/contests/urls.py index e25fa3dba..c6e298daf 100644 --- a/oioioi/contests/urls.py +++ b/oioioi/contests/urls.py @@ -221,10 +221,12 @@ def glob_namespaced_patterns(namespace): re_path( r'^api/c/(?P[a-z0-9_-]+)/round_list/$', api.GetContestRounds.as_view(), + name='api_round_list', ), re_path( r'^api/c/(?P[a-z0-9_-]+)/problem_list/$', api.GetContestProblems.as_view(), + name='api_problem_list', ), re_path( r'^api/problemset/submit/(?P[0-9a-zA-Z-_=]+)$', From 8255bbf700429d2b8e3d0457b785722691565b19 Mon Sep 17 00:00:00 2001 From: Mateusz Kolpa Date: Wed, 5 Jun 2024 18:27:23 +0200 Subject: [PATCH 23/34] Rename stuff --- oioioi/contests/api.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/oioioi/contests/api.py b/oioioi/contests/api.py index 7982cea74..cfa9e1168 100644 --- a/oioioi/contests/api.py +++ b/oioioi/contests/api.py @@ -175,7 +175,6 @@ def get(self, request, contest_id, problem_short_name): submissions = [submission_template_context(request, s) for s in last_20_submits] submissions_data = {'submissions': []} for submission_entry in submissions: - print(">>> CODE", submission_entry['submission'].comment) score = ( submission_entry['submission'].score if submission_entry['can_see_score'] @@ -230,7 +229,7 @@ def get(self, request, contest_id, submission_id): source_file = get_submission_source_file_or_error(request, int(submission_id)) raw_source, decode_error = decode_str(source_file.read()) return Response( - {"code_language": source_file.name.split('.')[-1], "code": raw_source}, + {"lang": source_file.name.split('.')[-1], "code": raw_source}, status=status.HTTP_200_OK, ) From 116d20773cd9903f9f7a844fd0edd2c8f7915569 Mon Sep 17 00:00:00 2001 From: Mateusz Kolpa Date: Wed, 12 Jun 2024 17:42:56 +0200 Subject: [PATCH 24/34] Add UnsafeApiAllowed permission class --- oioioi/contests/api.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/oioioi/contests/api.py b/oioioi/contests/api.py index cfa9e1168..2405536c2 100644 --- a/oioioi/contests/api.py +++ b/oioioi/contests/api.py @@ -19,6 +19,7 @@ get_problem_statements, visible_contests, ) +from oioioi.default_settings import MIDDLEWARE from oioioi.problems.models import Problem, ProblemInstance from oioioi.base.permissions import enforce_condition, not_anonymous @@ -47,6 +48,12 @@ def has_object_permission(self, request, view, obj): return can_enter_contest(request) +class UnsafeApiAllowed(permissions.BasePermission): + def has_object_permission(self, request, view, obj): + # TODO: Is that ok? + return 'oioioi.ipdnsauth.middleware.IpDnsAuthMiddleware' in MIDDLEWARE + + class GetContestRounds(views.APIView): permission_classes = ( IsAuthenticated, @@ -129,10 +136,7 @@ def get(self, request, contest_id): class GetUserProblemSubmissions(views.APIView): - permission_classes = ( - IsAuthenticated, - CanEnterContest, - ) + permission_classes = (IsAuthenticated, CanEnterContest, UnsafeApiAllowed) schema = AutoSchema( [ @@ -198,10 +202,7 @@ def get(self, request, contest_id, problem_short_name): class GetUserProblemSubmissionCode(views.APIView): - permission_classes = ( - IsAuthenticated, - CanEnterContest, - ) + permission_classes = (IsAuthenticated, CanEnterContest, UnsafeApiAllowed) schema = AutoSchema( [ @@ -228,6 +229,12 @@ def get(self, request, contest_id, submission_id): source_file = get_submission_source_file_or_error(request, int(submission_id)) raw_source, decode_error = decode_str(source_file.read()) + if decode_error: + return Response( + 'Error during decoding the source code.', + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + return Response( {"lang": source_file.name.split('.')[-1], "code": raw_source}, status=status.HTTP_200_OK, @@ -279,10 +286,7 @@ def get(self, request, contest_id, problem_short_name): # This is a base class for submitting a solution for contests and problemsets. # It lacks get_problem_instance, as it's specific to problem source. class SubmitSolutionView(views.APIView): - permission_classes = ( - IsAuthenticated, - CanEnterContest, - ) + permission_classes = (IsAuthenticated, CanEnterContest, UnsafeApiAllowed) parser_classes = (MultiPartParser,) From 2e9cd2b225c7547d071853955f36f8b3a9b06689 Mon Sep 17 00:00:00 2001 From: Mateusz Kolpa Date: Wed, 12 Jun 2024 17:44:44 +0200 Subject: [PATCH 25/34] Really add... --- oioioi/contests/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oioioi/contests/api.py b/oioioi/contests/api.py index 2405536c2..b6e256482 100644 --- a/oioioi/contests/api.py +++ b/oioioi/contests/api.py @@ -51,7 +51,7 @@ def has_object_permission(self, request, view, obj): class UnsafeApiAllowed(permissions.BasePermission): def has_object_permission(self, request, view, obj): # TODO: Is that ok? - return 'oioioi.ipdnsauth.middleware.IpDnsAuthMiddleware' in MIDDLEWARE + return 'oioioi.ipdnsauth.middleware.IpDnsAuthMiddleware' not in MIDDLEWARE class GetContestRounds(views.APIView): From b76176da25faa2873678418497e9c56751109e43 Mon Sep 17 00:00:00 2001 From: Mateusz Kolpa Date: Wed, 12 Jun 2024 17:55:18 +0200 Subject: [PATCH 26/34] Rename endpoint: problem_submissions -> problem_submission_list --- oioioi/contests/api.py | 4 ++-- oioioi/contests/tests/tests.py | 8 ++++++++ oioioi/contests/urls.py | 6 +++--- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/oioioi/contests/api.py b/oioioi/contests/api.py index b6e256482..3d1129a43 100644 --- a/oioioi/contests/api.py +++ b/oioioi/contests/api.py @@ -135,7 +135,7 @@ def get(self, request, contest_id): return Response(data) -class GetUserProblemSubmissions(views.APIView): +class GetUserProblemSubmissionList(views.APIView): permission_classes = (IsAuthenticated, CanEnterContest, UnsafeApiAllowed) schema = AutoSchema( @@ -216,7 +216,7 @@ class GetUserProblemSubmissionCode(views.APIView): make_path_coreapi_schema( name='submission_id', title="Submission id", - description="You can query submission ID list at problem_submissions endpoint.", + description="You can query submission ID list at problem_submission_list endpoint.", ), ] ) diff --git a/oioioi/contests/tests/tests.py b/oioioi/contests/tests/tests.py index bf44baa79..e1b1cc7de 100755 --- a/oioioi/contests/tests/tests.py +++ b/oioioi/contests/tests/tests.py @@ -3603,6 +3603,14 @@ def test_problemset_submission(self): self._assertSubmitted(response, 2) +# API endpoints to test: +# [] ProblemList +# [] ProblemSubmissionCode +# [] ProblemSubmissions +# [] RoundList +# [] ContestList + + class TestAPIContestList(TestCase): fixtures = [ 'test_users', diff --git a/oioioi/contests/urls.py b/oioioi/contests/urls.py index c6e298daf..338001889 100644 --- a/oioioi/contests/urls.py +++ b/oioioi/contests/urls.py @@ -209,9 +209,9 @@ def glob_namespaced_patterns(namespace): name='api_contest_get_problem_id', ), re_path( - r'^api/c/(?P[a-z0-9_-]+)/problem_submissions/(?P[a-z0-9_-]+)/$', - api.GetUserProblemSubmissions.as_view(), - name='api_contest_get_user_problem_submissions', + r'^api/c/(?P[a-z0-9_-]+)/problem_submission_list/(?P[a-z0-9_-]+)/$', + api.GetUserProblemSubmissionList.as_view(), + name='api_contest_get_user_problem_submission_list', ), re_path( r'^api/c/(?P[a-z0-9_-]+)/problem_submission_code/(?P[a-z0-9_-]+)/$', From e525cf0ea5cf7ebfc52ea716a93856c40847afa8 Mon Sep 17 00:00:00 2001 From: Mateusz Kolpa Date: Wed, 12 Jun 2024 17:57:33 +0200 Subject: [PATCH 27/34] Update test TODO list --- oioioi/contests/tests/tests.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/oioioi/contests/tests/tests.py b/oioioi/contests/tests/tests.py index e1b1cc7de..fd8915291 100755 --- a/oioioi/contests/tests/tests.py +++ b/oioioi/contests/tests/tests.py @@ -3606,9 +3606,9 @@ def test_problemset_submission(self): # API endpoints to test: # [] ProblemList # [] ProblemSubmissionCode -# [] ProblemSubmissions -# [] RoundList -# [] ContestList +# [] ProblemSubmissionList +# [...] RoundList +# [x] ContestList class TestAPIContestList(TestCase): From 7623080c2b5d759bbe2a940c7e1f589fdf03d2e5 Mon Sep 17 00:00:00 2001 From: Mateusz Kolpa Date: Mon, 24 Jun 2024 09:45:09 +0200 Subject: [PATCH 28/34] Write some tests --- oioioi/contests/api.py | 1 + oioioi/contests/tests/tests.py | 103 +++++++++++++++++++++++++++------ 2 files changed, 87 insertions(+), 17 deletions(-) diff --git a/oioioi/contests/api.py b/oioioi/contests/api.py index 3d1129a43..9d1c91bf5 100644 --- a/oioioi/contests/api.py +++ b/oioioi/contests/api.py @@ -159,6 +159,7 @@ class GetUserProblemSubmissionList(views.APIView): def get(self, request, contest_id, problem_short_name): contest = get_object_or_404(Contest, id=contest_id) + print(contest_id, problem_short_name) problem_instance = get_object_or_404( ProblemInstance, contest=contest, problem__short_name=problem_short_name ) diff --git a/oioioi/contests/tests/tests.py b/oioioi/contests/tests/tests.py index fd8915291..f17f259c7 100755 --- a/oioioi/contests/tests/tests.py +++ b/oioioi/contests/tests/tests.py @@ -3604,36 +3604,90 @@ def test_problemset_submission(self): # API endpoints to test: -# [] ProblemList +# [x] ProblemList # [] ProblemSubmissionCode # [] ProblemSubmissionList -# [...] RoundList +# [x] RoundList # [x] ContestList class TestAPIContestList(TestCase): fixtures = [ 'test_users', + 'test_participant', 'test_contest', - 'test_submission', - 'test_problem_instance', ] - def test_rights_anon(self): - contest_list = reverse('api_contest_list') - request_anon = self.client.get(contest_list) + def test(self): + contest_list_endpoint = reverse('api_contest_list') + request_anon = self.client.get(contest_list_endpoint) self.assertEqual(403, request_anon.status_code) - self.client.force_authenticate(user=User.objects.get(username='test_user2')) - - def test_rights_logged_in(self): - contest_list = reverse('api_contest_list') - self.client.force_authenticate(user=User.objects.get(username='test_user1')) - request_auth = self.client.get(contest_list) + self.assertTrue(self.client.login(username='test_user')) + request_auth = self.client.get(contest_list_endpoint) self.assertEqual(200, request_auth.status_code) class TestAPIRoundList(TestCase): + fixtures = [ + 'test_users', + 'test_contest', + ] + + def test(self): + contest_id = Contest.objects.get(pk='c').id + round_list_endpoint = reverse('api_round_list', args=(contest_id)) + request_anon = self.client.get(round_list_endpoint) + + self.assertEqual(401, request_anon.status_code) + self.assertTrue(self.client.login(username='test_user')) + request_auth = self.client.get(round_list_endpoint) + self.assertEqual(200, request_auth.status_code) + + json_data = request_auth.json()[0] + self.assertEqual(1, len(json_data)) + + json_data_0 = json_data[0] + self.assertEqual('Round 1', json_data_0['name']) + self.assertEqual(None, json_data_0['end_date']) + self.assertTrue(json_data_0['is_active']) + self.assertFalse(json_data_0['is_trial']) + + +class TestAPIProblemList(TestCase): + fixtures = [ + 'test_users', + 'test_contest', + 'test_full_package', + 'test_problem_instance', + ] + + def test(self): + contest_id = Contest.objects.get(pk='c').id + problem_list_endpoint = reverse('api_problem_list', args=(contest_id)) + request_anon = self.client.get(problem_list_endpoint) + + self.assertEqual(401, request_anon.status_code) + self.assertTrue(self.client.login(username='test_user')) + request_auth = self.client.get(problem_list_endpoint) + self.assertEqual(200, request_auth.status_code) + + json_data = request_auth.json() + self.assertEqual(1, len(json_data)) + + json_data_0 = json_data[0] + self.assertEqual(1, json_data_0['id']) + self.assertEqual('zad1', json_data_0['short_name']) + self.assertEqual(1, json_data_0['round']) + self.assertEqual(10, json_data_0['submissions_limit']) + self.assertEqual(1, json_data_0['round']) + self.assertEqual('Sumżyce', json_data_0['full_name']) + self.assertEqual(10, json_data_0['submissions_left']) + self.assertTrue(json_data_0['can_submit']) + self.assertEqual('.pdf', json_data_0['statement_extension']) + + +class TestAPIProblemSubmissionList(TestCase): fixtures = [ 'test_users', 'test_contest', @@ -3642,11 +3696,26 @@ class TestAPIRoundList(TestCase): 'test_submission', ] - def test_rights_anon(self): + + def test(self): contest_id = Contest.objects.get(pk='c').id - round_list = reverse('api_round_list', kwargs={'contest_id': contest_id}) - request = self.client.get(round_list) - print(request) + # problem_short_name = Submission.objects.get(pk=1) + print(contest_id, 'zad1') + submission_list_endpoint = reverse( + 'api_contest_get_user_problem_submission_list', + args=(contest_id, 'zad1') + ) + request_anon = self.client.get(submission_list_endpoint) + + self.assertEqual(401, request_anon.status_code) + self.assertTrue(self.client.login(username='test_user')) + request_auth = self.client.get(submission_list_endpoint) + self.assertEqual(200, request_auth.status_code) + + json_data = request_auth.json() + print(json_data) + json_data_0 = json_data[0] + self.assertTrue(False) class TestManyRoundsNoEnd(TestCase): From bc72119f72186c37f50f626203377219f3fd075a Mon Sep 17 00:00:00 2001 From: Mateusz Kolpa Date: Thu, 27 Jun 2024 18:59:53 +0200 Subject: [PATCH 29/34] Finish writing tests for the endpoints --- oioioi/contests/api.py | 1 - oioioi/contests/tests/tests.py | 64 +++++++++++++++++++++++++--------- oioioi/contests/urls.py | 4 +-- 3 files changed, 49 insertions(+), 20 deletions(-) diff --git a/oioioi/contests/api.py b/oioioi/contests/api.py index 9d1c91bf5..3d1129a43 100644 --- a/oioioi/contests/api.py +++ b/oioioi/contests/api.py @@ -159,7 +159,6 @@ class GetUserProblemSubmissionList(views.APIView): def get(self, request, contest_id, problem_short_name): contest = get_object_or_404(Contest, id=contest_id) - print(contest_id, problem_short_name) problem_instance = get_object_or_404( ProblemInstance, contest=contest, problem__short_name=problem_short_name ) diff --git a/oioioi/contests/tests/tests.py b/oioioi/contests/tests/tests.py index f17f259c7..71dd962e7 100755 --- a/oioioi/contests/tests/tests.py +++ b/oioioi/contests/tests/tests.py @@ -59,6 +59,7 @@ rounds_times, ) from oioioi.dashboard.contest_dashboard import unregister_contest_dashboard_view +from oioioi.default_settings import MIDDLEWARE from oioioi.filetracker.tests import TestStreamingMixin from oioioi.problems.models import ( Problem, @@ -3603,14 +3604,6 @@ def test_problemset_submission(self): self._assertSubmitted(response, 2) -# API endpoints to test: -# [x] ProblemList -# [] ProblemSubmissionCode -# [] ProblemSubmissionList -# [x] RoundList -# [x] ContestList - - class TestAPIContestList(TestCase): fixtures = [ 'test_users', @@ -3696,14 +3689,18 @@ class TestAPIProblemSubmissionList(TestCase): 'test_submission', ] - def test(self): - contest_id = Contest.objects.get(pk='c').id - # problem_short_name = Submission.objects.get(pk=1) - print(contest_id, 'zad1') + pi = ProblemInstance.objects.get(pk=1) + # It is really important, that ProblemInstance.short_name matches + # Problem.short_name, as otherwise this endpoint does not work. + # Situation, where it doesn't match is only possible in test. + pi.short_name = pi.problem.short_name + pi.save() submission_list_endpoint = reverse( - 'api_contest_get_user_problem_submission_list', - args=(contest_id, 'zad1') + 'api_user_problem_submission_list', args=( + pi.contest.id, + pi.problem.short_name + ) ) request_anon = self.client.get(submission_list_endpoint) @@ -3713,9 +3710,42 @@ def test(self): self.assertEqual(200, request_auth.status_code) json_data = request_auth.json() - print(json_data) - json_data_0 = json_data[0] - self.assertTrue(False) + self.assertFalse(json_data['is_truncated_to_20']) + self.assertEqual(len(json_data['submissions']), 1) + self.assertEqual(json_data['submissions'][0]['id'], 1) + self.assertEqual(json_data['submissions'][0]['score'], 34) + self.assertEqual(json_data['submissions'][0]['status'], 'OK') + + +class TestAPIProblemSubmissionCode(TestCase): + fixtures = [ + 'test_users', + 'test_contest', + 'test_full_package', + 'test_problem_instance', + 'test_submission', + 'test_submission_source', + ] + + def test(self): + pi = ProblemInstance.objects.get(pk=1) + # A submission of a file `submission.cpp` + submission_code_endpoint = reverse( + 'api_user_problem_submission_code', args=( + pi.contest.id, + 1 + ) + ) + request_anon = self.client.get(submission_code_endpoint) + + self.assertEqual(401, request_anon.status_code) + self.assertTrue(self.client.login(username='test_user')) + request_auth = self.client.get(submission_code_endpoint, follow=True) + self.assertEqual(200, request_auth.status_code) + + json_data = request_auth.json() + self.assertEqual(json_data['lang'], 'cpp'); + self.assertTrue('#include ' in json_data['code']); class TestManyRoundsNoEnd(TestCase): diff --git a/oioioi/contests/urls.py b/oioioi/contests/urls.py index 338001889..06f66bb79 100644 --- a/oioioi/contests/urls.py +++ b/oioioi/contests/urls.py @@ -211,12 +211,12 @@ def glob_namespaced_patterns(namespace): re_path( r'^api/c/(?P[a-z0-9_-]+)/problem_submission_list/(?P[a-z0-9_-]+)/$', api.GetUserProblemSubmissionList.as_view(), - name='api_contest_get_user_problem_submission_list', + name='api_user_problem_submission_list', ), re_path( r'^api/c/(?P[a-z0-9_-]+)/problem_submission_code/(?P[a-z0-9_-]+)/$', api.GetUserProblemSubmissionCode.as_view(), - name='api_contest_get_user_problem_submission_code', + name='api_user_problem_submission_code', ), re_path( r'^api/c/(?P[a-z0-9_-]+)/round_list/$', From 30f5c3af81ba9c1cd326c1c882f257f914055904 Mon Sep 17 00:00:00 2001 From: Mateusz Kolpa Date: Thu, 27 Jun 2024 19:05:00 +0200 Subject: [PATCH 30/34] Restore contests/tests/tests.py formatting. --- oioioi/contests/tests/tests.py | 387 ++++++++++++++++++--------------- 1 file changed, 213 insertions(+), 174 deletions(-) diff --git a/oioioi/contests/tests/tests.py b/oioioi/contests/tests/tests.py index 71dd962e7..be14f6541 100755 --- a/oioioi/contests/tests/tests.py +++ b/oioioi/contests/tests/tests.py @@ -1,6 +1,7 @@ # pylint: disable=abstract-method from __future__ import print_function +import bs4 import os import re from datetime import datetime, timedelta, timezone # pylint: disable=E0611 @@ -59,7 +60,6 @@ rounds_times, ) from oioioi.dashboard.contest_dashboard import unregister_contest_dashboard_view -from oioioi.default_settings import MIDDLEWARE from oioioi.filetracker.tests import TestStreamingMixin from oioioi.problems.models import ( Problem, @@ -76,6 +76,7 @@ from oioioi.teachers.views import ( contest_dashboard_redirect as teachers_contest_dashboard, ) +from oioioi.testspackages.models import TestsPackage from rest_framework.test import APITestCase @@ -255,6 +256,7 @@ def check_order_in_response(self, response, order, error_msg): self.assertTrue(test_first_index < test_second_index, error_msg) + # TODO: expand this TestCase @override_settings(CONTEST_MODE=ContestMode.neutral) class TestSubmissionListFilters(TestCase): @@ -317,13 +319,9 @@ def test_search(self): self.assertContains(response, '0 submissions') response = self.client.get(self.url, {'pi': 'Zadanie z tłumaczeniami'}) self.assertContains(response, '1 submission') - response = self.client.get( - self.url, {'pi': 'Zadanie z tłumaczeniami', 'q': 'Zadanie'} - ) + response = self.client.get(self.url, {'pi': 'Zadanie z tłumaczeniami', 'q': 'Zadanie'}) self.assertContains(response, '1 submission') - response = self.client.get( - self.url, {'pi': 'Zadanie z tłumaczeniami', 'q': 'Sumżyce'} - ) + response = self.client.get(self.url, {'pi': 'Zadanie z tłumaczeniami', 'q': 'Sumżyce'}) self.assertContains(response, '0 submissions') @@ -945,6 +943,42 @@ def test_mixin_past_rounds_hidden_during_prep_time(self): response = self.client.get(reverse('select_contest')) self.assertEqual(len(response.context['contests']), 1) + def test_round_dates(self): + contest = Contest.objects.get() + url = reverse('problems_list', kwargs={'contest_id': contest.id}) + with fake_time(datetime(2024, 1, 1, tzinfo=timezone.utc)): + for user in ['test_admin', 'test_contest_admin', 'test_user', 'test_observer']: + self.assertTrue(self.client.login(username=user)) + response = self.client.get(url) + self.assertContains(response, "(31 July 2011, 20:27 - )") + self.assertContains(response, "(31 July 2012, 20:27 - 21:27)") + self.assertContains(response, "(30 July 2012, 20:27 - 31 July 2012, 21:27)") + + def test_polish_round_dates(self): + self.client.cookies['lang'] = 'pl' + contest = Contest.objects.get() + url = reverse('problems_list', kwargs={'contest_id': contest.id}) + with fake_time(datetime(2024, 1, 1, tzinfo=timezone.utc)): + for user in ['test_admin', 'test_contest_admin', 'test_user', 'test_observer']: + self.assertTrue(self.client.login(username=user)) + response = self.client.get(url) + self.assertContains(response, "(31 lipca 2011, 20:27 - )") + self.assertContains(response, "(31 lipca 2012, 20:27 - 21:27)") + self.assertContains(response, "(30 lipca 2012, 20:27 - 31 lipca 2012, 21:27)") + self.client.cookies['lang'] = 'en' + + @override_settings(TIME_ZONE='Europe/Warsaw') + def test_round_dates_with_other_timezone(self): + contest = Contest.objects.get() + url = reverse('problems_list', kwargs={'contest_id': contest.id}) + with fake_time(datetime(2024, 1, 1, tzinfo=timezone.utc)): + for user in ['test_admin', 'test_contest_admin', 'test_user', 'test_observer']: + self.assertTrue(self.client.login(username=user)) + response = self.client.get(url) + self.assertContains(response, "(31 July 2011, 22:27 - )") + self.assertContains(response, "(31 July 2012, 22:27 - 23:27)") + self.assertContains(response, "(30 July 2012, 22:27 - 31 July 2012, 23:27)") + def test_rules_visibility(self): contest = Contest.objects.get() contest.controller_name = 'oioioi.oi.controllers.ProgrammingContestController' @@ -1514,9 +1548,7 @@ def get_time(hour): ContestAttachment.objects.create( contest=contest, description='round-attachment-pub-date-before', - content=ContentFile( - b'content-of-roundatt-before', name='roundatt-before.txt' - ), + content=ContentFile(b'content-of-roundatt-before', name='roundatt-before.txt'), round=round, pub_date=get_time(19), ), @@ -1527,9 +1559,7 @@ def get_time(hour): ContestAttachment.objects.create( contest=contest, description='round-attachment-pub-date-after', - content=ContentFile( - b'content-of-roundatt-after', name='roundatt-after.txt' - ), + content=ContentFile(b'content-of-roundatt-after', name='roundatt-after.txt'), round=round, pub_date=get_time(22), ), @@ -1550,17 +1580,17 @@ def check(visible, invisible): # File list response = self.client.get(list_url) self.assertEqual(response.status_code, 200) - for att, content, name in visible: + for (att, content, name) in visible: self.assertContains(response, name) self.assertContains(response, att.description) - for att, content, name in invisible: + for (att, content, name) in invisible: self.assertNotContains(response, name) self.assertNotContains(response, att.description) for f in response.context['files']: self.assertEqual(f['admin_only'], False) # Actual accessibility - for att, content, name in visible: + for (att, content, name) in visible: response = self.client.get( reverse( get_attachment_urlpattern_name(att), @@ -1568,7 +1598,7 @@ def check(visible, invisible): ) ) self.assertStreamingEqual(response, content) - for att, content, name in invisible: + for (att, content, name) in invisible: check_not_accessible( self, get_attachment_urlpattern_name(att), @@ -1579,7 +1609,7 @@ def check(visible, invisible): self.assertTrue(self.client.login(username='test_admin')) response = self.client.get(list_url) self.assertEqual(response.status_code, 200) - for att, content, name in visible + invisible: + for (att, content, name) in visible + invisible: self.assertContains(response, name) self.assertContains(response, att.description) invisible_names = set([f[2] for f in invisible]) @@ -1587,7 +1617,7 @@ def check(visible, invisible): self.assertEqual(f['admin_only'], f['name'] in invisible_names) # Actual accessibility as an admin - for att, content, name in visible + invisible: + for (att, content, name) in visible + invisible: response = self.client.get( reverse( get_attachment_urlpattern_name(att), @@ -1600,12 +1630,7 @@ def check(visible, invisible): check([ca, cb, pa, ra, rb, rc], []) # Before all dates with fake_time(get_time(18)): - check( - [ - ca, - ], - [cb, pa, ra, rb, rc], - ) + check([ca,], [cb, pa, ra, rb, rc]) # Before the round start, but after the pub_dates before it with fake_time(get_time(20)): check([ca, cb], [pa, ra, rb, rc]) @@ -1616,6 +1641,50 @@ def check(visible, invisible): with fake_time(get_time(23)): check([ca, cb, pa, ra, rb, rc], []) + def test_attachments_order(self): + contest = Contest.objects.get() + problem = Problem.objects.get() + list_url = reverse('contest_files', kwargs={'contest_id': contest.id}) + self.assertTrue(self.client.login(username='test_admin')) + + # Models have names that would make them sorted in a wrong order with old sorting. + TestsPackage.objects.create( + problem=problem, + name='A-2-test-package', + ) + TestsPackage.objects.create( + problem=problem, + name='A-1-test-package', + ) + ProblemAttachment.objects.create( + problem=problem, + description='problem-attachment', + content=ContentFile(b'content-of-pa', name='B-2-pa.txt'), + ) + ProblemAttachment.objects.create( + problem=problem, + description='problem-attachment', + content=ContentFile(b'content-of-pa', name='B-1-pa.txt'), + ) + ContestAttachment.objects.create( + contest=contest, + description='contest-attachment', + content=ContentFile(b'content-of-ca', name='C-2-ca.txt'), + ) + ContestAttachment.objects.create( + contest=contest, + description='contest-attachment', + content=ContentFile(b'content-of-ca', name='C-1-ca.txt'), + ) + + response = self.client.get(list_url) + last = 0 + for name in ['C-1-ca.txt', 'C-2-ca.txt', 'B-1-pa.txt', 'B-2-pa.txt', 'A-1-test-package', 'A-2-test-package']: + self.assertContains(response, name) + pos = response.content.find(name.encode()) + self.assertTrue(pos > last) + last = pos + class TestRoundExtension(TestCase, SubmitFileMixin): fixtures = [ @@ -1709,7 +1778,7 @@ def factory(self, user, timestamp=None): request = factory.request() request.contest = self.contest request.user = user - request.timestamp = timestamp or self.during + request.timestamp = timestamp or self.during return request def setUp(self): @@ -1759,22 +1828,15 @@ def check_perms(user, perms): self.assertEqual(f(request), f in perms) check_perms(self.superuser, all_funlist) - check_perms( - self.cowner, - [ - is_contest_owner, - is_contest_admin, - is_contest_basicadmin, - can_enter_contest, - ], - ) + check_perms(self.cowner, [ + is_contest_owner, + is_contest_admin, + is_contest_basicadmin, + can_enter_contest, + ]) check_perms( self.cadmin, - [ - is_contest_admin, - is_contest_basicadmin, - can_enter_contest, - ], + [is_contest_admin, is_contest_basicadmin, can_enter_contest,], ) check_perms(self.badmin, [is_contest_basicadmin, can_enter_contest]) check_perms(self.observer, [is_contest_observer, can_enter_contest]) @@ -1787,14 +1849,11 @@ def check_perms(user, perms): self.assertEqual(user.has_perm(p, self.contest), p in perms) check_perms(self.superuser, self.perms_list) - check_perms( - self.cowner, - [ - 'contests.contest_owner', - 'contests.contest_admin', - 'contests.contest_basicadmin', - ], - ) + check_perms(self.cowner, [ + 'contests.contest_owner', + 'contests.contest_admin', + 'contests.contest_basicadmin', + ]) check_perms( self.cadmin, ['contests.contest_admin', 'contests.contest_basicadmin'], @@ -1860,18 +1919,11 @@ def test_contestpermission_admin(self): ) # Only superusers and contest owners should see these pages for u in ( - self.observer, - self.cadmin, - self.badmin, - self.pdata, - self.user, + self.observer, self.cadmin, self.badmin, self.pdata, self.user, ): self.client.force_login(u) for url in ( - list_url, - add_url, - list_url_nocontest, - add_url_nocontest, + list_url, add_url, list_url_nocontest, add_url_nocontest, ): self.assertEqual(self.client.get(url).status_code, 403) @@ -1969,9 +2021,7 @@ def test_contestpermission_admin(self): tmp_perm.save() change_url = reverse( 'noncontest:oioioiadmin:contests_contestpermission_change', - kwargs={ - 'object_id': tmp_perm.id, - }, + kwargs={'object_id': tmp_perm.id,}, ) self.try_post_perm( change_url, @@ -1988,15 +2038,13 @@ def test_contestpermission_admin(self): 'post': 'yes', 'action': 'delete_selected', '_selected_action': ContestPermission.objects.get( - permission='contests.contest_owner' - ).id, + permission='contests.contest_owner').id, } data_admin = { 'post': 'yes', 'action': 'delete_selected', '_selected_action': ContestPermission.objects.get( - permission='contests.contest_admin' - ).id, + permission='contests.contest_admin').id, } data_different_contest = { 'post': 'yes', @@ -2025,54 +2073,42 @@ def test_contestpermission_admin(self): follow=True, ) self.assertEqual(resp.status_code, 403) - self.assertEqual( - ContestPermission.objects.filter(id=perm_different_contest.id).count(), 1 - ) + self.assertEqual(ContestPermission.objects.filter( + id=perm_different_contest.id).count(), 1) - self.assertEqual( - ContestPermission.objects.filter( - permission='contests.contest_admin' - ).count(), - 1, - ) + self.assertEqual(ContestPermission.objects.filter( + permission='contests.contest_admin').count(), 1) resp = self.client.post(list_url, data_admin) self.assertEqual(resp.status_code, 302) - self.assertEqual( - ContestPermission.objects.filter( - permission='contests.contest_admin' - ).count(), - 0, - ) + self.assertEqual(ContestPermission.objects.filter( + permission='contests.contest_admin').count(), 0) self.client.force_login(self.superuser) resp = self.client.post(list_url, data_different_contest, follow=True) self.assertEqual(resp.status_code, 200) - self.assertEqual( - ContestPermission.objects.filter(id=perm_different_contest.id).count(), 1 - ) + self.assertEqual(ContestPermission.objects.filter( + id=perm_different_contest.id).count(), 1) resp = self.client.post( list_url_another_contest, data_different_contest, ) self.assertEqual(resp.status_code, 302) - self.assertEqual( - ContestPermission.objects.filter(id=perm_different_contest.id).count(), 0 - ) + self.assertEqual(ContestPermission.objects.filter( + id=perm_different_contest.id).count(), 0) resp = self.client.post(list_url, data_owner) self.assertEqual(resp.status_code, 302) - self.assertEqual( - ContestPermission.objects.filter( - permission='contests.contest_owner' - ).count(), - 0, - ) + self.assertEqual(ContestPermission.objects.filter( + permission='contests.contest_owner').count(), 0) def test_menu(self): unregister_contest_dashboard_view(simpleui_contest_dashboard) unregister_contest_dashboard_view(teachers_contest_dashboard) - url = reverse('default_contest_view', kwargs={'contest_id': self.contest.id}) + url = reverse( + 'default_contest_view', + kwargs={'contest_id': self.contest.id} + ) self.client.force_login(self.cadmin) response = self.client.get(url, follow=True) @@ -3512,7 +3548,7 @@ def contest_submit(self, contest, pi, *args, **kwargs): 'api_contest_submit', {'contest_name': contest.id, 'problem_short_name': pi.short_name}, *args, - **kwargs, + **kwargs ) def test_simple_submission(self): @@ -3867,9 +3903,7 @@ def set_registration_availability(rvc, enabled, available_from=None, available_t rvc.save() -def check_registration( - self, expected_status_code, availability, available_from=None, available_to=None -): +def check_registration(self, expected_status_code, availability, available_from=None, available_to=None): contest = Contest.objects.get() contest.controller_name = 'oioioi.oi.controllers.OIContestController' contest.save() @@ -3885,7 +3919,10 @@ def check_registration( class TestOpenRegistration(TestCase): - fixtures = ['test_users', 'test_contest'] + fixtures = [ + 'test_users', + 'test_contest' + ] def test_open_registration(self): check_registration(self, 200, 'YES') @@ -3911,14 +3948,13 @@ def test_configured_registration_closed_after(self): available_to = now - timedelta(minutes=5) check_registration(self, 403, 'CONFIG', available_from, available_to) - class TestRulesVisibility(TestCase): fixtures = [ 'test_users', 'test_participant', 'test_contest', 'test_full_package', - 'test_three_problem_instances.json', + 'test_three_problem_instances.json' ] controller_names = [ @@ -3933,7 +3969,7 @@ class TestRulesVisibility(TestCase): 'oioioi.oi.controllers.BOIOnlineContestController', 'oioioi.pa.controllers.PAContestController', 'oioioi.pa.controllers.PAFinalsContestController', - 'oioioi.programs.controllers.ProgrammingContestController', + 'oioioi.programs.controllers.ProgrammingContestController' ] # left to fill in when added, in order of the controllers above @@ -3945,6 +3981,7 @@ class TestRulesVisibility(TestCase): "The lower the total time, the higher the rank.
" "Compilation errors and system errors are not considered as an incorrect submission.
" "The ranking is frozen 60 minutes before the end of the round.", + "The solutions are judged on real-time. " "The submission is correct if it passes all the test cases.
" "Participants are ranked by the number of solved problems. " @@ -3952,6 +3989,7 @@ class TestRulesVisibility(TestCase): "The lower the total time, the higher the rank.
" "Compilation errors and system errors are not considered as an incorrect submission.
" "The ranking is frozen 60 minutes before the end of the round.", + "The solutions are judged on real-time. " "The submission is correct if it passes all the test cases.
" "Participants are ranked by the number of solved problems. " @@ -3959,31 +3997,39 @@ class TestRulesVisibility(TestCase): "The lower the total time, the higher the rank.
" "Compilation errors and system errors are not considered as an incorrect submission.
" "The ranking is frozen 15 minutes before the end of the trial rounds and 60 minutes before the end of the normal rounds.", + "The submissions are scored from 0 to 100 points.
" "The participant can submit to finished rounds, but a multiplier is applied to the score of such submissions.", + "The solutions are judged with sio2jail. They can be scored from 0 to 100 points. " "If the submission runs for longer than half of the time limit, the points for this test are linearly decreased to 0.
" "The score for a group of test cases is the minimum score for any of the test cases.
" "The ranking is determined by the total score.
" "Until the end of the contest, participants can only see scoring of their submissions on example test cases. " "Full scoring is available after the end of the contest.", + "The solutions are judged with sio2jail. They can be scored from 0 to 100 points. " "If the submission runs for longer than half of the time limit, the points for this test are linearly decreased to 0.
" "The score for a group of test cases is the minimum score for any of the test cases.
" "The ranking is determined by the total score.
" "Until the end of the contest, participants can only see scoring of their submissions on example test cases. " "Full scoring is available after the end of the contest.", + "The solutions are judged with sio2jail. They can be scored from 0 to 100 points. " "If the submission runs for longer than half of the time limit, the points for this test are linearly decreased to 0.
" "The score for a group of test cases is the minimum score for any of the test cases
." "The ranking is determined by the total score.
" "Full scoring of the submissions can be revealed during the contest.", + '', + '', + "The submissions are judged on real-time. All problems have 10 test groups, each worth 1 point. " "If any of the tests in a group fails, the group is worth 0 points.
" "The full scoring is available after the end of the round." "The ranking is determined by the total score and number of 10-score submissions, 9-score, 8-score etc.", + "The solutions are judged on real-time. " "The submission is correct if it passes all the test cases.
" "Participants are ranked by the number of solved problems. " @@ -3991,31 +4037,38 @@ class TestRulesVisibility(TestCase): "The lower the total time, the higher the rank.
" "Compilation errors and system errors are not considered as an incorrect submission.
" "The ranking is frozen 15 minutes before the end of the trial rounds and 60 minutes before the end of the normal rounds.", + "The submissions are scored on a set of groups of test cases. Each group is worth a certain number of points.
" "The score is a sum of the scores of all groups. The ranking is determined by the total score.
" - "The full scoring is available after the results date for the round.", + "The full scoring is available after the results date for the round." ] visibility_dates = [ - [datetime(2012, 8, 15, 20, 27, 58, tzinfo=timezone.utc), None], [ datetime(2012, 8, 15, 20, 27, 58, tzinfo=timezone.utc), - datetime(2013, 4, 20, 21, 37, 13, tzinfo=timezone.utc), + None ], - [None, None], + [ + datetime(2012, 8, 15, 20, 27, 58, tzinfo=timezone.utc), + datetime(2013, 4, 20, 21, 37, 13, tzinfo=timezone.utc) + ], + [ + None, + None + ] ] def _set_problem_limits(self, url, limits_list): for i in range(len(limits_list)): - problem = ProblemInstance.objects.get(pk=i + 1) + problem = ProblemInstance.objects.get(pk=i+1) problem.submissions_limit = limits_list[i] problem.save() - + response = self.client.get(url, follow=True) self.assertEqual(response.status_code, 200) return response - + def _set_results_dates(self, url, dates): round = Round.objects.get() round.results_date = dates[0] @@ -4038,7 +4091,7 @@ def _change_controller(self, public_results=False): 'oioioi.programs.controllers.ProgrammingContestController' ) contest.save() - + def test_dashboard_view(self): for c in self.controller_names: contest = Contest.objects.get() @@ -4049,7 +4102,7 @@ def test_dashboard_view(self): response = self.client.get(url, follow=True) self.assertEqual(response.status_code, 200) self.assertContains(response, "Rules") - + def test_contest_type(self): for c, d in zip(self.controller_names, self.scoring_descriptions): contest = Contest.objects.get() @@ -4083,31 +4136,21 @@ def test_problem_limits(self): self.assertTrue(self.client.login(username='test_user')) url = reverse('contest_rules', kwargs={'contest_id': 'c'}) response = self._set_problem_limits(url, [0, 0, 0]) - self.assertContains( - response, "There is a limit of infinity submissions for each problem." - ) + self.assertContains(response, "There is a limit of infinity submissions for each problem.") response = self._set_problem_limits(url, [0, 10, 0]) - self.assertContains( - response, - "There is a limit of 10 to infinity submissions, depending on a problem.", - ) + self.assertContains(response, "There is a limit of 10 to infinity submissions, depending on a problem.") response = self._set_problem_limits(url, [20, 10, 0]) - self.assertContains( - response, - "There is a limit of 10 to infinity submissions, depending on a problem.", - ) + self.assertContains(response, "There is a limit of 10 to infinity submissions, depending on a problem.") response = self._set_problem_limits(url, [10, 10, 10]) - self.assertContains( - response, "There is a limit of 10 submissions for each problem." - ) + self.assertContains(response, "There is a limit of 10 submissions for each problem.") def test_contest_dates(self): times = [ fake_time(datetime(2012, 8, 5, 12, 37, 45, tzinfo=timezone.utc)), - fake_time(datetime(2012, 8, 15, 15, 16, 18, tzinfo=timezone.utc)), + fake_time(datetime(2012, 8, 15, 15, 16, 18, tzinfo=timezone.utc)) ] for t in times: @@ -4116,7 +4159,7 @@ def test_contest_dates(self): contest = Contest.objects.get() contest.controller_name = c contest.save() - + round = Round.objects.get() round.end_date = None round.save() @@ -4125,18 +4168,13 @@ def test_contest_dates(self): url = reverse('contest_rules', kwargs={'contest_id': 'c'}) response = self.client.get(url, follow=True) self.assertEqual(response.status_code, 200) - self.assertContains( - response, "The contest starts on 2011-07-31 20:27:58." - ) + self.assertContains(response, "The contest starts on 2011-07-31 20:27:58.") round.end_date = datetime(2012, 8, 10, 0, 0, tzinfo=timezone.utc) round.save() response = self.client.get(url, follow=True) self.assertEqual(response.status_code, 200) - self.assertContains( - response, - "The contest starts on 2011-07-31 20:27:58 and ends on 2012-08-10 00:00:00.", - ) + self.assertContains(response, "The contest starts on 2011-07-31 20:27:58 and ends on 2012-08-10 00:00:00.") def test_ranking_visibility(self): # here we don't check for individual contests, as it would be hard to get the separate_public_results() @@ -4146,66 +4184,39 @@ def test_ranking_visibility(self): with fake_time(datetime(2012, 8, 4, 13, 46, 37, tzinfo=timezone.utc)): response = self._set_results_dates(url, self.visibility_dates[0]) - self.assertContains( - response, - "In round Round 1, your results as well as " - "public ranking will be visible after 2012-08-15 20:27:58.", - ) - + self.assertContains(response, "In round Round 1, your results as well as " \ + "public ranking will be visible after 2012-08-15 20:27:58.") + self._change_controller(public_results=True) response = self._set_results_dates(url, self.visibility_dates[1]) - self.assertContains( - response, - "In round Round 1, your results will be visible after 2012-08-15 20:27:58" - " and the public ranking will be visible after 2013-04-20 21:37:13.", - ) + self.assertContains(response, "In round Round 1, your results will be visible after 2012-08-15 20:27:58" \ + " and the public ranking will be visible after 2013-04-20 21:37:13.") response = self._set_results_dates(url, self.visibility_dates[2]) - self.assertContains( - response, - "In round Round 1, your results as well as public ranking will be visible immediately.", - ) + self.assertContains(response, "In round Round 1, your results as well as public ranking will be visible immediately.") with fake_time(datetime(2012, 12, 24, 11, 23, 56, tzinfo=timezone.utc)): response = self._set_results_dates(url, self.visibility_dates[0]) - self.assertContains( - response, - "In round Round 1, your results as well as public ranking will be visible immediately.", - ) - + self.assertContains(response, "In round Round 1, your results as well as public ranking will be visible immediately.") + self._change_controller(public_results=True) response = self._set_results_dates(url, self.visibility_dates[1]) - self.assertContains( - response, - "In round Round 1, your results will be visible immediately" - " and the public ranking will be visible after 2013-04-20 21:37:13.", - ) + self.assertContains(response, "In round Round 1, your results will be visible immediately" \ + " and the public ranking will be visible after 2013-04-20 21:37:13.") response = self._set_results_dates(url, self.visibility_dates[2]) - self.assertContains( - response, - "In round Round 1, your results as well as public ranking will be visible immediately.", - ) + self.assertContains(response, "In round Round 1, your results as well as public ranking will be visible immediately.") with fake_time(datetime(2014, 8, 26, 11, 23, 56, tzinfo=timezone.utc)): response = self._set_results_dates(url, self.visibility_dates[0]) - self.assertContains( - response, - "In round Round 1, your results as well as public ranking will be visible immediately.", - ) - + self.assertContains(response, "In round Round 1, your results as well as public ranking will be visible immediately.") + self._change_controller(public_results=True) response = self._set_results_dates(url, self.visibility_dates[1]) - self.assertContains( - response, - "In round Round 1, your results as well as public ranking will be visible immediately.", - ) + self.assertContains(response, "In round Round 1, your results as well as public ranking will be visible immediately.") response = self._set_results_dates(url, self.visibility_dates[2]) - self.assertContains( - response, - "In round Round 1, your results as well as public ranking will be visible immediately.", - ) + self.assertContains(response, "In round Round 1, your results as well as public ranking will be visible immediately.") class PublicMessageContestController(ProgrammingContestController): @@ -4244,6 +4255,7 @@ class TestSubmitMessage(TestPublicMessage): controller_name = 'oioioi.contests.tests.tests.PublicMessageContestController' + class TestContestArchived(TestCase): fixtures = [ 'test_users', @@ -4305,3 +4317,30 @@ def test_registration(self): response = self.client.get(url) self.assertEqual(403, response.status_code) + +class TestScoreBadges(TestCase): + fixtures = [ + 'test_users', + 'test_contest', + 'test_three_problem_instances', + 'test_full_package', + 'test_three_submissions', + ] + + def _get_badge_for_problem(self, content, problem): + soup = bs4.BeautifulSoup(content, 'html.parser') + problem_row = soup.find('td', string=problem).parent + return problem_row.find_all('td')[2].a.div.attrs['class'] + + def test_score_badge(self): + contest = Contest.objects.get() + url = reverse('problems_list', kwargs={'contest_id': contest.id}) + + self.assertTrue(self.client.login(username='test_user')) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + self.assertIn('badge-success', self._get_badge_for_problem(response.content, 'zad1')) + self.assertIn('badge-warning', self._get_badge_for_problem(response.content, 'zad2')) + self.assertIn('badge-danger', self._get_badge_for_problem(response.content, 'zad3')) + From 65ea3feb6167b737394a5b800205d88c22e0beda Mon Sep 17 00:00:00 2001 From: Mateusz Kolpa Date: Thu, 27 Jun 2024 19:10:11 +0200 Subject: [PATCH 31/34] Fix APIRoundList test --- oioioi/contests/tests/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oioioi/contests/tests/tests.py b/oioioi/contests/tests/tests.py index be14f6541..2c3ad2c73 100755 --- a/oioioi/contests/tests/tests.py +++ b/oioioi/contests/tests/tests.py @@ -3674,7 +3674,7 @@ def test(self): self.assertEqual(200, request_auth.status_code) json_data = request_auth.json()[0] - self.assertEqual(1, len(json_data)) + self.assertEqual(7, len(json_data)) json_data_0 = json_data[0] self.assertEqual('Round 1', json_data_0['name']) From 12a40294b5966d3eb19c720b2f8237e22160c4d5 Mon Sep 17 00:00:00 2001 From: Mateusz Kolpa Date: Thu, 27 Jun 2024 19:33:19 +0200 Subject: [PATCH 32/34] Fix APIRoundList test better --- oioioi/contests/tests/tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oioioi/contests/tests/tests.py b/oioioi/contests/tests/tests.py index 2c3ad2c73..3f2ab6482 100755 --- a/oioioi/contests/tests/tests.py +++ b/oioioi/contests/tests/tests.py @@ -3673,8 +3673,8 @@ def test(self): request_auth = self.client.get(round_list_endpoint) self.assertEqual(200, request_auth.status_code) - json_data = request_auth.json()[0] - self.assertEqual(7, len(json_data)) + json_data = request_auth.json() + self.assertEqual(1, len(json_data)) json_data_0 = json_data[0] self.assertEqual('Round 1', json_data_0['name']) From 66af42f52905640c4ffd3deb47511b86d5203590 Mon Sep 17 00:00:00 2001 From: Mateusz Kolpa Date: Wed, 11 Dec 2024 22:10:38 +0100 Subject: [PATCH 33/34] Update api.py --- oioioi/contests/api.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/oioioi/contests/api.py b/oioioi/contests/api.py index 3d1129a43..9be47233a 100644 --- a/oioioi/contests/api.py +++ b/oioioi/contests/api.py @@ -50,8 +50,7 @@ def has_object_permission(self, request, view, obj): class UnsafeApiAllowed(permissions.BasePermission): def has_object_permission(self, request, view, obj): - # TODO: Is that ok? - return 'oioioi.ipdnsauth.middleware.IpDnsAuthMiddleware' not in MIDDLEWARE + return not any('IpDnsAuthMiddleware' in x for x in MIDDLEWARE) class GetContestRounds(views.APIView): From 3d9b4d6264dd18bc92711d8d9cf6cb8e4d9bae1d Mon Sep 17 00:00:00 2001 From: Mateusz Kolpa Date: Wed, 11 Dec 2024 22:12:36 +0100 Subject: [PATCH 34/34] Update urls.py --- oioioi/contests/urls.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/oioioi/contests/urls.py b/oioioi/contests/urls.py index 06f66bb79..59018c76e 100644 --- a/oioioi/contests/urls.py +++ b/oioioi/contests/urls.py @@ -150,11 +150,7 @@ def glob_namespaced_patterns(namespace): name='user_info_redirect', ), re_path(r'^admin/', admin.contest_site.urls), - re_path( - r'^archive/confirm$', - views.confirm_archive_contest, - name='confirm_archive_contest', - ), + re_path(r'^archive/confirm$', views.confirm_archive_contest, name='confirm_archive_contest'), re_path(r'^unarchive/$', views.unarchive_contest, name='unarchive_contest'), ]