diff --git a/oioioi/base/templatetags/simple_filters.py b/oioioi/base/templatetags/simple_filters.py index 0bcf5109d..def85645e 100644 --- a/oioioi/base/templatetags/simple_filters.py +++ b/oioioi/base/templatetags/simple_filters.py @@ -8,6 +8,7 @@ from django.utils.safestring import mark_safe from oioioi.contests.scores import IntegerScore +from oioioi.mp.score import FloatScore from oioioi.pa.score import PAScore register = template.Library() @@ -274,6 +275,8 @@ def result_color_class(raw_score): score_max_value = 100 elif isinstance(raw_score, PAScore): score_max_value = 10 + elif isinstance(raw_score, FloatScore): + score_max_value = 100 else: # There should be a method to get maximum points for # contest, for now, support just above cases. diff --git a/oioioi/mp/admin.py b/oioioi/mp/admin.py index 06c596372..aa113754a 100644 --- a/oioioi/mp/admin.py +++ b/oioioi/mp/admin.py @@ -52,7 +52,7 @@ def get_inlines(self, request, obj): if hasattr(obj, 'controller') and isinstance( obj.controller, MPContestController ): - return inlines + [SubmissionScoreMultiplierInline] + return inlines + (SubmissionScoreMultiplierInline,) return inlines diff --git a/oioioi/mp/controllers.py b/oioioi/mp/controllers.py index e5111f9a3..b6be9a21c 100644 --- a/oioioi/mp/controllers.py +++ b/oioioi/mp/controllers.py @@ -5,7 +5,7 @@ from oioioi.base.utils.query_helpers import Q_always_true from oioioi.base.utils.redirect import safe_redirect -from oioioi.contests.models import Submission +from oioioi.contests.models import Submission, SubmissionReport from oioioi.mp.models import MPRegistration, SubmissionScoreMultiplier from oioioi.mp.score import FloatScore from oioioi.participants.controllers import ParticipantsController @@ -107,6 +107,17 @@ def registration_controller(self): def ranking_controller(self): return MPRankingController(self.contest) + def _get_score_for_submission(self, submission, ssm): + score = FloatScore(submission.score.value) + rtimes = self.get_round_times(None, submission.problem_instance.round) + # Round was active when the submission was sent + if rtimes.is_active(submission.date): + return score + # Round was over when the submission was sent but multiplier was ahead + if ssm and ssm.end_date >= submission.date: + return score * ssm.multiplier + return None + def update_user_result_for_problem(self, result): """Submissions sent during the round are scored as normal. Submissions sent while the round was over but SubmissionScoreMultiplier was active @@ -119,28 +130,31 @@ def update_user_result_for_problem(self, result): score__isnull=False, ) - if submissions: - best_submission = None - for submission in submissions: - ssm = SubmissionScoreMultiplier.objects.filter( - contest=submission.problem_instance.contest, - ) - - score = FloatScore(submission.score.value) - rtimes = self.get_round_times(None, submission.problem_instance.round) - if rtimes.is_active(submission.date): - pass - elif ssm.exists() and ssm[0].end_date >= submission.date: - score = score * ssm[0].multiplier - else: - score = None - if not best_submission or ( - score is not None and best_submission[1] < score - ): - best_submission = [submission, score] + best_submission = None + best_submission_score = None + try: + ssm = SubmissionScoreMultiplier.objects.get( + contest=result.problem_instance.contest + ) + except SubmissionScoreMultiplier.DoesNotExist: + ssm = None + + for submission in submissions: + score = self._get_score_for_submission(submission, ssm) + if not best_submission or (score and best_submission_score < score): + best_submission = submission + best_submission_score = score + + try: + report = SubmissionReport.objects.get( + submission=best_submission, status='ACTIVE', kind='NORMAL' + ) + except SubmissionReport.DoesNotExist: + report = None - result.score = best_submission[1] - result.status = best_submission[0].status + result.score = best_submission_score + result.status = best_submission.status if best_submission else None + result.submission_report = report def can_submit(self, request, problem_instance, check_round_times=True): """Contest admin can always submit. @@ -155,6 +169,8 @@ def can_submit(self, request, problem_instance, check_round_times=True): return True if not is_participant(request): return False + if problem_instance.round is None: + return False rtimes = self.get_round_times(None, problem_instance.round) round_over_contest_running = rtimes.is_past( diff --git a/oioioi/mp/fixtures/test_mp_contest.json b/oioioi/mp/fixtures/test_mp_contest.json index 9434c0a92..d183aa64e 100644 --- a/oioioi/mp/fixtures/test_mp_contest.json +++ b/oioioi/mp/fixtures/test_mp_contest.json @@ -13,6 +13,16 @@ "enable_editor": false } }, + { + "model": "participants.participant", + "pk": 1, + "fields": { + "contest": "contest1", + "user": 2, + "status": "ACTIVE", + "anonymous": false + } + }, { "model": "problems.problem", "pk": 1, @@ -105,6 +115,18 @@ "needs_rejudge": false } }, + { + "model": "contests.probleminstance", + "pk": 5, + "fields": { + "contest": "contest1", + "round": null, + "problem": 1, + "short_name": "squ2", + "submissions_limit": 10, + "needs_rejudge": false + } + }, { "model": "contests.submission", "pk": 1, diff --git a/oioioi/mp/models.py b/oioioi/mp/models.py index 3e14c89d4..1bbc29a09 100644 --- a/oioioi/mp/models.py +++ b/oioioi/mp/models.py @@ -7,7 +7,6 @@ from oioioi.participants.models import RegistrationModel from oioioi.contests.models import Contest -check_django_app_dependencies(__name__, ['oioioi.participants']) check_django_app_dependencies(__name__, ['oioioi.contests']) @@ -20,12 +19,13 @@ def erase_data(self): class SubmissionScoreMultiplier(models.Model): - """ If SubmissionScoreMultiplier exists, users can submit problems + """If SubmissionScoreMultiplier exists, users can submit problems even after round ends, until end_date - + Result score for submission after round's end is multiplied by given multiplier value """ + contest = models.OneToOneField( Contest, verbose_name=_("contest"), on_delete=models.CASCADE ) diff --git a/oioioi/mp/tests.py b/oioioi/mp/tests.py index d3f2cc0d9..0ac36489c 100644 --- a/oioioi/mp/tests.py +++ b/oioioi/mp/tests.py @@ -1,5 +1,5 @@ -from datetime import datetime, timezone import re +from datetime import datetime, timezone from django.urls import reverse @@ -23,7 +23,7 @@ class TestMPRanking(TestCase): def _ranking_url(self, key='c'): contest = Contest.objects.get(name='contest1') return reverse('ranking', kwargs={'contest_id': contest.id, 'key': key}) - + def _check_order(self, response, expected): prev_pos = 0 for round_name in expected: @@ -48,12 +48,15 @@ def test_columns_order(self): self.assertTrue(self.client.login(username='test_user1')) with fake_time(datetime(2023, 1, 6, 0, 0, tzinfo=timezone.utc)): response = self.client.get(self._ranking_url()) - self._check_order(response, [ - b'User', - b']*>Sum', - b']*>\s*(]*>)*\s*squ1\s*()*\s*', - b']*>\s*(]*>)*\s*squ\s*()*\s*' - ]) + self._check_order( + response, + [ + b'User', + b']*>Sum', + b']*>\s*(]*>)*\s*squ1\s*()*\s*', + b']*>\s*(]*>)*\s*squ\s*()*\s*', + ], + ) def test_no_zero_scores_in_ranking(self): self.assertTrue(self.client.login(username='test_user1')) @@ -65,6 +68,23 @@ def test_no_zero_scores_in_ranking(self): self.assertFalse(re.search(b']*>Test User4', response.content)) +class TestNoRoundProblem(TestCase): + fixtures = ['test_mp_users', 'test_mp_contest'] + + def test_no_round_problem(self): + self.assertTrue(self.client.login(username='test_user1')) + contest = Contest.objects.get() + url = reverse('submit', kwargs={'contest_id': contest.id}) + with fake_time(datetime(2023, 1, 5, 12, 10, tzinfo=timezone.utc)): + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertIn('form', response.context) + form = response.context['form'] + # there are 3 problems, one of them doesn't have round + # +1 because of blank field + self.assertEqual(len(form.fields['problem_instance_id'].choices), 3) + + class TestSubmissionScoreMultiplier(TestCase): def _create_result(user, pi): res = UserResultForProblem()