Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Api improvements #373

Open
wants to merge 36 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
4c56b19
Add api_issue_md
matihope Mar 20, 2024
1f21272
Add contest_list
matihope Mar 20, 2024
1c63db0
Merge branch 'sio2project:master' into api_improvements
matihope Mar 20, 2024
d6e0b02
Round listing might work, not tested yet, cuz gotta go hit the gym
matihope Mar 20, 2024
87583eb
Add round_list, problem_list
matihope Apr 3, 2024
42929bb
Improve problem_list
matihope Apr 17, 2024
a852fa9
Add problem statement extension to api response
matihope Apr 17, 2024
28c1c3e
Do not show invisible problems
matihope Apr 17, 2024
7699811
Fix None extension in GetContestProblems
fmkra Apr 17, 2024
cf35bc7
Rename statement_ext to statement_extension
matihope Apr 24, 2024
b20d4d2
Merge with remote
matihope Apr 24, 2024
4a84e6e
Prepare for initial PR without tests
matihope Apr 24, 2024
3069447
Rm a redundant file
matihope Apr 24, 2024
cb08e99
Add rate limiting
matihope May 8, 2024
d18af57
Start writing tests
matihope May 8, 2024
374ed9c
Merge branch 'master' of github.com:matihope/oioioi
matihope May 8, 2024
48cfb4f
Merge branch 'master' into api_improvements
matihope May 8, 2024
6db7e0d
Add submission list
matihope May 15, 2024
e7d8cec
Add stuff
matihope May 22, 2024
487baf6
Fix stuff
matihope May 22, 2024
2833c98
Better fix stuff
matihope May 22, 2024
237a94c
Even better fix stuff
matihope May 22, 2024
1c35b78
Fix getProblemSubmission
matihope May 22, 2024
20e26f8
Add stuff (like code)
matihope May 22, 2024
736f2b7
Update stuff
matihope Jun 5, 2024
cc2b53d
Fix stuff
matihope Jun 5, 2024
8255bbf
Rename stuff
matihope Jun 5, 2024
116d207
Add UnsafeApiAllowed permission class
matihope Jun 12, 2024
2e9cd2b
Really add...
matihope Jun 12, 2024
b76176d
Rename endpoint: problem_submissions -> problem_submission_list
matihope Jun 12, 2024
e525cf0
Update test TODO list
matihope Jun 12, 2024
7623080
Write some tests
matihope Jun 24, 2024
bc72119
Finish writing tests for the endpoints
matihope Jun 27, 2024
30f5c3a
Restore contests/tests/tests.py formatting.
matihope Jun 27, 2024
65ea3fe
Fix APIRoundList test
matihope Jun 27, 2024
12a4029
Fix APIRoundList test better
matihope Jun 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
240 changes: 231 additions & 9 deletions oioioi/contests/api.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,246 @@
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.controllers import submission_template_context
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.models import Contest, ProblemInstance, Submission
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.default_settings import MIDDLEWARE
from oioioi.problems.models import Problem, ProblemInstance
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
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


@api_view(['GET'])
@enforce_condition(not_anonymous, login_redirect=False)
def contest_list(request):
contests = visible_contests(request)
serializer = ContestSerializer(contests, many=True)
return Response(serializer.data)


class CanEnterContest(permissions.BasePermission):
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?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove comment + add

return not any('IpDnsAuthMiddleware' in x for x in MIDDLEWARE)

return 'oioioi.ipdnsauth.middleware.IpDnsAuthMiddleware' not in MIDDLEWARE


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",
),
]
)

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)


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
problem_instances = (
ProblemInstance.objects.filter(contest=request.contest)
.select_related('problem')
.prefetch_related('round')
)

# 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:
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_extension"] = (
st.extension
if (st := query_statement(problem_stmt[0].problem))
else None
)
data.append(serialized)

return Response(data)


class GetUserProblemSubmissionList(views.APIView):
permission_classes = (IsAuthenticated, CanEnterContest, UnsafeApiAllowed)

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
)

user_problem_submits = (
Submission.objects.filter(
user=request.user, problem_instance=problem_instance
)
.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
if submission_entry['can_see_score']
else None
)
submission_status = (
submission_entry['submission'].status
if submission_entry['can_see_status']
else None
)
submissions_data['submissions'].append(
{
'id': submission_entry['submission'].id,
'date': submission_entry['submission'].date,
'score': score.to_int() if score else None,
'status': submission_status,
}
)
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, UnsafeApiAllowed)

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_submission_list endpoint.",
),
]
)

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())
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,
)


class GetProblemIdView(views.APIView):
permission_classes = (
IsAuthenticated,
Expand Down Expand Up @@ -60,8 +283,11 @@ 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, UnsafeApiAllowed)

parser_classes = (MultiPartParser,)

def get_problem_instance(self, **kwargs):
Expand Down Expand Up @@ -90,10 +316,6 @@ def post(self, request, **kwargs):


class SubmitContestSolutionView(SubmitSolutionView):
permission_classes = (
IsAuthenticated,
CanEnterContest,
)
schema = AutoSchema(
[
make_path_coreapi_schema(
Expand Down
44 changes: 44 additions & 0 deletions oioioi/contests/serializers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from oioioi.contests.models import Contest, ProblemInstance, Round, UserResultForProblem
from rest_framework import serializers
from django.utils.timezone import now


class SubmissionSerializer(serializers.Serializer):
Expand Down Expand Up @@ -30,3 +32,45 @@ 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


# This is a partial serializer and it serves as a base for the API response.
class ProblemSerializer(serializers.ModelSerializer):

class Meta:
model = ProblemInstance
exclude = ['needs_rejudge', 'problem', 'contest']


class UserResultForProblemSerializer(serializers.ModelSerializer):

class Meta:
model = UserResultForProblem
fields = ['score', 'status']
Loading
Loading