From 81fd2ab398d85238e5401462001c3b8088c24800 Mon Sep 17 00:00:00 2001 From: Tomasz Date: Mon, 2 Jan 2023 19:18:45 +0100 Subject: [PATCH 01/14] added mp module --- oioioi/mp/README.rst | 2 + oioioi/mp/__init__.py | 0 oioioi/mp/admin.py | 35 ++ oioioi/mp/apps.py | 6 + oioioi/mp/controllers.py | 177 +++++++ oioioi/mp/forms.py | 27 + oioioi/mp/migrations/0001_initial.py | 28 ++ oioioi/mp/migrations/__init__.py | 0 oioioi/mp/models.py | 17 + oioioi/mp/score.py | 69 +++ oioioi/mp/templates/mp/default_ranking.html | 53 ++ .../mp/templates/mp/registration-notice.html | 8 + oioioi/mp/tests.py | 476 ++++++++++++++++++ oioioi/mp/urls.py | 9 + oioioi/mp/views.py | 23 + 15 files changed, 930 insertions(+) create mode 100644 oioioi/mp/README.rst create mode 100644 oioioi/mp/__init__.py create mode 100644 oioioi/mp/admin.py create mode 100644 oioioi/mp/apps.py create mode 100644 oioioi/mp/controllers.py create mode 100644 oioioi/mp/forms.py create mode 100644 oioioi/mp/migrations/0001_initial.py create mode 100644 oioioi/mp/migrations/__init__.py create mode 100644 oioioi/mp/models.py create mode 100644 oioioi/mp/score.py create mode 100644 oioioi/mp/templates/mp/default_ranking.html create mode 100644 oioioi/mp/templates/mp/registration-notice.html create mode 100644 oioioi/mp/tests.py create mode 100644 oioioi/mp/urls.py create mode 100644 oioioi/mp/views.py diff --git a/oioioi/mp/README.rst b/oioioi/mp/README.rst new file mode 100644 index 000000000..bc90ded84 --- /dev/null +++ b/oioioi/mp/README.rst @@ -0,0 +1,2 @@ +This module is responsible for handling Mistrz Programowania programming contests +(see 2022 contest website: https://oki.org.pl/mistrz-programowania-2022/). diff --git a/oioioi/mp/__init__.py b/oioioi/mp/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/oioioi/mp/admin.py b/oioioi/mp/admin.py new file mode 100644 index 000000000..bf3cc35c2 --- /dev/null +++ b/oioioi/mp/admin.py @@ -0,0 +1,35 @@ +from django.utils.translation import gettext_lazy as _ + +from oioioi.base import admin +from oioioi.contests.utils import is_contest_admin +from oioioi.mp.forms import MPRegistrationForm +from oioioi.mp.models import MPRegistration +from oioioi.participants.admin import ParticipantAdmin + + +class MPRegistrationInline(admin.StackedInline): + model = MPRegistration + fk_name = 'participant' + form = MPRegistrationForm + can_delete = False + inline_classes = ('collapse open',) + # We don't allow admins to change users' acceptance of contest's terms. + exclude = ('terms_accepted',) + + +class MPRegistrationParticipantAdmin(ParticipantAdmin): + list_display = ParticipantAdmin.list_display + inlines = ParticipantAdmin.inlines + [MPRegistrationInline] + readonly_fields = ['user'] + + def has_add_permission(self, request): + return request.user.is_superuser + + def has_delete_permission(self, request, obj=None): + return request.user.is_superuser + + def get_actions(self, request): + actions = super(MPRegistrationParticipantAdmin, self).get_actions(request) + if 'delete_selected' in actions: + del actions['delete_selected'] + return actions diff --git a/oioioi/mp/apps.py b/oioioi/mp/apps.py new file mode 100644 index 000000000..e084d3146 --- /dev/null +++ b/oioioi/mp/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class MPAppConfig(AppConfig): + default_auto_field = 'django.db.models.AutoField' + name = "oioioi.mp" diff --git a/oioioi/mp/controllers.py b/oioioi/mp/controllers.py new file mode 100644 index 000000000..d4d28ce06 --- /dev/null +++ b/oioioi/mp/controllers.py @@ -0,0 +1,177 @@ +import datetime +import logging +import unicodecsv + +from django import forms +from django.db.models import Q +from django.http import HttpResponse +from django.shortcuts import redirect +from django.template.response import TemplateResponse +from django.utils.translation import gettext_lazy as _ +from django.utils.encoding import force_str +from django.utils.translation import gettext_lazy as _ + +from oioioi.base.utils.query_helpers import Q_always_true +from oioioi.base.utils.redirect import safe_redirect +from oioioi.contests.utils import ( + all_non_trial_public_results_visible, + is_contest_admin, + is_contest_observer, +) +from oioioi.filetracker.utils import make_content_disposition_header +from oioioi.mp.models import MPRegistration +# from oioioi.mp.score import PAScore +from oioioi.participants.controllers import ParticipantsController +from oioioi.participants.models import Participant +from oioioi.participants.utils import is_participant +from oioioi.programs.controllers import ProgrammingContestController +from oioioi.rankings.controllers import DefaultRankingController + + +class MPRegistrationController(ParticipantsController): + @property + def form_class(self): + from oioioi.mp.forms import MPRegistrationForm + + return MPRegistrationForm + + @property + def participant_admin(self): + from oioioi.mp.admin import MPRegistrationParticipantAdmin + + return MPRegistrationParticipantAdmin + + @classmethod + def anonymous_can_enter_contest(self): + return True + + def allow_login_as_public_name(self): + return True + + # Redundant because of filter_visible_contests, but saves a db query + def can_enter_contest(self, request): + return True + + def visible_contests_query(self, request): + return Q_always_true() + + def can_register(self, request): + return True + + def can_unregister(self, request, participant): + return False + + def registration_view(self, request): + participant = self._get_participant_for_form(request) + + if 'mp_paregistrationformdata' in request.session: + # pylint: disable=not-callable + form = self.form_class(request.session['mp_paregistrationformdata']) + del request.session['mp_paregistrationformdata'] + else: + form = self.get_form(request, participant) + form.set_terms_accepted_text(self.get_terms_accepted_phrase()) + + if request.method == 'POST': + # pylint: disable=maybe-no-member + if form.is_valid(): + participant, created = Participant.objects.get_or_create( + contest=self.contest, user=request.user + ) + self.handle_validated_form(request, form, participant) + if 'next' in request.GET: + return safe_redirect(request, request.GET['next']) + else: + return redirect('default_contest_view', contest_id=self.contest.id) + + context = {'form': form, 'participant': participant} + return TemplateResponse(request, self.registration_template, context) + + def mixins_for_admin(self): + from oioioi.participants.admin import TermsAcceptedPhraseAdminMixin + + return super(MPRegistrationController, self).mixins_for_admin() + ( + TermsAcceptedPhraseAdminMixin, + ) + + def can_change_terms_accepted_phrase(self, request): + return not MPRegistration.objects.filter( + participant__contest=request.contest + ).exists() + + +class MPContestController(ProgrammingContestController): + description = _("Mistrz Programowania") + create_forum = False + + # def update_user_result_for_problem(self, result): + # super(MPContestController, self).update_user_result_for_problem(result) + # if result.score is not None: + # result.score = MPScore(result.score) + + def registration_controller(self): + return MPRegistrationController(self.contest) + + def ranking_controller(self): + return MPRankingController(self.contest) + + def separate_public_results(self): + return True + + def can_submit(self, request, problem_instance, check_round_times=True): + if request.user.is_anonymous: + return False + if request.user.has_perm('contests.contest_admin', self.contest): + return True + if not is_participant(request): + return False + return super(MPContestController, self).can_submit( + request, problem_instance, check_round_times + ) + + +class MPRankingController(DefaultRankingController): + """ + """ + + description = _("MP style ranking") + + def _render_ranking_page(self, key, data, page): + request = self._fake_request(page) + data['is_admin'] = self.is_admin_key(key) + return render_to_string( + 'mp/defaut_ranking.html', context=data, request=request + ) + + def _get_csv_header(self, key, data): + header = [_("No."), _("Login"), _("First name"), _("Last name"), _("Sum")] + for pi, _statement_visible in data['problem_instances']: + header.append(pi.get_short_name_display()) + return header + + def _get_csv_row(self, key, row): + line = [ + row['place'], + row['user'].username, + row['user'].first_name, + row['user'].last_name, + row['sum'], + ] + line += [r.score if r and r.score is not None else '' for r in row['results']] + return line + + def render_ranking_to_csv(self, request, partial_key): + key = self.get_full_key(request, partial_key) + data = self.serialize_ranking(key) + + response = HttpResponse(content_type='text/csv') + response['Content-Disposition'] = make_content_disposition_header( + 'attachment', u'%s-%s-%s.csv' % (_("ranking"), self.contest.id, key) + ) + writer = unicodecsv.writer(response) + + writer.writerow(list(map(force_str, self._get_csv_header(key, data)))) + for row in data['rows']: + writer.writerow(list(map(force_str, self._get_csv_row(key, row)))) + + return response diff --git a/oioioi/mp/forms.py b/oioioi/mp/forms.py new file mode 100644 index 000000000..fe6983427 --- /dev/null +++ b/oioioi/mp/forms.py @@ -0,0 +1,27 @@ +from django import forms +from django.forms import ValidationError +from django.utils.safestring import mark_safe +from django.utils.translation import gettext_lazy as _ + +from oioioi.mp.models import MPRegistration + + +class MPRegistrationForm(forms.ModelForm): + class Meta(object): + model = MPRegistration + exclude = ['participant'] + + def set_terms_accepted_text(self, terms_accepted_phrase): + if terms_accepted_phrase is None: + self.fields['terms_accepted'].label = _( + "I declare that I have read the contest rules and " + "the technical arrangements. I fully understand them and " + "accept them unconditionally." + ) + else: + self.fields['terms_accepted'].label = mark_safe(terms_accepted_phrase.text) + + def clean_terms_accepted(self): + if not self.cleaned_data['terms_accepted']: + raise ValidationError(_("Terms not accepted")) + return True diff --git a/oioioi/mp/migrations/0001_initial.py b/oioioi/mp/migrations/0001_initial.py new file mode 100644 index 000000000..6f9fa8538 --- /dev/null +++ b/oioioi/mp/migrations/0001_initial.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.16 on 2023-01-01 22:16 + +from django.db import migrations, models +import django.db.models.deletion +import oioioi.participants.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('participants', '0010_alter_termsacceptedphrase_options'), + ] + + operations = [ + migrations.CreateModel( + name='MPRegistration', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('terms_accepted', models.BooleanField(default=False, verbose_name='terms accepted')), + ('participant', oioioi.participants.fields.OneToOneBothHandsCascadingParticipantField(on_delete=django.db.models.deletion.CASCADE, related_name='mp_mpregistration', to='participants.participant')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/oioioi/mp/migrations/__init__.py b/oioioi/mp/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/oioioi/mp/models.py b/oioioi/mp/models.py new file mode 100644 index 000000000..0807af6d7 --- /dev/null +++ b/oioioi/mp/models.py @@ -0,0 +1,17 @@ +# coding: utf-8 +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from oioioi.base.utils.deps import check_django_app_dependencies + +from oioioi.participants.models import RegistrationModel + +check_django_app_dependencies(__name__, ['oioioi.participants']) + + +class MPRegistration(RegistrationModel): + terms_accepted = models.BooleanField(_("terms accepted"), default=False) + + def erase_data(self): + self.terms_accepted = False + self.save() \ No newline at end of file diff --git a/oioioi/mp/score.py b/oioioi/mp/score.py new file mode 100644 index 000000000..f27363b39 --- /dev/null +++ b/oioioi/mp/score.py @@ -0,0 +1,69 @@ +from functools import total_ordering + +from oioioi.contests.scores import IntegerScore, ScoreValue + + +@total_ordering +class PAScore(ScoreValue): + """PA style score. + + It consists of a number of points scored, together with their + distribution. + When two users get the same number of points, then the number of tasks + for which they got 10pts (maximal score) is taken into consideration. + If this still does not break the tie, number of 9 point scores is + considered, then 8 point scores etc. + """ + + symbol = 'MP' + + def __init__(self, points=None, distribution=None): + if points: + assert isinstance(points, IntegerScore) + self.points = points + else: + self.points = IntegerScore(0) + if distribution: + assert isinstance(distribution, ScoreDistribution) + self.distribution = distribution + else: + self.distribution = ScoreDistribution() + self.distribution.update(self.points.value) + + def __add__(self, other): + return PAScore( + self.points + other.points, self.distribution + other.distribution + ) + + def __eq__(self, other): + if not isinstance(other, PAScore): + return self.points == other + return (self.points, self.distribution) == (other.points, other.distribution) + + def __lt__(self, other): + if not isinstance(other, PAScore): + return self.points < other + return (self.points, self.distribution) < (other.points, other.distribution) + + def __unicode__(self): + return str(self.points) + + def __repr__(self): + return "PAScore(%r, %r)" % (self.points, self.distribution) + + def __str__(self): + return str(self.points) + + @classmethod + def _from_repr(cls, value): + points, distribution = value.split(';') + return cls( + points=IntegerScore._from_repr(points), + distribution=ScoreDistribution._from_repr(distribution), + ) + + def _to_repr(self): + return '%s;%s' % (self.points._to_repr(), self.distribution._to_repr()) + + def to_int(self): + return self.points.to_int() diff --git a/oioioi/mp/templates/mp/default_ranking.html b/oioioi/mp/templates/mp/default_ranking.html new file mode 100644 index 000000000..26062ca26 --- /dev/null +++ b/oioioi/mp/templates/mp/default_ranking.html @@ -0,0 +1,53 @@ +{% load i18n pagination_tags get_user_name static simple_filters %} + +{% if rows %} + +{% else %} +
+ {% blocktrans %}Strange, there is no one in this ranking...{% endblocktrans %} +
+{% endif %} + + + diff --git a/oioioi/mp/templates/mp/registration-notice.html b/oioioi/mp/templates/mp/registration-notice.html new file mode 100644 index 000000000..a5a13f825 --- /dev/null +++ b/oioioi/mp/templates/mp/registration-notice.html @@ -0,0 +1,8 @@ +{% load i18n %} +
+ {% url 'participants_register' contest_id=contest.id as reg_url %} + {% blocktrans %} + Heads up! In order to be able to send solutions fill the + contest registration form. + {% endblocktrans %} +
diff --git a/oioioi/mp/tests.py b/oioioi/mp/tests.py new file mode 100644 index 000000000..84525a6b3 --- /dev/null +++ b/oioioi/mp/tests.py @@ -0,0 +1,476 @@ +# import re +# from datetime import datetime # pylint: disable=E0611 + +# import urllib.parse + +# from django.contrib.admin.utils import quote +# from django.contrib.auth.models import User +# from django.core.files.base import ContentFile +# from django.test import RequestFactory +# from django.test.utils import override_settings +# from django.urls import reverse +# from django.utils.timezone import utc +# from oioioi.base.tests import TestCase, fake_time, fake_timezone_now +# from oioioi.contests.models import ( +# Contest, +# ProblemInstance, +# Submission, +# UserResultForProblem, +# ) +# from oioioi.contests.scores import IntegerScore +# from oioioi.pa.controllers import A_PLUS_B_RANKING_KEY, B_RANKING_KEY +# from oioioi.pa.models import PAProblemInstanceData, PARegistration +# from oioioi.pa.score import PAScore, ScoreDistribution +# from oioioi.participants.models import Participant, TermsAcceptedPhrase +# from oioioi.problems.models import Problem + + +# class TestPAScore(TestCase): +# def test_score_distribution(self): +# dist1 = ScoreDistribution([1] + [0] * 9) +# dist2 = ScoreDistribution([0] + [10] * 9) +# dist_null = ScoreDistribution([0] * 10) + +# self.assertLess(dist2, dist1) +# self.assertLess(dist1, dist1 + dist2) +# self.assertLess(dist2 + dist2, dist1) +# self.assertLess(dist_null, dist1) +# self.assertLess(dist_null, dist2) + +# self.assertEqual(dist_null, ScoreDistribution()) +# self.assertEqual(dist_null + dist_null, dist_null) +# self.assertEqual(dist1 + dist_null, dist1) + +# self.assertEqual( +# dist1._to_repr(), +# '00001:00000:00000:00000:00000:00000:00000:00000:00000:00000', +# ) +# self.assertEqual( +# dist2._to_repr(), +# '00000:00010:00010:00010:00010:00010:00010:00010:00010:00010', +# ) +# self.assertEqual( +# (dist1 + dist2)._to_repr(), +# '00001:00010:00010:00010:00010:00010:00010:00010:00010:00010', +# ) + +# self.assertEqual(dist1, ScoreDistribution._from_repr(dist1._to_repr())) +# self.assertEqual(dist2, ScoreDistribution._from_repr(dist2._to_repr())) + +# self.assertEqual( +# repr(dist1), +# 'ScoreDistribution(10: 1, 9: 0, 8: 0, 7: 0, 6: 0, 5: 0, 4: 0, ' +# '3: 0, 2: 0, 1: 0)', +# ) + +# def test_pa_score(self): +# score = [PAScore(IntegerScore(x)) for x in range(0, 11)] + +# self.assertLess(score[0], score[5]) +# self.assertLess(score[5], score[10]) +# self.assertLess(score[5] + score[5], score[10]) +# self.assertLess(score[5] + score[5], score[2] + score[2] + score[6]) +# self.assertLess(score[10], score[2] + score[4] + score[5]) +# self.assertLess(score[2] + score[2] + score[6], score[1] + score[3] + score[6]) + +# dist1 = ScoreDistribution([0] * 8 + [2, 4]) +# dist2 = ScoreDistribution([0] * 8 + [1, 6]) +# score1 = PAScore(IntegerScore(8), dist1) +# score2 = PAScore(IntegerScore(8), dist2) +# self.assertLess(score2, score1) + +# score3 = ( +# score[10] + score[10] + score[10] + score[4] + score[2] + score1 + score2 +# ) + +# self.assertEqual(score3, (3 * 10 + 4 + 2 + 2 * 8)) +# self.assertEqual( +# repr(score3), +# 'PAScore(IntegerScore(52), ScoreDistribution(10: 3, 9: 0, 8: ' +# '0, 7: 0, 6: 0, 5: 0, 4: 1, 3: 0, 2: 4, 1: 10))', +# ) +# self.assertEqual( +# score3._to_repr(), +# '0000000000000000052;00003:00000:' +# '00000:00000:00000:00000:00001:00000:00004:00010', +# ) +# self.assertEqual(score3, PAScore._from_repr(score3._to_repr())) + + +# class TestPARoundTimes(TestCase): +# fixtures = ['test_users', 'test_pa_contest'] + +# def test_round_states(self): +# contest = Contest.objects.get() +# controller = contest.controller + +# not_last_submission = Submission.objects.get(id=6) +# # user's last submission +# not_my_submission = Submission.objects.get(id=10) +# user = User.objects.get(username='test_user') + +# def check_round_state(date, expected): +# request = RequestFactory().request() +# request.contest = contest +# request.user = user +# request.timestamp = date + +# self.assertTrue(self.client.login(username='test_user')) +# with fake_timezone_now(date): +# url = reverse( +# 'ranking', kwargs={'contest_id': 'c', 'key': A_PLUS_B_RANKING_KEY} +# ) +# response = self.client.get(url) +# if expected[0]: +# self.assertContains(response, 'taskA1') +# else: +# self.assertNotContains(response, 'taskA1') + +# self.assertEqual( +# expected[1], controller.can_see_source(request, not_my_submission) +# ) + +# self.assertEqual( +# False, controller.can_see_source(request, not_last_submission) +# ) + +# dates = [ +# datetime(2012, 6, 1, 0, 0, tzinfo=utc), +# datetime(2012, 8, 1, 0, 0, tzinfo=utc), +# datetime(2012, 10, 1, 0, 0, tzinfo=utc), +# ] + +# # 1) results date of round 1 +# # 2) public results date of round 1 +# # 3) public results date of all rounds +# # +# # ============== ============== +# # can: | see ranking | see solutions of other participants +# # ============== ============== +# # 1 -> | | | +# # | False | False | +# # 2 -> | | | +# # | True | False | +# # 3 -> | | | +# # | True | True | +# # | | | +# # ============== ============== +# expected = [[False, False], [True, False], [True, True]] + +# for date, exp in zip(dates, expected): +# check_round_state(date, exp) + + +# class TestPARanking(TestCase): +# fixtures = ['test_users', 'test_pa_contest'] + +# def _ranking_url(self, key): +# contest = Contest.objects.get() +# return reverse('ranking', kwargs={'contest_id': contest.id, 'key': key}) + +# def test_divisions(self): +# def check_visibility(good_keys, response): +# division_for_pi = {1: 'A', 2: 'A', 3: 'B', 4: 'B', 5: 'NONE'} +# for key, div in division_for_pi.items(): +# p = ProblemInstance.objects.get(pk=key) +# if div in good_keys: +# self.assertContains(response, p.short_name) +# else: +# self.assertNotContains(response, p.short_name) + +# self.assertTrue(self.client.login(username='test_user')) + +# with fake_timezone_now(datetime(2013, 1, 1, 0, 0, tzinfo=utc)): +# response = self.client.get(self._ranking_url(B_RANKING_KEY)) +# check_visibility(['B'], response) +# response = self.client.get(self._ranking_url(A_PLUS_B_RANKING_KEY)) +# check_visibility(['A', 'B'], response) +# # Round 3 is trial +# response = self.client.get(self._ranking_url(3)) +# check_visibility(['NONE'], response) + +# def test_no_zero_scores_in_ranking(self): +# self.assertTrue(self.client.login(username='test_user')) +# with fake_time(datetime(2013, 1, 1, 0, 0, tzinfo=utc)): +# response = self.client.get(self._ranking_url(3)) +# # Test User should be present in the ranking. +# self.assertTrue(re.search(b']*>Test User', response.content)) +# # Test User 2 scored 0 points for the only task in the round. +# self.assertFalse(re.search(b']*>Test User 2', response.content)) + +# def test_ranking_ordering(self): +# def check_order(response, expected): +# prev_pos = 0 +# for user in expected: +# pattern = b']*>%s' % (user,) +# pattern_match = re.search(pattern, response.content) + +# self.assertTrue(pattern_match) +# self.assertContains(response, user) + +# pos = pattern_match.start() +# self.assertGreater( +# pos, prev_pos, msg=('User %s has incorrect ' 'position' % (user,)) +# ) +# prev_pos = pos + +# self.assertTrue(self.client.login(username='test_user')) + +# with fake_timezone_now(datetime(2013, 1, 1, 0, 0, tzinfo=utc)): +# # 28 (10, 8, 6, 4), 28 (9, 9, 7, 3), 10 (10) +# response = self.client.get(self._ranking_url(A_PLUS_B_RANKING_KEY)) +# check_order(response, [b'Test User', b'Test User 2', b'Test User 3']) +# self.assertContains(response, b'28') + +# # 10 (10), 10 (7, 3), 10 (6, 4) +# response = self.client.get(self._ranking_url(B_RANKING_KEY)) +# check_order(response, [b'Test User 3', b'Test User 2', b'Test User']) +# self.assertNotContains(response, b'28') + + +# class TestPARegistration(TestCase): +# fixtures = ['test_users', 'test_contest', 'test_terms_accepted_phrase'] + +# def setUp(self): +# contest = Contest.objects.get() +# contest.controller_name = 'oioioi.pa.controllers.PAContestController' +# contest.save() +# self.reg_data = { +# 'address': 'The Castle', +# 'postal_code': '31-337', +# 'city': 'Camelot', +# 't_shirt_size': 'L', +# 'job': 'AS', +# 'job_name': 'WSRH', +# 'terms_accepted': 't', +# } + +# def test_default_terms_accepted_phrase(self): +# TermsAcceptedPhrase.objects.get().delete() +# contest = Contest.objects.get() +# url = reverse('participants_register', kwargs={'contest_id': contest.id}) + +# self.assertTrue(self.client.login(username='test_user')) +# response = self.client.get(url) + +# self.assertContains( +# response, +# 'I declare that I have read the contest rules and ' +# 'the technical arrangements. I fully understand ' +# 'them and accept them unconditionally.', +# ) + +# def test_participants_registration(self): +# contest = Contest.objects.get() +# user = User.objects.get(username='test_user') +# url = reverse('participants_register', kwargs={'contest_id': contest.id}) +# self.assertTrue(self.client.login(username='test_user')) +# response = self.client.get(url) + +# self.assertContains(response, 'Postal code') +# self.assertContains(response, 'Test terms accepted') + +# user.first_name = 'Sir Lancelot' +# user.last_name = 'du Lac' +# user.save() + +# response = self.client.post(url, self.reg_data) +# self.assertEqual(302, response.status_code) + +# registration = PARegistration.objects.get(participant__user=user) +# self.assertEqual(registration.address, self.reg_data['address']) + +# def test_contest_info(self): +# contest = Contest.objects.get() +# user = User.objects.get(username='test_user') +# p = Participant(contest=contest, user=user) +# p.save() +# PARegistration(participant_id=p.id, **self.reg_data).save() +# url = reverse('contest_info', kwargs={'contest_id': contest.id}) +# data = self.client.get(url).json() +# self.assertEqual(data['users_count'], 1) + + +# class TestPAScorer(TestCase): +# t_results_ok = ( +# ( +# {'exec_time_limit': 100, 'max_score': 100}, +# {'result_code': 'OK', 'time_used': 0}, +# ), +# ( +# {'exec_time_limit': 100, 'max_score': 10}, +# {'result_code': 'OK', 'time_used': 99}, +# ), +# ( +# {'exec_time_limit': 1000, 'max_score': 0}, +# {'result_code': 'OK', 'time_used': 123}, +# ), +# ) + +# t_expected_ok = [ +# (IntegerScore(1), IntegerScore(1), 'OK'), +# (IntegerScore(1), IntegerScore(1), 'OK'), +# (IntegerScore(0), IntegerScore(0), 'OK'), +# ] + +# t_results_wrong = [ +# ( +# {'exec_time_limit': 100, 'max_score': 100}, +# {'result_code': 'WA', 'time_used': 75}, +# ), +# ( +# {'exec_time_limit': 100, 'max_score': 0}, +# {'result_code': 'RV', 'time_used': 75}, +# ), +# ] + +# t_expected_wrong = [ +# (IntegerScore(0), IntegerScore(1), 'WA'), +# (IntegerScore(0), IntegerScore(0), 'RV'), +# ] + +# def test_pa_test_scorer(self): +# results = list(map(utils.pa_test_scorer, *list(zip(*self.t_results_ok)))) +# self.assertEqual(self.t_expected_ok, results) + +# results = list(map(utils.pa_test_scorer, *list(zip(*self.t_results_wrong)))) +# self.assertEqual(self.t_expected_wrong, results) + + +# class TestPAResults(TestCase): +# fixtures = ['test_users', 'test_pa_contest'] + +# def test_pa_user_results(self): +# contest = Contest.objects.get() +# user = User.objects.get(username='test_user') +# old_results = sorted( +# [result.score for result in UserResultForProblem.objects.filter(user=user)] +# ) +# for pi in ProblemInstance.objects.all(): +# contest.controller.update_user_results(user, pi) +# new_results = sorted( +# [result.score for result in UserResultForProblem.objects.filter(user=user)] +# ) +# self.assertEqual(old_results, new_results) + + +# @override_settings( +# PROBLEM_PACKAGE_BACKENDS=('oioioi.problems.tests.DummyPackageBackend',) +# ) +# class TestPADivisions(TestCase): +# fixtures = ['test_users', 'test_contest'] + +# def test_prolem_upload(self): +# contest = Contest.objects.get() +# contest.controller_name = 'oioioi.pa.controllers.PAContestController' +# contest.save() + +# self.assertTrue(self.client.login(username='test_admin')) +# url = ( +# reverse('add_or_update_problem', kwargs={'contest_id': contest.id}) +# + '?' +# + urllib.parse.urlencode({'key': 'upload'}) +# ) + +# response = self.client.get(url) +# # "NONE" is the default division +# self.assertContains(response, '') + +# data = { +# 'package_file': ContentFile('eloziom', name='foo'), +# 'visibility': Problem.VISIBILITY_FRIENDS, +# 'division': 'A', +# } +# response = self.client.post(url, data, follow=True) +# self.assertEqual(response.status_code, 200) +# pid = PAProblemInstanceData.objects.get() +# problem = Problem.objects.get() +# self.assertEqual(pid.division, 'A') +# self.assertEqual(pid.problem_instance.problem, problem) + +# url = ( +# reverse('add_or_update_problem', kwargs={'contest_id': contest.id}) +# + '?' +# + urllib.parse.urlencode({'problem': problem.id, 'key': 'upload'}) +# ) +# response = self.client.get(url) +# self.assertContains(response, '') + + +# class TestPAContestInfo(TestCase): +# fixtures = ['test_users', 'test_pa_contest'] + +# def test_contest_info_anonymous(self): +# c = Contest.objects.get() +# url = reverse('contest_info', kwargs={'contest_id': c.id}) +# self.client.logout() +# response = self.client.get(url).json() +# self.assertEqual(response['users_count'], 2) + +# def test_cross_origin(self): +# c = Contest.objects.get() +# url = reverse('contest_info', kwargs={'contest_id': c.id}) +# response = self.client.get(url) +# self.assertEqual(response['Access-Control-Allow-Origin'], '*') + + +# class TestPASafeExecModes(TestCase): +# fixtures = ['test_pa_contests_safe_exec_mode'] + +# def test_pa_quals_controller_safe_exec_mode(self): +# c = Contest.objects.get(pk="quals") +# self.assertEqual(c.controller.get_safe_exec_mode(), 'cpu') + +# def test_pa_finals_controller_safe_exec_mode(self): +# c = Contest.objects.get(pk="finals") +# self.assertEqual(c.controller.get_safe_exec_mode(), 'cpu') + + +# class TestPAAdmin(TestCase): +# fixtures = [ +# 'test_users', +# 'test_contest', +# 'test_pa_registration', +# 'test_permissions', +# ] + +# def setUp(self): +# contest = Contest.objects.get() +# contest.controller_name = 'oioioi.pa.controllers.PAContestController' +# contest.save() + +# def test_terms_accepted_phrase_inline_admin_permissions(self): +# PARegistration.objects.all().delete() + +# # Logging as superuser. +# self.assertTrue(self.client.login(username='test_admin')) +# self.client.get('/c/c/') # 'c' becomes the current contest +# url = reverse('oioioiadmin:contests_contest_change', args=(quote('c'),)) + +# response = self.client.get(url) +# self.assertContains(response, 'Text asking participant to accept contest terms') + +# # Checks if the field is editable. +# self.assertContains(response, 'id_terms_accepted_phrase-0-text') + +# # Logging as contest admin. +# self.assertTrue(self.client.login(username='test_contest_admin')) +# self.client.get('/c/c/') # 'c' becomes the current contest +# url = reverse('oioioiadmin:contests_contest_change', args=(quote('c'),)) + +# response = self.client.get(url) +# self.assertContains(response, 'Text asking participant to accept contest terms') + +# # Checks if the field is editable. +# self.assertContains(response, 'id_terms_accepted_phrase-0-text') + +# def test_terms_accepted_phrase_inline_edit_restrictions(self): +# self.assertTrue(self.client.login(username='test_admin')) +# self.client.get('/c/c/') # 'c' becomes the current contest +# url = reverse('oioioiadmin:contests_contest_change', args=(quote('c'),)) + +# response = self.client.get(url) +# self.assertContains(response, 'Text asking participant to accept contest terms') + +# # Checks if the field is not editable. +# self.assertNotContains(response, 'id_terms_accepted_phrase-0-text') diff --git a/oioioi/mp/urls.py b/oioioi/mp/urls.py new file mode 100644 index 000000000..fe4bd1a90 --- /dev/null +++ b/oioioi/mp/urls.py @@ -0,0 +1,9 @@ +from django.urls import re_path + +from oioioi.mp import views + +app_name = 'mp' + +contest_patterns = [ + #re_path(r'^contest_info/$', views.contest_info_view, name='contest_info') +] diff --git a/oioioi/mp/views.py b/oioioi/mp/views.py new file mode 100644 index 000000000..aa0a6d382 --- /dev/null +++ b/oioioi/mp/views.py @@ -0,0 +1,23 @@ +from django.contrib.auth.models import User +from django.template.loader import render_to_string + +from oioioi.base.utils import allow_cross_origin, jsonify +from oioioi.contests.utils import is_contest_admin +from oioioi.dashboard.registry import dashboard_headers_registry +from oioioi.mp.controllers import MPRegistrationController +from oioioi.participants.utils import is_participant + + +@dashboard_headers_registry.register_decorator(order=10) +def registration_notice_fragment(request): + rc = request.contest.controller.registration_controller() + if ( + isinstance(rc, MPRegistrationController) + and request.user.is_authenticated + and not is_contest_admin(request) + and not is_participant(request) + and rc.can_register(request) + ): + return render_to_string('mp/registration-notice.html', request=request) + else: + return None From 4cb7177036b0d85a3a60c976135c25849c3cefa6 Mon Sep 17 00:00:00 2001 From: geoff128 Date: Mon, 2 Jan 2023 22:28:26 +0100 Subject: [PATCH 02/14] added mp module to default_settings --- oioioi/cypress_settings.py | 1 + oioioi/default_settings.py | 1 + oioioi/deployment/settings.py.template | 1 + oioioi/selenium_settings.py | 1 + oioioi/test_settings.py | 1 + 5 files changed, 5 insertions(+) diff --git a/oioioi/cypress_settings.py b/oioioi/cypress_settings.py index e3aefb085..7279051bc 100644 --- a/oioioi/cypress_settings.py +++ b/oioioi/cypress_settings.py @@ -22,6 +22,7 @@ 'oioioi.newsfeed', 'oioioi.simpleui', 'oioioi.livedata', + 'oioioi.mp', ) + INSTALLED_APPS DATABASES = { diff --git a/oioioi/default_settings.py b/oioioi/default_settings.py index ef9311961..a5525d21b 100755 --- a/oioioi/default_settings.py +++ b/oioioi/default_settings.py @@ -217,6 +217,7 @@ 'oioioi.workers', 'oioioi.quizzes', 'oioioi._locale', + 'oioioi.mp', 'djsupervisor', 'registration', diff --git a/oioioi/deployment/settings.py.template b/oioioi/deployment/settings.py.template index 0d142c34c..6efd5c21d 100755 --- a/oioioi/deployment/settings.py.template +++ b/oioioi/deployment/settings.py.template @@ -346,6 +346,7 @@ INSTALLED_APPS = ( # 'oioioi.problemsharing', # 'oioioi.usergroups', # 'oioioi.usercontests', + 'oioioi.mp', ) + INSTALLED_APPS # Set to True to show the link to the problemset with contests on navbar. diff --git a/oioioi/selenium_settings.py b/oioioi/selenium_settings.py index 633ef1159..c4c583efb 100644 --- a/oioioi/selenium_settings.py +++ b/oioioi/selenium_settings.py @@ -22,6 +22,7 @@ 'oioioi.newsfeed', 'oioioi.simpleui', 'oioioi.livedata', + 'oioioi.mp', ) + INSTALLED_APPS TEMPLATES[0]['OPTIONS']['context_processors'] += [ diff --git a/oioioi/test_settings.py b/oioioi/test_settings.py index 944121d50..95a8861e6 100644 --- a/oioioi/test_settings.py +++ b/oioioi/test_settings.py @@ -58,6 +58,7 @@ 'oioioi.usergroups', 'oioioi.problemsharing', 'oioioi.usercontests', + 'oioioi.mp', ) + INSTALLED_APPS TEMPLATES[0]['OPTIONS']['context_processors'] += [ From 299352e43a6f17d8f31ff96f543d7fc34c536e98 Mon Sep 17 00:00:00 2001 From: geoff128 Date: Mon, 2 Jan 2023 23:01:04 +0100 Subject: [PATCH 03/14] typo in mp/controllers.py --- oioioi/mp/controllers.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/oioioi/mp/controllers.py b/oioioi/mp/controllers.py index d4d28ce06..cb386c4e3 100644 --- a/oioioi/mp/controllers.py +++ b/oioioi/mp/controllers.py @@ -6,11 +6,13 @@ from django.db.models import Q from django.http import HttpResponse from django.shortcuts import redirect +from django.template.loader import render_to_string from django.template.response import TemplateResponse from django.utils.translation import gettext_lazy as _ from django.utils.encoding import force_str from django.utils.translation import gettext_lazy as _ + from oioioi.base.utils.query_helpers import Q_always_true from oioioi.base.utils.redirect import safe_redirect from oioioi.contests.utils import ( @@ -136,12 +138,12 @@ class MPRankingController(DefaultRankingController): description = _("MP style ranking") - def _render_ranking_page(self, key, data, page): - request = self._fake_request(page) - data['is_admin'] = self.is_admin_key(key) - return render_to_string( - 'mp/defaut_ranking.html', context=data, request=request - ) + # def _render_ranking_page(self, key, data, page): + # request = self._fake_request(page) + # data['is_admin'] = self.is_admin_key(key) + # return render_to_string( + # 'mp/default_ranking.html', context=data, request=request + # ) def _get_csv_header(self, key, data): header = [_("No."), _("Login"), _("First name"), _("Last name"), _("Sum")] From c5fd5fffca28f2bdf7d1e160e9a8f044f1e64299 Mon Sep 17 00:00:00 2001 From: geoff128 Date: Thu, 5 Jan 2023 02:14:38 +0100 Subject: [PATCH 04/14] further changes for MP --- oioioi/deployment/settings.py.template | 1 - oioioi/mp/admin.py | 24 +- oioioi/mp/controllers.py | 159 +- oioioi/mp/fixtures/test_mp_contest.json | 1350 +++++++++++++++++ oioioi/mp/fixtures/test_mp_users.json | 75 + .../migrations/0002_roundscoremultiplier.py | 24 + oioioi/mp/models.py | 17 +- oioioi/mp/score.py | 74 +- .../mp/{default_ranking.html => ranking.html} | 0 oioioi/mp/templates/mp/registration.html | 40 + oioioi/mp/tests.py | 243 +-- oioioi/test_settings.py | 1 - 12 files changed, 1726 insertions(+), 282 deletions(-) create mode 100644 oioioi/mp/fixtures/test_mp_contest.json create mode 100644 oioioi/mp/fixtures/test_mp_users.json create mode 100644 oioioi/mp/migrations/0002_roundscoremultiplier.py rename oioioi/mp/templates/mp/{default_ranking.html => ranking.html} (100%) create mode 100644 oioioi/mp/templates/mp/registration.html diff --git a/oioioi/deployment/settings.py.template b/oioioi/deployment/settings.py.template index 6efd5c21d..0d142c34c 100755 --- a/oioioi/deployment/settings.py.template +++ b/oioioi/deployment/settings.py.template @@ -346,7 +346,6 @@ INSTALLED_APPS = ( # 'oioioi.problemsharing', # 'oioioi.usergroups', # 'oioioi.usercontests', - 'oioioi.mp', ) + INSTALLED_APPS # Set to True to show the link to the problemset with contests on navbar. diff --git a/oioioi/mp/admin.py b/oioioi/mp/admin.py index bf3cc35c2..9ba9cf843 100644 --- a/oioioi/mp/admin.py +++ b/oioioi/mp/admin.py @@ -1,9 +1,10 @@ from django.utils.translation import gettext_lazy as _ from oioioi.base import admin -from oioioi.contests.utils import is_contest_admin +from oioioi.contests.admin import ContestAdmin +from oioioi.contests.models import User from oioioi.mp.forms import MPRegistrationForm -from oioioi.mp.models import MPRegistration +from oioioi.mp.models import MPRegistration, SubmissionScoreMultiplier from oioioi.participants.admin import ParticipantAdmin @@ -33,3 +34,22 @@ def get_actions(self, request): if 'delete_selected' in actions: del actions['delete_selected'] return actions + + +class SubmissionScoreMultiplierInline(admin.StackedInline): + model = SubmissionScoreMultiplier + extra = 0 + category = _("Advanced") + + +class SubmissionScoreMultiplierAdminMixin(object): + """Adds :class:`~oioioi.mp.SubmissionScoreMultiplier` fields to an + admin panel. + """ + + def __init__(self, *args, **kwargs): + super(SubmissionScoreMultiplierAdminMixin, self).__init__(*args, **kwargs) + self.inlines = self.inlines + [SubmissionScoreMultiplierInline] + + +ContestAdmin.mix_in(SubmissionScoreMultiplierAdminMixin) diff --git a/oioioi/mp/controllers.py b/oioioi/mp/controllers.py index cb386c4e3..fc6acca67 100644 --- a/oioioi/mp/controllers.py +++ b/oioioi/mp/controllers.py @@ -1,36 +1,26 @@ -import datetime -import logging -import unicodecsv - -from django import forms -from django.db.models import Q -from django.http import HttpResponse from django.shortcuts import redirect from django.template.loader import render_to_string from django.template.response import TemplateResponse from django.utils.translation import gettext_lazy as _ -from django.utils.encoding import force_str -from django.utils.translation import gettext_lazy as _ from oioioi.base.utils.query_helpers import Q_always_true from oioioi.base.utils.redirect import safe_redirect -from oioioi.contests.utils import ( - all_non_trial_public_results_visible, - is_contest_admin, - is_contest_observer, -) -from oioioi.filetracker.utils import make_content_disposition_header -from oioioi.mp.models import MPRegistration -# from oioioi.mp.score import PAScore +from oioioi.contests.models import Submission +from oioioi.mp.models import MPRegistration, SubmissionScoreMultiplier +from oioioi.mp.score import FloatScore from oioioi.participants.controllers import ParticipantsController from oioioi.participants.models import Participant from oioioi.participants.utils import is_participant from oioioi.programs.controllers import ProgrammingContestController from oioioi.rankings.controllers import DefaultRankingController +CONTEST_RANKING_KEY = 'c' + class MPRegistrationController(ParticipantsController): + registration_template = 'mp/registration.html' + @property def form_class(self): from oioioi.mp.forms import MPRegistrationForm @@ -60,16 +50,13 @@ def visible_contests_query(self, request): def can_register(self, request): return True - def can_unregister(self, request, participant): - return False - def registration_view(self, request): participant = self._get_participant_for_form(request) - if 'mp_paregistrationformdata' in request.session: + if 'mp_mpregistrationformdata' in request.session: # pylint: disable=not-callable - form = self.form_class(request.session['mp_paregistrationformdata']) - del request.session['mp_paregistrationformdata'] + form = self.form_class(request.session['mp_mpregistrationformdata']) + del request.session['mp_mpregistrationformdata'] else: form = self.get_form(request, participant) form.set_terms_accepted_text(self.get_terms_accepted_phrase()) @@ -85,8 +72,15 @@ def registration_view(self, request): return safe_redirect(request, request.GET['next']) else: return redirect('default_contest_view', contest_id=self.contest.id) - - context = {'form': form, 'participant': participant} + can_unregister = False + if participant: + can_unregister = self.can_unregister(request, participant) + context = { + 'form': form, + 'participant': participant, + 'can_unregister': can_unregister, + 'contest_name': self.contest.name + } return TemplateResponse(request, self.registration_template, context) def mixins_for_admin(self): @@ -106,10 +100,7 @@ class MPContestController(ProgrammingContestController): description = _("Mistrz Programowania") create_forum = False - # def update_user_result_for_problem(self, result): - # super(MPContestController, self).update_user_result_for_problem(result) - # if result.score is not None: - # result.score = MPScore(result.score) + show_email_in_participants_data = True def registration_controller(self): return MPRegistrationController(self.contest) @@ -117,63 +108,91 @@ def registration_controller(self): def ranking_controller(self): return MPRankingController(self.contest) - def separate_public_results(self): - return True + def update_user_result_for_problem(self, result): + submissions = Submission.objects.filter( + problem_instance=result.problem_instance, + user=result.user, + kind='NORMAL', + score__isnull=False, + ) + + if submissions: + best_submission = None + for submission in submissions: + ssm = SubmissionScoreMultiplier.objects.filter( + contest=submission.problem_instance.contest, + ) + + if submission.score is None: + continue + 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] + + result.score = best_submission[1] + result.status = best_submission[0].status + else: + result.score = None + result.status = None def can_submit(self, request, problem_instance, check_round_times=True): + """Contest admin can always submit + + """ if request.user.is_anonymous: return False if request.user.has_perm('contests.contest_admin', self.contest): return True if not is_participant(request): return False + + rtimes = self.get_round_times(None, problem_instance.round) + round_over_contest_running = ( + rtimes.is_past(request.timestamp) and + SubmissionScoreMultiplier.objects.filter( + contest=problem_instance.contest, + end_date__gt=request.timestamp, + ) + ) return super(MPContestController, self).can_submit( request, problem_instance, check_round_times - ) + ) or round_over_contest_running class MPRankingController(DefaultRankingController): - """ + """Changes to Default Ranking: + 1. Sum column is just after User column + 2. Rounds with earlier start_date are more to the left """ description = _("MP style ranking") - # def _render_ranking_page(self, key, data, page): - # request = self._fake_request(page) - # data['is_admin'] = self.is_admin_key(key) - # return render_to_string( - # 'mp/default_ranking.html', context=data, request=request - # ) - - def _get_csv_header(self, key, data): - header = [_("No."), _("Login"), _("First name"), _("Last name"), _("Sum")] - for pi, _statement_visible in data['problem_instances']: - header.append(pi.get_short_name_display()) - return header - - def _get_csv_row(self, key, row): - line = [ - row['place'], - row['user'].username, - row['user'].first_name, - row['user'].last_name, - row['sum'], - ] - line += [r.score if r and r.score is not None else '' for r in row['results']] - return line - - def render_ranking_to_csv(self, request, partial_key): - key = self.get_full_key(request, partial_key) - data = self.serialize_ranking(key) - - response = HttpResponse(content_type='text/csv') - response['Content-Disposition'] = make_content_disposition_header( - 'attachment', u'%s-%s-%s.csv' % (_("ranking"), self.contest.id, key) + def _iter_rounds(self, can_see_all, timestamp, partial_key, request=None): + ccontroller = self.contest.controller + queryset = self.contest.round_set.all().order_by("-start_date") + if partial_key != CONTEST_RANKING_KEY: + queryset = queryset.filter(id=partial_key).order_by("-start_date") + for round in queryset: + times = ccontroller.get_round_times(request, round) + if can_see_all or times.public_results_visible(timestamp): + yield round + + def _filter_pis_for_ranking(self, partial_key, queryset): + return queryset.order_by("-round__start_date") + + def _render_ranking_page(self, key, data, page): + request = self._fake_request(page) + data['is_admin'] = self.is_admin_key(key) + return render_to_string( + 'mp/ranking.html', context=data, request=request ) - writer = unicodecsv.writer(response) - writer.writerow(list(map(force_str, self._get_csv_header(key, data)))) - for row in data['rows']: - writer.writerow(list(map(force_str, self._get_csv_row(key, row)))) - - return response + def _allow_zero_score(self): + return False diff --git a/oioioi/mp/fixtures/test_mp_contest.json b/oioioi/mp/fixtures/test_mp_contest.json new file mode 100644 index 000000000..9fdd8e25a --- /dev/null +++ b/oioioi/mp/fixtures/test_mp_contest.json @@ -0,0 +1,1350 @@ +[ + { + "model": "contests.contest", + "pk": "contest1", + "fields": { + "name": "contest1", + "controller_name": "oioioi.mp.controllers.MPContestController", + "creation_date": "2023-01-04T23:05:16.030Z", + "default_submissions_limit": 10, + "contact_email": "", + "judging_priority": 10, + "judging_weight": 1000, + "enable_editor": false + } + }, + { + "model": "problems.problem", + "pk": 1, + "fields": { + "legacy_name": "squ", + "short_name": "squ", + "controller_name": "oioioi.sinolpack.controllers.SinolProblemController" + } + }, + { + "model": "problems.problem", + "pk": 2, + "fields": { + "legacy_name": "squ", + "short_name": "squ", + "controller_name": "oioioi.sinolpack.controllers.SinolProblemController" + } + }, + { + "model": "contests.round", + "pk": 1, + "fields": { + "contest": "contest1", + "name": "Round 1", + "start_date": "2023-01-04T23:00:00Z", + "end_date": "2023-01-04T23:30:00Z", + "results_date": "2023-01-04T23:00:01Z", + "public_results_date": null, + "is_trial": false + } + }, + { + "model": "contests.round", + "pk": 2, + "fields": { + "contest": "contest1", + "name": "Round 2", + "start_date": "2023-01-04T23:15:00Z", + "end_date": "2023-01-04T23:45:00Z", + "results_date": "2023-01-04T23:15:00Z", + "public_results_date": null, + "is_trial": false + } + }, + { + "model": "contests.probleminstance", + "pk": 1, + "fields": { + "contest": null, + "round": null, + "problem": 1, + "short_name": "squ_main", + "submissions_limit": 10, + "needs_rejudge": false + } + }, + { + "model": "contests.probleminstance", + "pk": 2, + "fields": { + "contest": "contest1", + "round": 1, + "problem": 1, + "short_name": "squ", + "submissions_limit": 10, + "needs_rejudge": false + } + }, + { + "model": "contests.probleminstance", + "pk": 3, + "fields": { + "contest": null, + "round": null, + "problem": 2, + "short_name": "squ_main", + "submissions_limit": 10, + "needs_rejudge": false + } + }, + { + "model": "contests.probleminstance", + "pk": 4, + "fields": { + "contest": "contest1", + "round": 2, + "problem": 2, + "short_name": "squ1", + "submissions_limit": 10, + "needs_rejudge": false + } + }, + { + "model": "contests.submission", + "pk": 1, + "fields": { + "problem_instance": 2, + "user": 2, + "date": "2023-01-04T23:09:47.480Z", + "kind": "NORMAL", + "score": null, + "status": "CE", + "comment": "" + } + }, + { + "model": "contests.submission", + "pk": 2, + "fields": { + "problem_instance": 2, + "user": 2, + "date": "2023-01-04T23:10:15.645Z", + "kind": "NORMAL", + "score": "int:0000000000000000100", + "status": "INI_OK", + "comment": "" + } + }, + { + "model": "contests.submission", + "pk": 3, + "fields": { + "problem_instance": 2, + "user": 2, + "date": "2023-01-04T23:10:22.649Z", + "kind": "NORMAL", + "score": "int:0000000000000000066", + "status": "INI_OK", + "comment": "" + } + }, + { + "model": "contests.submission", + "pk": 4, + "fields": { + "problem_instance": 2, + "user": 2, + "date": "2023-01-04T23:13:28.652Z", + "kind": "IGNORED", + "score": "int:0000000000000000100", + "status": "INI_OK", + "comment": "" + } + }, + { + "model": "contests.submission", + "pk": 5, + "fields": { + "problem_instance": 4, + "user": 2, + "date": "2023-01-04T23:15:09.703Z", + "kind": "NORMAL", + "score": "int:0000000000000000100", + "status": "INI_OK", + "comment": "" + } + }, + { + "model": "contests.submission", + "pk": 6, + "fields": { + "problem_instance": 2, + "user": 2, + "date": "2023-01-04T23:16:51.055Z", + "kind": "NORMAL", + "score": "int:0000000000000000066", + "status": "INI_OK", + "comment": "" + } + }, + { + "model": "contests.submission", + "pk": 7, + "fields": { + "problem_instance": 4, + "user": 3, + "date": "2023-01-04T23:24:50.615Z", + "kind": "NORMAL", + "score": "int:0000000000000000100", + "status": "INI_OK", + "comment": "" + } + }, + { + "model": "contests.submission", + "pk": 8, + "fields": { + "problem_instance": 4, + "user": 3, + "date": "2023-01-04T23:27:19.682Z", + "kind": "NORMAL", + "score": "int:0000000000000000000", + "status": "INI_OK", + "comment": "" + } + }, + { + "model": "contests.submission", + "pk": 9, + "fields": { + "problem_instance": 2, + "user": 3, + "date": "2023-01-04T23:30:12.410Z", + "kind": "NORMAL", + "score": "int:0000000000000000100", + "status": "INI_OK", + "comment": "" + } + }, + { + "model": "contests.submission", + "pk": 10, + "fields": { + "problem_instance": 2, + "user": 3, + "date": "2023-01-04T23:30:47.495Z", + "kind": "NORMAL", + "score": "int:0000000000000000066", + "status": "INI_OK", + "comment": "" + } + }, + { + "model": "contests.submission", + "pk": 11, + "fields": { + "problem_instance": 2, + "user": 2, + "date": "2023-01-04T23:31:20.972Z", + "kind": "NORMAL", + "score": "int:0000000000000000016", + "status": "INI_OK", + "comment": "" + } + }, + { + "model": "contests.submission", + "pk": 12, + "fields": { + "problem_instance": 2, + "user": 4, + "date": "2023-01-04T23:46:58.520Z", + "kind": "NORMAL", + "score": "int:0000000000000000100", + "status": "INI_OK", + "comment": "" + } + }, + { + "model": "contests.submission", + "pk": 13, + "fields": { + "problem_instance": 4, + "user": 4, + "date": "2023-01-04T23:47:06.106Z", + "kind": "NORMAL", + "score": "int:0000000000000000100", + "status": "INI_OK", + "comment": "" + } + }, + { + "model": "contests.submission", + "pk": 14, + "fields": { + "problem_instance": 2, + "user": 5, + "date": "2023-01-05T00:05:18.423Z", + "kind": "NORMAL", + "score": null, + "status": "CE", + "comment": "" + } + }, + { + "model": "contests.submission", + "pk": 15, + "fields": { + "problem_instance": 4, + "user": 5, + "date": "2023-01-05T00:05:49.898Z", + "kind": "NORMAL", + "score": "int:0000000000000000000", + "status": "INI_OK", + "comment": "" + } + }, + { + "model": "contests.submissionreport", + "pk": 1, + "fields": { + "submission": 1, + "creation_date": "2023-01-04T23:09:49.129Z", + "kind": "INITIAL", + "status": "SUPERSEDED" + } + }, + { + "model": "contests.submissionreport", + "pk": 2, + "fields": { + "submission": 1, + "creation_date": "2023-01-04T23:09:49.159Z", + "kind": "NORMAL", + "status": "SUPERSEDED" + } + }, + { + "model": "contests.submissionreport", + "pk": 3, + "fields": { + "submission": 1, + "creation_date": "2023-01-04T23:09:49.200Z", + "kind": "FAILURE", + "status": "SUPERSEDED" + } + }, + { + "model": "contests.submissionreport", + "pk": 4, + "fields": { + "submission": 2, + "creation_date": "2023-01-04T23:10:16.993Z", + "kind": "INITIAL", + "status": "SUPERSEDED" + } + }, + { + "model": "contests.submissionreport", + "pk": 5, + "fields": { + "submission": 2, + "creation_date": "2023-01-04T23:10:17.207Z", + "kind": "NORMAL", + "status": "SUPERSEDED" + } + }, + { + "model": "contests.submissionreport", + "pk": 6, + "fields": { + "submission": 3, + "creation_date": "2023-01-04T23:10:23.917Z", + "kind": "INITIAL", + "status": "SUPERSEDED" + } + }, + { + "model": "contests.submissionreport", + "pk": 7, + "fields": { + "submission": 3, + "creation_date": "2023-01-04T23:10:24.107Z", + "kind": "NORMAL", + "status": "SUPERSEDED" + } + }, + { + "model": "contests.submissionreport", + "pk": 8, + "fields": { + "submission": 4, + "creation_date": "2023-01-04T23:13:29.977Z", + "kind": "INITIAL", + "status": "SUPERSEDED" + } + }, + { + "model": "contests.submissionreport", + "pk": 9, + "fields": { + "submission": 4, + "creation_date": "2023-01-04T23:13:30.156Z", + "kind": "NORMAL", + "status": "SUPERSEDED" + } + }, + { + "model": "contests.submissionreport", + "pk": 10, + "fields": { + "submission": 5, + "creation_date": "2023-01-04T23:15:11.080Z", + "kind": "INITIAL", + "status": "SUPERSEDED" + } + }, + { + "model": "contests.submissionreport", + "pk": 11, + "fields": { + "submission": 5, + "creation_date": "2023-01-04T23:15:11.313Z", + "kind": "NORMAL", + "status": "SUPERSEDED" + } + }, + { + "model": "contests.submissionreport", + "pk": 12, + "fields": { + "submission": 1, + "creation_date": "2023-01-04T23:15:37.527Z", + "kind": "INITIAL", + "status": "ACTIVE" + } + }, + { + "model": "contests.submissionreport", + "pk": 13, + "fields": { + "submission": 1, + "creation_date": "2023-01-04T23:15:37.560Z", + "kind": "NORMAL", + "status": "ACTIVE" + } + }, + { + "model": "contests.submissionreport", + "pk": 14, + "fields": { + "submission": 1, + "creation_date": "2023-01-04T23:15:37.602Z", + "kind": "FAILURE", + "status": "INACTIVE" + } + }, + { + "model": "contests.submissionreport", + "pk": 15, + "fields": { + "submission": 2, + "creation_date": "2023-01-04T23:15:38.746Z", + "kind": "INITIAL", + "status": "ACTIVE" + } + }, + { + "model": "contests.submissionreport", + "pk": 16, + "fields": { + "submission": 3, + "creation_date": "2023-01-04T23:15:38.832Z", + "kind": "INITIAL", + "status": "ACTIVE" + } + }, + { + "model": "contests.submissionreport", + "pk": 17, + "fields": { + "submission": 4, + "creation_date": "2023-01-04T23:15:39.975Z", + "kind": "INITIAL", + "status": "ACTIVE" + } + }, + { + "model": "contests.submissionreport", + "pk": 18, + "fields": { + "submission": 2, + "creation_date": "2023-01-04T23:15:40.103Z", + "kind": "NORMAL", + "status": "ACTIVE" + } + }, + { + "model": "contests.submissionreport", + "pk": 19, + "fields": { + "submission": 5, + "creation_date": "2023-01-04T23:15:40.196Z", + "kind": "INITIAL", + "status": "ACTIVE" + } + }, + { + "model": "contests.submissionreport", + "pk": 20, + "fields": { + "submission": 3, + "creation_date": "2023-01-04T23:15:40.254Z", + "kind": "NORMAL", + "status": "ACTIVE" + } + }, + { + "model": "contests.submissionreport", + "pk": 21, + "fields": { + "submission": 4, + "creation_date": "2023-01-04T23:15:40.343Z", + "kind": "NORMAL", + "status": "ACTIVE" + } + }, + { + "model": "contests.submissionreport", + "pk": 22, + "fields": { + "submission": 5, + "creation_date": "2023-01-04T23:15:40.429Z", + "kind": "NORMAL", + "status": "ACTIVE" + } + }, + { + "model": "contests.submissionreport", + "pk": 23, + "fields": { + "submission": 6, + "creation_date": "2023-01-04T23:16:52.371Z", + "kind": "INITIAL", + "status": "ACTIVE" + } + }, + { + "model": "contests.submissionreport", + "pk": 24, + "fields": { + "submission": 6, + "creation_date": "2023-01-04T23:16:52.556Z", + "kind": "NORMAL", + "status": "ACTIVE" + } + }, + { + "model": "contests.submissionreport", + "pk": 25, + "fields": { + "submission": 7, + "creation_date": "2023-01-04T23:24:51.909Z", + "kind": "INITIAL", + "status": "ACTIVE" + } + }, + { + "model": "contests.submissionreport", + "pk": 26, + "fields": { + "submission": 7, + "creation_date": "2023-01-04T23:24:52.097Z", + "kind": "NORMAL", + "status": "ACTIVE" + } + }, + { + "model": "contests.submissionreport", + "pk": 27, + "fields": { + "submission": 8, + "creation_date": "2023-01-04T23:27:20.955Z", + "kind": "INITIAL", + "status": "ACTIVE" + } + }, + { + "model": "contests.submissionreport", + "pk": 28, + "fields": { + "submission": 8, + "creation_date": "2023-01-04T23:27:21.143Z", + "kind": "NORMAL", + "status": "ACTIVE" + } + }, + { + "model": "contests.submissionreport", + "pk": 29, + "fields": { + "submission": 9, + "creation_date": "2023-01-04T23:30:13.762Z", + "kind": "INITIAL", + "status": "ACTIVE" + } + }, + { + "model": "contests.submissionreport", + "pk": 30, + "fields": { + "submission": 9, + "creation_date": "2023-01-04T23:30:13.956Z", + "kind": "NORMAL", + "status": "ACTIVE" + } + }, + { + "model": "contests.submissionreport", + "pk": 31, + "fields": { + "submission": 10, + "creation_date": "2023-01-04T23:30:48.833Z", + "kind": "INITIAL", + "status": "ACTIVE" + } + }, + { + "model": "contests.submissionreport", + "pk": 32, + "fields": { + "submission": 10, + "creation_date": "2023-01-04T23:30:49.018Z", + "kind": "NORMAL", + "status": "ACTIVE" + } + }, + { + "model": "contests.submissionreport", + "pk": 33, + "fields": { + "submission": 11, + "creation_date": "2023-01-04T23:31:22.295Z", + "kind": "INITIAL", + "status": "ACTIVE" + } + }, + { + "model": "contests.submissionreport", + "pk": 34, + "fields": { + "submission": 11, + "creation_date": "2023-01-04T23:31:22.486Z", + "kind": "NORMAL", + "status": "ACTIVE" + } + }, + { + "model": "contests.submissionreport", + "pk": 35, + "fields": { + "submission": 6, + "creation_date": "2023-01-04T23:33:05.059Z", + "kind": "HIDDEN", + "status": "INACTIVE" + } + }, + { + "model": "contests.submissionreport", + "pk": 36, + "fields": { + "submission": 12, + "creation_date": "2023-01-04T23:46:59.811Z", + "kind": "INITIAL", + "status": "ACTIVE" + } + }, + { + "model": "contests.submissionreport", + "pk": 37, + "fields": { + "submission": 12, + "creation_date": "2023-01-04T23:46:59.996Z", + "kind": "NORMAL", + "status": "ACTIVE" + } + }, + { + "model": "contests.submissionreport", + "pk": 38, + "fields": { + "submission": 13, + "creation_date": "2023-01-04T23:47:07.429Z", + "kind": "INITIAL", + "status": "ACTIVE" + } + }, + { + "model": "contests.submissionreport", + "pk": 39, + "fields": { + "submission": 13, + "creation_date": "2023-01-04T23:47:07.631Z", + "kind": "NORMAL", + "status": "ACTIVE" + } + }, + { + "model": "contests.submissionreport", + "pk": 40, + "fields": { + "submission": 14, + "creation_date": "2023-01-05T00:05:18.600Z", + "kind": "INITIAL", + "status": "ACTIVE" + } + }, + { + "model": "contests.submissionreport", + "pk": 41, + "fields": { + "submission": 14, + "creation_date": "2023-01-05T00:05:18.629Z", + "kind": "NORMAL", + "status": "ACTIVE" + } + }, + { + "model": "contests.submissionreport", + "pk": 42, + "fields": { + "submission": 14, + "creation_date": "2023-01-05T00:05:18.663Z", + "kind": "FAILURE", + "status": "INACTIVE" + } + }, + { + "model": "contests.submissionreport", + "pk": 43, + "fields": { + "submission": 15, + "creation_date": "2023-01-05T00:05:51.217Z", + "kind": "INITIAL", + "status": "ACTIVE" + } + }, + { + "model": "contests.submissionreport", + "pk": 44, + "fields": { + "submission": 15, + "creation_date": "2023-01-05T00:05:51.413Z", + "kind": "NORMAL", + "status": "ACTIVE" + } + }, + { + "model": "contests.scorereport", + "pk": 1, + "fields": { + "submission_report": 1, + "status": "CE", + "score": null, + "max_score": null, + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 2, + "fields": { + "submission_report": 2, + "status": "CE", + "score": null, + "max_score": null, + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 3, + "fields": { + "submission_report": 4, + "status": "OK", + "score": null, + "max_score": null, + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 4, + "fields": { + "submission_report": 5, + "status": "OK", + "score": "int:0000000000000000100", + "max_score": "int:0000000000000000100", + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 5, + "fields": { + "submission_report": 6, + "status": "OK", + "score": null, + "max_score": null, + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 6, + "fields": { + "submission_report": 7, + "status": "WA", + "score": "int:0000000000000000066", + "max_score": "int:0000000000000000100", + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 7, + "fields": { + "submission_report": 8, + "status": "OK", + "score": null, + "max_score": null, + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 8, + "fields": { + "submission_report": 9, + "status": "OK", + "score": "int:0000000000000000100", + "max_score": "int:0000000000000000100", + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 9, + "fields": { + "submission_report": 10, + "status": "OK", + "score": null, + "max_score": null, + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 10, + "fields": { + "submission_report": 11, + "status": "OK", + "score": "int:0000000000000000100", + "max_score": "int:0000000000000000100", + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 11, + "fields": { + "submission_report": 12, + "status": "CE", + "score": null, + "max_score": null, + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 12, + "fields": { + "submission_report": 13, + "status": "CE", + "score": null, + "max_score": null, + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 13, + "fields": { + "submission_report": 15, + "status": "OK", + "score": null, + "max_score": null, + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 14, + "fields": { + "submission_report": 16, + "status": "OK", + "score": null, + "max_score": null, + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 15, + "fields": { + "submission_report": 17, + "status": "OK", + "score": null, + "max_score": null, + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 16, + "fields": { + "submission_report": 18, + "status": "OK", + "score": "int:0000000000000000100", + "max_score": "int:0000000000000000100", + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 17, + "fields": { + "submission_report": 19, + "status": "OK", + "score": null, + "max_score": null, + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 18, + "fields": { + "submission_report": 20, + "status": "WA", + "score": "int:0000000000000000066", + "max_score": "int:0000000000000000100", + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 19, + "fields": { + "submission_report": 21, + "status": "OK", + "score": "int:0000000000000000100", + "max_score": "int:0000000000000000100", + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 20, + "fields": { + "submission_report": 22, + "status": "OK", + "score": "int:0000000000000000100", + "max_score": "int:0000000000000000100", + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 21, + "fields": { + "submission_report": 23, + "status": "OK", + "score": null, + "max_score": null, + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 22, + "fields": { + "submission_report": 24, + "status": "WA", + "score": "int:0000000000000000066", + "max_score": "int:0000000000000000100", + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 23, + "fields": { + "submission_report": 25, + "status": "OK", + "score": null, + "max_score": null, + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 24, + "fields": { + "submission_report": 26, + "status": "OK", + "score": "int:0000000000000000100", + "max_score": "int:0000000000000000100", + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 25, + "fields": { + "submission_report": 27, + "status": "OK", + "score": null, + "max_score": null, + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 26, + "fields": { + "submission_report": 28, + "status": "WA", + "score": "int:0000000000000000000", + "max_score": "int:0000000000000000100", + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 27, + "fields": { + "submission_report": 29, + "status": "OK", + "score": null, + "max_score": null, + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 28, + "fields": { + "submission_report": 30, + "status": "OK", + "score": "int:0000000000000000100", + "max_score": "int:0000000000000000100", + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 29, + "fields": { + "submission_report": 31, + "status": "OK", + "score": null, + "max_score": null, + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 30, + "fields": { + "submission_report": 32, + "status": "WA", + "score": "int:0000000000000000066", + "max_score": "int:0000000000000000100", + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 31, + "fields": { + "submission_report": 33, + "status": "OK", + "score": null, + "max_score": null, + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 32, + "fields": { + "submission_report": 34, + "status": "WA", + "score": "int:0000000000000000016", + "max_score": "int:0000000000000000100", + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 33, + "fields": { + "submission_report": 35, + "status": "WA", + "score": "int:0000000000000000066", + "max_score": "int:0000000000000000100", + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 34, + "fields": { + "submission_report": 36, + "status": "OK", + "score": null, + "max_score": null, + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 35, + "fields": { + "submission_report": 37, + "status": "OK", + "score": "int:0000000000000000100", + "max_score": "int:0000000000000000100", + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 36, + "fields": { + "submission_report": 38, + "status": "OK", + "score": null, + "max_score": null, + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 37, + "fields": { + "submission_report": 39, + "status": "OK", + "score": "int:0000000000000000100", + "max_score": "int:0000000000000000100", + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 38, + "fields": { + "submission_report": 40, + "status": "CE", + "score": null, + "max_score": null, + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 39, + "fields": { + "submission_report": 41, + "status": "CE", + "score": null, + "max_score": null, + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 40, + "fields": { + "submission_report": 43, + "status": "OK", + "score": null, + "max_score": null, + "comment": null + } + }, + { + "model": "contests.scorereport", + "pk": 41, + "fields": { + "submission_report": 44, + "status": "WA", + "score": "int:0000000000000000000", + "max_score": "int:0000000000000000100", + "comment": null + } + }, + { + "model": "contests.userresultforproblem", + "pk": 5, + "fields": { + "user": 2, + "problem_instance": 4, + "score": "float:00000000000100.00", + "status": "INI_OK", + "submission_report": null + } + }, + { + "model": "contests.userresultforproblem", + "pk": 11, + "fields": { + "user": 3, + "problem_instance": 4, + "score": "float:00000000000100.00", + "status": "INI_OK", + "submission_report": null + } + }, + { + "model": "contests.userresultforproblem", + "pk": 12, + "fields": { + "user": 3, + "problem_instance": 2, + "score": "float:00000000000050.00", + "status": "INI_OK", + "submission_report": null + } + }, + { + "model": "contests.userresultforproblem", + "pk": 15, + "fields": { + "user": 4, + "problem_instance": 2, + "score": "float:00000000000050.00", + "status": "INI_OK", + "submission_report": null + } + }, + { + "model": "contests.userresultforproblem", + "pk": 16, + "fields": { + "user": 4, + "problem_instance": 4, + "score": "float:00000000000050.00", + "status": "INI_OK", + "submission_report": null + } + }, + { + "model": "contests.userresultforproblem", + "pk": 18, + "fields": { + "user": 5, + "problem_instance": 4, + "score": "float:00000000000000.00", + "status": "INI_OK", + "submission_report": null + } + }, + { + "model": "contests.userresultforround", + "pk": 1, + "fields": { + "user": 2, + "round": 2, + "score": "float:00000000000100.00" + } + }, + { + "model": "contests.userresultforround", + "pk": 2, + "fields": { + "user": 3, + "round": 2, + "score": "float:00000000000100.00" + } + }, + { + "model": "contests.userresultforround", + "pk": 3, + "fields": { + "user": 3, + "round": 1, + "score": "float:00000000000050.00" + } + }, + { + "model": "contests.userresultforround", + "pk": 4, + "fields": { + "user": 4, + "round": 1, + "score": "float:00000000000050.00" + } + }, + { + "model": "contests.userresultforround", + "pk": 5, + "fields": { + "user": 4, + "round": 2, + "score": "float:00000000000050.00" + } + }, + { + "model": "contests.userresultforround", + "pk": 6, + "fields": { + "user": 5, + "round": 2, + "score": "float:00000000000000.00" + } + }, + { + "model": "contests.userresultforcontest", + "pk": 1, + "fields": { + "user": 2, + "contest": "contest1", + "score": "float:00000000000100.00" + } + }, + { + "model": "contests.userresultforcontest", + "pk": 2, + "fields": { + "user": 3, + "contest": "contest1", + "score": "float:00000000000150.00" + } + }, + { + "model": "contests.userresultforcontest", + "pk": 3, + "fields": { + "user": 4, + "contest": "contest1", + "score": "float:00000000000100.00" + } + }, + { + "model": "contests.userresultforcontest", + "pk": 4, + "fields": { + "user": 5, + "contest": "contest1", + "score": "float:00000000000000.00" + } + } +] diff --git a/oioioi/mp/fixtures/test_mp_users.json b/oioioi/mp/fixtures/test_mp_users.json new file mode 100644 index 000000000..9eaa4ab43 --- /dev/null +++ b/oioioi/mp/fixtures/test_mp_users.json @@ -0,0 +1,75 @@ +[ + { + "pk": 2, + "model": "auth.user", + "fields": { + "username": "test_user1", + "first_name": "Test", + "last_name": "User1", + "is_active": true, + "is_superuser": false, + "is_staff": false, + "last_login": "2012-07-31T20:27:58.768Z", + "groups": [], + "user_permissions": [], + "password": "", + "email": "test_user@example.com", + "date_joined": "2012-07-31T20:27:58.768Z" + } + }, + { + "pk": 3, + "model": "auth.user", + "fields": { + "username": "test_user2", + "first_name": "Test", + "last_name": "User2", + "is_active": true, + "is_superuser": false, + "is_staff": false, + "last_login": "2012-07-31T20:27:58.768Z", + "groups": [], + "user_permissions": [], + "password": "", + "email": "test_user2@example.com", + "date_joined": "2012-07-31T20:27:58.768Z" + } + }, + { + "pk": 4, + "model": "auth.user", + "fields": { + "username": "test_user3", + "first_name": "Test", + "last_name": "User3", + "is_active": true, + "is_superuser": false, + "is_staff": false, + "last_login": "2013-07-31T20:27:58.768Z", + "groups": [], + "user_permissions": [], + "password": "", + "email": "test_user3@example.com", + "date_joined": "2012-08-31T20:27:58.768Z" + } + }, + { + "pk": 5, + "model": "auth.user", + "fields": { + "username": "test_user4", + "first_name": "Test", + "last_name": "User4", + "is_active": true, + "is_superuser": false, + "is_staff": false, + "last_login": "2013-07-31T20:27:58.768Z", + "groups": [], + "user_permissions": [], + "password": "", + "email": "test_user4@example.com", + "date_joined": "2012-08-31T20:27:58.768Z" + } + } + ] + \ No newline at end of file diff --git a/oioioi/mp/migrations/0002_roundscoremultiplier.py b/oioioi/mp/migrations/0002_roundscoremultiplier.py new file mode 100644 index 000000000..bda00c7b9 --- /dev/null +++ b/oioioi/mp/migrations/0002_roundscoremultiplier.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.16 on 2023-01-03 00:55 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contests', '0014_contest_enable_editor'), + ('mp', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='SubmissionScoreMultiplier', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('multiplier', models.FloatField(verbose_name='multiplier')), + ('end_date', models.DateTimeField(verbose_name='end date')), + ('contest', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='contests.contest', verbose_name='contest')), + ], + ) + ] diff --git a/oioioi/mp/models.py b/oioioi/mp/models.py index 0807af6d7..139834131 100644 --- a/oioioi/mp/models.py +++ b/oioioi/mp/models.py @@ -5,6 +5,7 @@ from oioioi.base.utils.deps import check_django_app_dependencies from oioioi.participants.models import RegistrationModel +from oioioi.contests.models import Contest check_django_app_dependencies(__name__, ['oioioi.participants']) @@ -14,4 +15,18 @@ class MPRegistration(RegistrationModel): def erase_data(self): self.terms_accepted = False - self.save() \ No newline at end of file + self.save() + + +class SubmissionScoreMultiplier(models.Model): + """ 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 + ) + multiplier = models.FloatField(_("multiplier")) + end_date = models.DateTimeField(_("end date")) \ No newline at end of file diff --git a/oioioi/mp/score.py b/oioioi/mp/score.py index f27363b39..284d11b3c 100644 --- a/oioioi/mp/score.py +++ b/oioioi/mp/score.py @@ -1,69 +1,53 @@ from functools import total_ordering -from oioioi.contests.scores import IntegerScore, ScoreValue +from oioioi.contests.scores import ScoreValue @total_ordering -class PAScore(ScoreValue): - """PA style score. +class FloatScore(ScoreValue): + symbol = 'float' - It consists of a number of points scored, together with their - distribution. - When two users get the same number of points, then the number of tasks - for which they got 10pts (maximal score) is taken into consideration. - If this still does not break the tie, number of 9 point scores is - considered, then 8 point scores etc. - """ + def __init__(self, value): + assert isinstance(value, float) or isinstance(value, int) + self.value = float(value) - symbol = 'MP' + def __add__(self, other): + if not isinstance(other, FloatScore): + return FloatScore(self.value + other) + return FloatScore(self.value + other.value) - def __init__(self, points=None, distribution=None): - if points: - assert isinstance(points, IntegerScore) - self.points = points - else: - self.points = IntegerScore(0) - if distribution: - assert isinstance(distribution, ScoreDistribution) - self.distribution = distribution - else: - self.distribution = ScoreDistribution() - self.distribution.update(self.points.value) + def __mul__(self, other): + if not isinstance(other, FloatScore): + return FloatScore(self.value * other) + return FloatScore(self.value * other.value) - def __add__(self, other): - return PAScore( - self.points + other.points, self.distribution + other.distribution - ) + __rmul__ = __mul__ def __eq__(self, other): - if not isinstance(other, PAScore): - return self.points == other - return (self.points, self.distribution) == (other.points, other.distribution) + if not isinstance(other, FloatScore): + return self.value == other + return self.value == other.value def __lt__(self, other): - if not isinstance(other, PAScore): - return self.points < other - return (self.points, self.distribution) < (other.points, other.distribution) + if not isinstance(other, FloatScore): + return self.value < other + return self.value < other.value + + def __str__(self): + return str(self.value) def __unicode__(self): - return str(self.points) + return str(self.value) def __repr__(self): - return "PAScore(%r, %r)" % (self.points, self.distribution) - - def __str__(self): - return str(self.points) + return "FloatScore(%s)" % (self.value,) @classmethod def _from_repr(cls, value): - points, distribution = value.split(';') - return cls( - points=IntegerScore._from_repr(points), - distribution=ScoreDistribution._from_repr(distribution), - ) + return cls(float(value)) def _to_repr(self): - return '%s;%s' % (self.points._to_repr(), self.distribution._to_repr()) + return '%017.2f' % self.value def to_int(self): - return self.points.to_int() + return int(self.value) diff --git a/oioioi/mp/templates/mp/default_ranking.html b/oioioi/mp/templates/mp/ranking.html similarity index 100% rename from oioioi/mp/templates/mp/default_ranking.html rename to oioioi/mp/templates/mp/ranking.html diff --git a/oioioi/mp/templates/mp/registration.html b/oioioi/mp/templates/mp/registration.html new file mode 100644 index 000000000..9ac3bb768 --- /dev/null +++ b/oioioi/mp/templates/mp/registration.html @@ -0,0 +1,40 @@ +{% extends "base-with-menu.html" %} +{% load i18n %} + +{% block styles %} + {{ block.super }} + {{ form.media.css }} +{% endblock %} + +{% block scripts %} + {{ block.super }} + {{ form.media.js }} +{% endblock %} + +{% block title %}{% trans "Register to the contest" %}{{ contest_name }}{% endblock %} + +{% block main-content %} +

{% trans "Register to the contest" %} {{ contest_name }}

+ +{% if not participant %} +

+ {% trans "To enter this contest, you need to fill the following form." %} +

+{% endif %} + +
+ {% csrf_token %} + {% include "ingredients/form.html" %} +
+ + {% if can_unregister %} + + {% trans "Deregister" %} + + {% endif %} +
+
+ +{% endblock %} diff --git a/oioioi/mp/tests.py b/oioioi/mp/tests.py index 84525a6b3..48dea7083 100644 --- a/oioioi/mp/tests.py +++ b/oioioi/mp/tests.py @@ -1,100 +1,34 @@ -# import re -# from datetime import datetime # pylint: disable=E0611 - -# import urllib.parse - -# from django.contrib.admin.utils import quote -# from django.contrib.auth.models import User -# from django.core.files.base import ContentFile -# from django.test import RequestFactory -# from django.test.utils import override_settings -# from django.urls import reverse -# from django.utils.timezone import utc -# from oioioi.base.tests import TestCase, fake_time, fake_timezone_now -# from oioioi.contests.models import ( -# Contest, -# ProblemInstance, -# Submission, -# UserResultForProblem, -# ) -# from oioioi.contests.scores import IntegerScore -# from oioioi.pa.controllers import A_PLUS_B_RANKING_KEY, B_RANKING_KEY -# from oioioi.pa.models import PAProblemInstanceData, PARegistration -# from oioioi.pa.score import PAScore, ScoreDistribution -# from oioioi.participants.models import Participant, TermsAcceptedPhrase -# from oioioi.problems.models import Problem - - -# class TestPAScore(TestCase): -# def test_score_distribution(self): -# dist1 = ScoreDistribution([1] + [0] * 9) -# dist2 = ScoreDistribution([0] + [10] * 9) -# dist_null = ScoreDistribution([0] * 10) - -# self.assertLess(dist2, dist1) -# self.assertLess(dist1, dist1 + dist2) -# self.assertLess(dist2 + dist2, dist1) -# self.assertLess(dist_null, dist1) -# self.assertLess(dist_null, dist2) - -# self.assertEqual(dist_null, ScoreDistribution()) -# self.assertEqual(dist_null + dist_null, dist_null) -# self.assertEqual(dist1 + dist_null, dist1) - -# self.assertEqual( -# dist1._to_repr(), -# '00001:00000:00000:00000:00000:00000:00000:00000:00000:00000', -# ) -# self.assertEqual( -# dist2._to_repr(), -# '00000:00010:00010:00010:00010:00010:00010:00010:00010:00010', -# ) -# self.assertEqual( -# (dist1 + dist2)._to_repr(), -# '00001:00010:00010:00010:00010:00010:00010:00010:00010:00010', -# ) - -# self.assertEqual(dist1, ScoreDistribution._from_repr(dist1._to_repr())) -# self.assertEqual(dist2, ScoreDistribution._from_repr(dist2._to_repr())) - -# self.assertEqual( -# repr(dist1), -# 'ScoreDistribution(10: 1, 9: 0, 8: 0, 7: 0, 6: 0, 5: 0, 4: 0, ' -# '3: 0, 2: 0, 1: 0)', -# ) - -# def test_pa_score(self): -# score = [PAScore(IntegerScore(x)) for x in range(0, 11)] - -# self.assertLess(score[0], score[5]) -# self.assertLess(score[5], score[10]) -# self.assertLess(score[5] + score[5], score[10]) -# self.assertLess(score[5] + score[5], score[2] + score[2] + score[6]) -# self.assertLess(score[10], score[2] + score[4] + score[5]) -# self.assertLess(score[2] + score[2] + score[6], score[1] + score[3] + score[6]) - -# dist1 = ScoreDistribution([0] * 8 + [2, 4]) -# dist2 = ScoreDistribution([0] * 8 + [1, 6]) -# score1 = PAScore(IntegerScore(8), dist1) -# score2 = PAScore(IntegerScore(8), dist2) -# self.assertLess(score2, score1) - -# score3 = ( -# score[10] + score[10] + score[10] + score[4] + score[2] + score1 + score2 -# ) - -# self.assertEqual(score3, (3 * 10 + 4 + 2 + 2 * 8)) -# self.assertEqual( -# repr(score3), -# 'PAScore(IntegerScore(52), ScoreDistribution(10: 3, 9: 0, 8: ' -# '0, 7: 0, 6: 0, 5: 0, 4: 1, 3: 0, 2: 4, 1: 10))', -# ) -# self.assertEqual( -# score3._to_repr(), -# '0000000000000000052;00003:00000:' -# '00000:00000:00000:00000:00001:00000:00004:00010', -# ) -# self.assertEqual(score3, PAScore._from_repr(score3._to_repr())) +import re +from datetime import datetime # pylint: disable=E0611 + +import urllib.parse + +from django.contrib.admin.utils import quote +from django.contrib.auth.models import User +from django.core.files.base import ContentFile +from django.test import RequestFactory +from django.test.utils import override_settings +from django.urls import reverse +from django.utils.timezone import utc +from oioioi.base.tests import TestCase, fake_time, fake_timezone_now +from oioioi.contests.models import ( + Contest, + ProblemInstance, + Submission, + UserResultForProblem, +) +from oioioi.mp.score import FloatScore +from oioioi.participants.models import Participant, TermsAcceptedPhrase +from oioioi.problems.models import Problem + + +class TestFloatScore(TestCase): + def test_float_score(self): + self.assertEqual(FloatScore(100) * 0.5, FloatScore(50)) + self.assertEqual(FloatScore(50) + FloatScore(50), FloatScore(100)) + self.assertLess(FloatScore(50), FloatScore(50.5)) + self.assertLess(FloatScore(99) * 0.5, FloatScore(50)) + self.assertEqual(FloatScore(45) * 0.6, 0.6 * FloatScore(45)) # class TestPARoundTimes(TestCase): @@ -161,71 +95,56 @@ # check_round_state(date, exp) -# class TestPARanking(TestCase): -# fixtures = ['test_users', 'test_pa_contest'] - -# def _ranking_url(self, key): -# contest = Contest.objects.get() -# return reverse('ranking', kwargs={'contest_id': contest.id, 'key': key}) - -# def test_divisions(self): -# def check_visibility(good_keys, response): -# division_for_pi = {1: 'A', 2: 'A', 3: 'B', 4: 'B', 5: 'NONE'} -# for key, div in division_for_pi.items(): -# p = ProblemInstance.objects.get(pk=key) -# if div in good_keys: -# self.assertContains(response, p.short_name) -# else: -# self.assertNotContains(response, p.short_name) - -# self.assertTrue(self.client.login(username='test_user')) - -# with fake_timezone_now(datetime(2013, 1, 1, 0, 0, tzinfo=utc)): -# response = self.client.get(self._ranking_url(B_RANKING_KEY)) -# check_visibility(['B'], response) -# response = self.client.get(self._ranking_url(A_PLUS_B_RANKING_KEY)) -# check_visibility(['A', 'B'], response) -# # Round 3 is trial -# response = self.client.get(self._ranking_url(3)) -# check_visibility(['NONE'], response) - -# def test_no_zero_scores_in_ranking(self): -# self.assertTrue(self.client.login(username='test_user')) -# with fake_time(datetime(2013, 1, 1, 0, 0, tzinfo=utc)): -# response = self.client.get(self._ranking_url(3)) -# # Test User should be present in the ranking. -# self.assertTrue(re.search(b']*>Test User', response.content)) -# # Test User 2 scored 0 points for the only task in the round. -# self.assertFalse(re.search(b']*>Test User 2', response.content)) - -# def test_ranking_ordering(self): -# def check_order(response, expected): -# prev_pos = 0 -# for user in expected: -# pattern = b']*>%s' % (user,) -# pattern_match = re.search(pattern, response.content) - -# self.assertTrue(pattern_match) -# self.assertContains(response, user) - -# pos = pattern_match.start() -# self.assertGreater( -# pos, prev_pos, msg=('User %s has incorrect ' 'position' % (user,)) -# ) -# prev_pos = pos - -# self.assertTrue(self.client.login(username='test_user')) - -# with fake_timezone_now(datetime(2013, 1, 1, 0, 0, tzinfo=utc)): -# # 28 (10, 8, 6, 4), 28 (9, 9, 7, 3), 10 (10) -# response = self.client.get(self._ranking_url(A_PLUS_B_RANKING_KEY)) -# check_order(response, [b'Test User', b'Test User 2', b'Test User 3']) -# self.assertContains(response, b'28') - -# # 10 (10), 10 (7, 3), 10 (6, 4) -# response = self.client.get(self._ranking_url(B_RANKING_KEY)) -# check_order(response, [b'Test User 3', b'Test User 2', b'Test User']) -# self.assertNotContains(response, b'28') +class TestMPRanking(TestCase): + fixtures = ['test_mp_users', 'test_mp_contest'] + + def _ranking_url(self, key='c'): + contest = Contest.objects.get() + return reverse('ranking', kwargs={'contest_id': contest.id, 'key': key}) + + # def test_divisions(self): + # def check_visibility(good_keys, response): + # division_for_pi = {1: 'A', 2: 'A', 3: 'B', 4: 'B', 5: 'NONE'} + # for key, div in division_for_pi.items(): + # p = ProblemInstance.objects.get(pk=key) + # if div in good_keys: + # self.assertContains(response, p.short_name) + # else: + # self.assertNotContains(response, p.short_name) + + # self.assertTrue(self.client.login(username='test_user')) + + # with fake_timezone_now(datetime(2013, 1, 1, 0, 0, tzinfo=utc)): + # response = self.client.get(self._ranking_url()) + # check_visibility(['B'], response) + # response = self.client.get(self._ranking_url()) + # check_visibility(['A', 'B'], response) + # # Round 3 is trial + # response = self.client.get(self._ranking_url(3)) + # check_visibility(['NONE'], response) + + def test_no_zero_scores_in_ranking(self): + # self.assertTrue(self.client.login(username='test_user1')) + with fake_time(datetime(2013, 1, 1, 0, 0, tzinfo=utc)): + response = self.client.get(self._ranking_url()) + # Test User1 should be present in the ranking. + print(response.content) + self.assertTrue(re.search(b']*>Test User1', response.content)) + # Test User4 scored 0 points. + self.assertIsNone(re.search(b']*>Test User4', response.content)) + + def test_SubmissionScoreMultiplier_and_round_ordering(self): + # self.assertTrue(self.client.login(username='test_user1')) + with fake_time(datetime(2013, 1, 1, 0, 0, tzinfo=utc)): + response = self.client.get(self._ranking_url()) + # Test User1 scored 100.0 in both tasks. + self.assertTrue(re.search( + b''']*>Test User1 + ]*>200.0 + ]*>]*>100.0 + ]*>]*>100.0''', + response.content) + ) # class TestPARegistration(TestCase): diff --git a/oioioi/test_settings.py b/oioioi/test_settings.py index 95a8861e6..944121d50 100644 --- a/oioioi/test_settings.py +++ b/oioioi/test_settings.py @@ -58,7 +58,6 @@ 'oioioi.usergroups', 'oioioi.problemsharing', 'oioioi.usercontests', - 'oioioi.mp', ) + INSTALLED_APPS TEMPLATES[0]['OPTIONS']['context_processors'] += [ From f9cafbf0ac4249c3d4f87b274262b99f6578215f Mon Sep 17 00:00:00 2001 From: geoff128 Date: Thu, 5 Jan 2023 12:37:48 +0100 Subject: [PATCH 05/14] fixed update_user_result_for_problem --- oioioi/mp/controllers.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/oioioi/mp/controllers.py b/oioioi/mp/controllers.py index fc6acca67..ea52eea3a 100644 --- a/oioioi/mp/controllers.py +++ b/oioioi/mp/controllers.py @@ -123,8 +123,6 @@ def update_user_result_for_problem(self, result): contest=submission.problem_instance.contest, ) - if submission.score is None: - continue score = FloatScore(submission.score.value) rtimes = self.get_round_times(None, submission.problem_instance.round) if rtimes.is_active(submission.date): @@ -138,9 +136,6 @@ def update_user_result_for_problem(self, result): result.score = best_submission[1] result.status = best_submission[0].status - else: - result.score = None - result.status = None def can_submit(self, request, problem_instance, check_round_times=True): """Contest admin can always submit From 0b9cf45a2840b2f1e95427d2c0ded14c3f15b093 Mon Sep 17 00:00:00 2001 From: geoff128 Date: Thu, 5 Jan 2023 13:35:59 +0100 Subject: [PATCH 06/14] tests --- oioioi/mp/controllers.py | 11 +- oioioi/mp/fixtures/test_mp_contest.json | 772 ++++++------------------ oioioi/mp/tests.py | 372 +----------- 3 files changed, 223 insertions(+), 932 deletions(-) diff --git a/oioioi/mp/controllers.py b/oioioi/mp/controllers.py index ea52eea3a..61cda9fe6 100644 --- a/oioioi/mp/controllers.py +++ b/oioioi/mp/controllers.py @@ -109,6 +109,8 @@ def ranking_controller(self): return MPRankingController(self.contest) def update_user_result_for_problem(self, result): + """ + """ submissions = Submission.objects.filter( problem_instance=result.problem_instance, user=result.user, @@ -138,8 +140,11 @@ def update_user_result_for_problem(self, result): result.status = best_submission[0].status def can_submit(self, request, problem_instance, check_round_times=True): - """Contest admin can always submit - + """Contest admin can always submit. + Participant can submit if: + a. round is active + OR + b. SubmissionScoreMultiplier exists and it's end_time is ahead """ if request.user.is_anonymous: return False @@ -153,7 +158,7 @@ def can_submit(self, request, problem_instance, check_round_times=True): rtimes.is_past(request.timestamp) and SubmissionScoreMultiplier.objects.filter( contest=problem_instance.contest, - end_date__gt=request.timestamp, + end_date__gte=request.timestamp, ) ) return super(MPContestController, self).can_submit( diff --git a/oioioi/mp/fixtures/test_mp_contest.json b/oioioi/mp/fixtures/test_mp_contest.json index 9fdd8e25a..9434c0a92 100644 --- a/oioioi/mp/fixtures/test_mp_contest.json +++ b/oioioi/mp/fixtures/test_mp_contest.json @@ -5,7 +5,7 @@ "fields": { "name": "contest1", "controller_name": "oioioi.mp.controllers.MPContestController", - "creation_date": "2023-01-04T23:05:16.030Z", + "creation_date": "2023-01-05T12:00:25.933Z", "default_submissions_limit": 10, "contact_email": "", "judging_priority": 10, @@ -37,9 +37,9 @@ "fields": { "contest": "contest1", "name": "Round 1", - "start_date": "2023-01-04T23:00:00Z", - "end_date": "2023-01-04T23:30:00Z", - "results_date": "2023-01-04T23:00:01Z", + "start_date": "2023-01-05T12:00:00Z", + "end_date": "2023-01-05T12:15:00Z", + "results_date": "2023-01-05T12:00:00Z", "public_results_date": null, "is_trial": false } @@ -50,9 +50,9 @@ "fields": { "contest": "contest1", "name": "Round 2", - "start_date": "2023-01-04T23:15:00Z", - "end_date": "2023-01-04T23:45:00Z", - "results_date": "2023-01-04T23:15:00Z", + "start_date": "2023-01-05T12:10:00Z", + "end_date": "2023-01-05T12:20:00Z", + "results_date": "2023-01-05T12:10:00Z", "public_results_date": null, "is_trial": false } @@ -111,7 +111,7 @@ "fields": { "problem_instance": 2, "user": 2, - "date": "2023-01-04T23:09:47.480Z", + "date": "2023-01-05T12:01:14.237Z", "kind": "NORMAL", "score": null, "status": "CE", @@ -124,7 +124,7 @@ "fields": { "problem_instance": 2, "user": 2, - "date": "2023-01-04T23:10:15.645Z", + "date": "2023-01-05T12:01:22.788Z", "kind": "NORMAL", "score": "int:0000000000000000100", "status": "INI_OK", @@ -137,9 +137,9 @@ "fields": { "problem_instance": 2, "user": 2, - "date": "2023-01-04T23:10:22.649Z", + "date": "2023-01-05T12:01:44.381Z", "kind": "NORMAL", - "score": "int:0000000000000000066", + "score": "int:0000000000000000000", "status": "INI_OK", "comment": "" } @@ -149,10 +149,10 @@ "pk": 4, "fields": { "problem_instance": 2, - "user": 2, - "date": "2023-01-04T23:13:28.652Z", - "kind": "IGNORED", - "score": "int:0000000000000000100", + "user": 5, + "date": "2023-01-05T12:02:42.748Z", + "kind": "NORMAL", + "score": "int:0000000000000000000", "status": "INI_OK", "comment": "" } @@ -163,9 +163,9 @@ "fields": { "problem_instance": 4, "user": 2, - "date": "2023-01-04T23:15:09.703Z", + "date": "2023-01-05T12:10:20.735Z", "kind": "NORMAL", - "score": "int:0000000000000000100", + "score": "int:0000000000000000066", "status": "INI_OK", "comment": "" } @@ -174,11 +174,11 @@ "model": "contests.submission", "pk": 6, "fields": { - "problem_instance": 2, + "problem_instance": 4, "user": 2, - "date": "2023-01-04T23:16:51.055Z", + "date": "2023-01-05T12:10:48.882Z", "kind": "NORMAL", - "score": "int:0000000000000000066", + "score": "int:0000000000000000100", "status": "INI_OK", "comment": "" } @@ -189,7 +189,7 @@ "fields": { "problem_instance": 4, "user": 3, - "date": "2023-01-04T23:24:50.615Z", + "date": "2023-01-05T12:12:43.031Z", "kind": "NORMAL", "score": "int:0000000000000000100", "status": "INI_OK", @@ -202,10 +202,10 @@ "fields": { "problem_instance": 4, "user": 3, - "date": "2023-01-04T23:27:19.682Z", + "date": "2023-01-05T12:13:26.543Z", "kind": "NORMAL", - "score": "int:0000000000000000000", - "status": "INI_OK", + "score": null, + "status": "CE", "comment": "" } }, @@ -214,8 +214,8 @@ "pk": 9, "fields": { "problem_instance": 2, - "user": 3, - "date": "2023-01-04T23:30:12.410Z", + "user": 4, + "date": "2023-01-05T12:15:05.302Z", "kind": "NORMAL", "score": "int:0000000000000000100", "status": "INI_OK", @@ -228,33 +228,7 @@ "fields": { "problem_instance": 2, "user": 3, - "date": "2023-01-04T23:30:47.495Z", - "kind": "NORMAL", - "score": "int:0000000000000000066", - "status": "INI_OK", - "comment": "" - } - }, - { - "model": "contests.submission", - "pk": 11, - "fields": { - "problem_instance": 2, - "user": 2, - "date": "2023-01-04T23:31:20.972Z", - "kind": "NORMAL", - "score": "int:0000000000000000016", - "status": "INI_OK", - "comment": "" - } - }, - { - "model": "contests.submission", - "pk": 12, - "fields": { - "problem_instance": 2, - "user": 4, - "date": "2023-01-04T23:46:58.520Z", + "date": "2023-01-05T12:15:51.808Z", "kind": "NORMAL", "score": "int:0000000000000000100", "status": "INI_OK", @@ -263,24 +237,11 @@ }, { "model": "contests.submission", - "pk": 13, + "pk": 11, "fields": { "problem_instance": 4, - "user": 4, - "date": "2023-01-04T23:47:06.106Z", - "kind": "NORMAL", - "score": "int:0000000000000000100", - "status": "INI_OK", - "comment": "" - } - }, - { - "model": "contests.submission", - "pk": 14, - "fields": { - "problem_instance": 2, "user": 5, - "date": "2023-01-05T00:05:18.423Z", + "date": "2023-01-05T12:16:34.845Z", "kind": "NORMAL", "score": null, "status": "CE", @@ -289,13 +250,13 @@ }, { "model": "contests.submission", - "pk": 15, + "pk": 12, "fields": { "problem_instance": 4, - "user": 5, - "date": "2023-01-05T00:05:49.898Z", + "user": 4, + "date": "2023-01-05T12:20:05.125Z", "kind": "NORMAL", - "score": "int:0000000000000000000", + "score": "int:0000000000000000100", "status": "INI_OK", "comment": "" } @@ -305,9 +266,9 @@ "pk": 1, "fields": { "submission": 1, - "creation_date": "2023-01-04T23:09:49.129Z", + "creation_date": "2023-01-05T12:01:15.917Z", "kind": "INITIAL", - "status": "SUPERSEDED" + "status": "ACTIVE" } }, { @@ -315,427 +276,227 @@ "pk": 2, "fields": { "submission": 1, - "creation_date": "2023-01-04T23:09:49.159Z", + "creation_date": "2023-01-05T12:01:15.951Z", "kind": "NORMAL", - "status": "SUPERSEDED" + "status": "ACTIVE" } }, { "model": "contests.submissionreport", "pk": 3, - "fields": { - "submission": 1, - "creation_date": "2023-01-04T23:09:49.200Z", - "kind": "FAILURE", - "status": "SUPERSEDED" - } - }, - { - "model": "contests.submissionreport", - "pk": 4, - "fields": { - "submission": 2, - "creation_date": "2023-01-04T23:10:16.993Z", - "kind": "INITIAL", - "status": "SUPERSEDED" - } - }, - { - "model": "contests.submissionreport", - "pk": 5, "fields": { "submission": 2, - "creation_date": "2023-01-04T23:10:17.207Z", - "kind": "NORMAL", - "status": "SUPERSEDED" - } - }, - { - "model": "contests.submissionreport", - "pk": 6, - "fields": { - "submission": 3, - "creation_date": "2023-01-04T23:10:23.917Z", - "kind": "INITIAL", - "status": "SUPERSEDED" - } - }, - { - "model": "contests.submissionreport", - "pk": 7, - "fields": { - "submission": 3, - "creation_date": "2023-01-04T23:10:24.107Z", - "kind": "NORMAL", - "status": "SUPERSEDED" - } - }, - { - "model": "contests.submissionreport", - "pk": 8, - "fields": { - "submission": 4, - "creation_date": "2023-01-04T23:13:29.977Z", - "kind": "INITIAL", - "status": "SUPERSEDED" - } - }, - { - "model": "contests.submissionreport", - "pk": 9, - "fields": { - "submission": 4, - "creation_date": "2023-01-04T23:13:30.156Z", - "kind": "NORMAL", - "status": "SUPERSEDED" - } - }, - { - "model": "contests.submissionreport", - "pk": 10, - "fields": { - "submission": 5, - "creation_date": "2023-01-04T23:15:11.080Z", - "kind": "INITIAL", - "status": "SUPERSEDED" - } - }, - { - "model": "contests.submissionreport", - "pk": 11, - "fields": { - "submission": 5, - "creation_date": "2023-01-04T23:15:11.313Z", - "kind": "NORMAL", - "status": "SUPERSEDED" - } - }, - { - "model": "contests.submissionreport", - "pk": 12, - "fields": { - "submission": 1, - "creation_date": "2023-01-04T23:15:37.527Z", + "creation_date": "2023-01-05T12:01:24.143Z", "kind": "INITIAL", "status": "ACTIVE" } }, { "model": "contests.submissionreport", - "pk": 13, + "pk": 4, "fields": { - "submission": 1, - "creation_date": "2023-01-04T23:15:37.560Z", + "submission": 2, + "creation_date": "2023-01-05T12:01:24.374Z", "kind": "NORMAL", "status": "ACTIVE" } }, { "model": "contests.submissionreport", - "pk": 14, - "fields": { - "submission": 1, - "creation_date": "2023-01-04T23:15:37.602Z", - "kind": "FAILURE", - "status": "INACTIVE" - } - }, - { - "model": "contests.submissionreport", - "pk": 15, + "pk": 5, "fields": { - "submission": 2, - "creation_date": "2023-01-04T23:15:38.746Z", + "submission": 3, + "creation_date": "2023-01-05T12:01:45.684Z", "kind": "INITIAL", "status": "ACTIVE" } }, { "model": "contests.submissionreport", - "pk": 16, + "pk": 6, "fields": { "submission": 3, - "creation_date": "2023-01-04T23:15:38.832Z", - "kind": "INITIAL", + "creation_date": "2023-01-05T12:01:45.878Z", + "kind": "NORMAL", "status": "ACTIVE" } }, { "model": "contests.submissionreport", - "pk": 17, + "pk": 7, "fields": { "submission": 4, - "creation_date": "2023-01-04T23:15:39.975Z", + "creation_date": "2023-01-05T12:02:44.043Z", "kind": "INITIAL", "status": "ACTIVE" } }, { "model": "contests.submissionreport", - "pk": 18, + "pk": 8, "fields": { - "submission": 2, - "creation_date": "2023-01-04T23:15:40.103Z", + "submission": 4, + "creation_date": "2023-01-05T12:02:44.242Z", "kind": "NORMAL", "status": "ACTIVE" } }, { "model": "contests.submissionreport", - "pk": 19, + "pk": 9, "fields": { "submission": 5, - "creation_date": "2023-01-04T23:15:40.196Z", + "creation_date": "2023-01-05T12:10:22.095Z", "kind": "INITIAL", "status": "ACTIVE" } }, { "model": "contests.submissionreport", - "pk": 20, - "fields": { - "submission": 3, - "creation_date": "2023-01-04T23:15:40.254Z", - "kind": "NORMAL", - "status": "ACTIVE" - } - }, - { - "model": "contests.submissionreport", - "pk": 21, - "fields": { - "submission": 4, - "creation_date": "2023-01-04T23:15:40.343Z", - "kind": "NORMAL", - "status": "ACTIVE" - } - }, - { - "model": "contests.submissionreport", - "pk": 22, + "pk": 10, "fields": { "submission": 5, - "creation_date": "2023-01-04T23:15:40.429Z", + "creation_date": "2023-01-05T12:10:22.334Z", "kind": "NORMAL", "status": "ACTIVE" } }, { "model": "contests.submissionreport", - "pk": 23, + "pk": 11, "fields": { "submission": 6, - "creation_date": "2023-01-04T23:16:52.371Z", + "creation_date": "2023-01-05T12:10:50.200Z", "kind": "INITIAL", "status": "ACTIVE" } }, { "model": "contests.submissionreport", - "pk": 24, + "pk": 12, "fields": { "submission": 6, - "creation_date": "2023-01-04T23:16:52.556Z", + "creation_date": "2023-01-05T12:10:50.418Z", "kind": "NORMAL", "status": "ACTIVE" } }, { "model": "contests.submissionreport", - "pk": 25, + "pk": 13, "fields": { "submission": 7, - "creation_date": "2023-01-04T23:24:51.909Z", + "creation_date": "2023-01-05T12:12:44.364Z", "kind": "INITIAL", "status": "ACTIVE" } }, { "model": "contests.submissionreport", - "pk": 26, + "pk": 14, "fields": { "submission": 7, - "creation_date": "2023-01-04T23:24:52.097Z", + "creation_date": "2023-01-05T12:12:44.553Z", "kind": "NORMAL", "status": "ACTIVE" } }, { "model": "contests.submissionreport", - "pk": 27, + "pk": 15, "fields": { "submission": 8, - "creation_date": "2023-01-04T23:27:20.955Z", + "creation_date": "2023-01-05T12:13:27.129Z", "kind": "INITIAL", "status": "ACTIVE" } }, { "model": "contests.submissionreport", - "pk": 28, + "pk": 16, "fields": { "submission": 8, - "creation_date": "2023-01-04T23:27:21.143Z", + "creation_date": "2023-01-05T12:13:27.162Z", "kind": "NORMAL", "status": "ACTIVE" } }, { "model": "contests.submissionreport", - "pk": 29, + "pk": 17, "fields": { "submission": 9, - "creation_date": "2023-01-04T23:30:13.762Z", + "creation_date": "2023-01-05T12:15:06.611Z", "kind": "INITIAL", "status": "ACTIVE" } }, { "model": "contests.submissionreport", - "pk": 30, + "pk": 18, "fields": { "submission": 9, - "creation_date": "2023-01-04T23:30:13.956Z", + "creation_date": "2023-01-05T12:15:06.800Z", "kind": "NORMAL", "status": "ACTIVE" } }, { "model": "contests.submissionreport", - "pk": 31, + "pk": 19, "fields": { "submission": 10, - "creation_date": "2023-01-04T23:30:48.833Z", + "creation_date": "2023-01-05T12:15:53.122Z", "kind": "INITIAL", "status": "ACTIVE" } }, { "model": "contests.submissionreport", - "pk": 32, + "pk": 20, "fields": { "submission": 10, - "creation_date": "2023-01-04T23:30:49.018Z", + "creation_date": "2023-01-05T12:15:53.323Z", "kind": "NORMAL", "status": "ACTIVE" } }, { "model": "contests.submissionreport", - "pk": 33, + "pk": 21, "fields": { "submission": 11, - "creation_date": "2023-01-04T23:31:22.295Z", + "creation_date": "2023-01-05T12:16:35.399Z", "kind": "INITIAL", "status": "ACTIVE" } }, { "model": "contests.submissionreport", - "pk": 34, + "pk": 22, "fields": { "submission": 11, - "creation_date": "2023-01-04T23:31:22.486Z", + "creation_date": "2023-01-05T12:16:35.440Z", "kind": "NORMAL", "status": "ACTIVE" } }, { "model": "contests.submissionreport", - "pk": 35, - "fields": { - "submission": 6, - "creation_date": "2023-01-04T23:33:05.059Z", - "kind": "HIDDEN", - "status": "INACTIVE" - } - }, - { - "model": "contests.submissionreport", - "pk": 36, + "pk": 23, "fields": { "submission": 12, - "creation_date": "2023-01-04T23:46:59.811Z", + "creation_date": "2023-01-05T12:20:06.468Z", "kind": "INITIAL", "status": "ACTIVE" } }, { "model": "contests.submissionreport", - "pk": 37, + "pk": 24, "fields": { "submission": 12, - "creation_date": "2023-01-04T23:46:59.996Z", - "kind": "NORMAL", - "status": "ACTIVE" - } - }, - { - "model": "contests.submissionreport", - "pk": 38, - "fields": { - "submission": 13, - "creation_date": "2023-01-04T23:47:07.429Z", - "kind": "INITIAL", - "status": "ACTIVE" - } - }, - { - "model": "contests.submissionreport", - "pk": 39, - "fields": { - "submission": 13, - "creation_date": "2023-01-04T23:47:07.631Z", - "kind": "NORMAL", - "status": "ACTIVE" - } - }, - { - "model": "contests.submissionreport", - "pk": 40, - "fields": { - "submission": 14, - "creation_date": "2023-01-05T00:05:18.600Z", - "kind": "INITIAL", - "status": "ACTIVE" - } - }, - { - "model": "contests.submissionreport", - "pk": 41, - "fields": { - "submission": 14, - "creation_date": "2023-01-05T00:05:18.629Z", - "kind": "NORMAL", - "status": "ACTIVE" - } - }, - { - "model": "contests.submissionreport", - "pk": 42, - "fields": { - "submission": 14, - "creation_date": "2023-01-05T00:05:18.663Z", - "kind": "FAILURE", - "status": "INACTIVE" - } - }, - { - "model": "contests.submissionreport", - "pk": 43, - "fields": { - "submission": 15, - "creation_date": "2023-01-05T00:05:51.217Z", - "kind": "INITIAL", - "status": "ACTIVE" - } - }, - { - "model": "contests.submissionreport", - "pk": 44, - "fields": { - "submission": 15, - "creation_date": "2023-01-05T00:05:51.413Z", + "creation_date": "2023-01-05T12:20:06.657Z", "kind": "NORMAL", "status": "ACTIVE" } @@ -766,7 +527,7 @@ "model": "contests.scorereport", "pk": 3, "fields": { - "submission_report": 4, + "submission_report": 3, "status": "OK", "score": null, "max_score": null, @@ -777,7 +538,7 @@ "model": "contests.scorereport", "pk": 4, "fields": { - "submission_report": 5, + "submission_report": 4, "status": "OK", "score": "int:0000000000000000100", "max_score": "int:0000000000000000100", @@ -788,7 +549,7 @@ "model": "contests.scorereport", "pk": 5, "fields": { - "submission_report": 6, + "submission_report": 5, "status": "OK", "score": null, "max_score": null, @@ -799,9 +560,9 @@ "model": "contests.scorereport", "pk": 6, "fields": { - "submission_report": 7, + "submission_report": 6, "status": "WA", - "score": "int:0000000000000000066", + "score": "int:0000000000000000000", "max_score": "int:0000000000000000100", "comment": null } @@ -810,7 +571,7 @@ "model": "contests.scorereport", "pk": 7, "fields": { - "submission_report": 8, + "submission_report": 7, "status": "OK", "score": null, "max_score": null, @@ -821,9 +582,9 @@ "model": "contests.scorereport", "pk": 8, "fields": { - "submission_report": 9, - "status": "OK", - "score": "int:0000000000000000100", + "submission_report": 8, + "status": "WA", + "score": "int:0000000000000000000", "max_score": "int:0000000000000000100", "comment": null } @@ -832,7 +593,7 @@ "model": "contests.scorereport", "pk": 9, "fields": { - "submission_report": 10, + "submission_report": 9, "status": "OK", "score": null, "max_score": null, @@ -843,9 +604,9 @@ "model": "contests.scorereport", "pk": 10, "fields": { - "submission_report": 11, - "status": "OK", - "score": "int:0000000000000000100", + "submission_report": 10, + "status": "WA", + "score": "int:0000000000000000066", "max_score": "int:0000000000000000100", "comment": null } @@ -854,8 +615,8 @@ "model": "contests.scorereport", "pk": 11, "fields": { - "submission_report": 12, - "status": "CE", + "submission_report": 11, + "status": "OK", "score": null, "max_score": null, "comment": null @@ -865,10 +626,10 @@ "model": "contests.scorereport", "pk": 12, "fields": { - "submission_report": 13, - "status": "CE", - "score": null, - "max_score": null, + "submission_report": 12, + "status": "OK", + "score": "int:0000000000000000100", + "max_score": "int:0000000000000000100", "comment": null } }, @@ -876,7 +637,7 @@ "model": "contests.scorereport", "pk": 13, "fields": { - "submission_report": 15, + "submission_report": 13, "status": "OK", "score": null, "max_score": null, @@ -887,29 +648,7 @@ "model": "contests.scorereport", "pk": 14, "fields": { - "submission_report": 16, - "status": "OK", - "score": null, - "max_score": null, - "comment": null - } - }, - { - "model": "contests.scorereport", - "pk": 15, - "fields": { - "submission_report": 17, - "status": "OK", - "score": null, - "max_score": null, - "comment": null - } - }, - { - "model": "contests.scorereport", - "pk": 16, - "fields": { - "submission_report": 18, + "submission_report": 14, "status": "OK", "score": "int:0000000000000000100", "max_score": "int:0000000000000000100", @@ -918,10 +657,10 @@ }, { "model": "contests.scorereport", - "pk": 17, + "pk": 15, "fields": { - "submission_report": 19, - "status": "OK", + "submission_report": 15, + "status": "CE", "score": null, "max_score": null, "comment": null @@ -929,43 +668,10 @@ }, { "model": "contests.scorereport", - "pk": 18, - "fields": { - "submission_report": 20, - "status": "WA", - "score": "int:0000000000000000066", - "max_score": "int:0000000000000000100", - "comment": null - } - }, - { - "model": "contests.scorereport", - "pk": 19, - "fields": { - "submission_report": 21, - "status": "OK", - "score": "int:0000000000000000100", - "max_score": "int:0000000000000000100", - "comment": null - } - }, - { - "model": "contests.scorereport", - "pk": 20, - "fields": { - "submission_report": 22, - "status": "OK", - "score": "int:0000000000000000100", - "max_score": "int:0000000000000000100", - "comment": null - } - }, - { - "model": "contests.scorereport", - "pk": 21, + "pk": 16, "fields": { - "submission_report": 23, - "status": "OK", + "submission_report": 16, + "status": "CE", "score": null, "max_score": null, "comment": null @@ -973,20 +679,9 @@ }, { "model": "contests.scorereport", - "pk": 22, - "fields": { - "submission_report": 24, - "status": "WA", - "score": "int:0000000000000000066", - "max_score": "int:0000000000000000100", - "comment": null - } - }, - { - "model": "contests.scorereport", - "pk": 23, + "pk": 17, "fields": { - "submission_report": 25, + "submission_report": 17, "status": "OK", "score": null, "max_score": null, @@ -995,9 +690,9 @@ }, { "model": "contests.scorereport", - "pk": 24, + "pk": 18, "fields": { - "submission_report": 26, + "submission_report": 18, "status": "OK", "score": "int:0000000000000000100", "max_score": "int:0000000000000000100", @@ -1006,31 +701,9 @@ }, { "model": "contests.scorereport", - "pk": 25, - "fields": { - "submission_report": 27, - "status": "OK", - "score": null, - "max_score": null, - "comment": null - } - }, - { - "model": "contests.scorereport", - "pk": 26, - "fields": { - "submission_report": 28, - "status": "WA", - "score": "int:0000000000000000000", - "max_score": "int:0000000000000000100", - "comment": null - } - }, - { - "model": "contests.scorereport", - "pk": 27, + "pk": 19, "fields": { - "submission_report": 29, + "submission_report": 19, "status": "OK", "score": null, "max_score": null, @@ -1039,9 +712,9 @@ }, { "model": "contests.scorereport", - "pk": 28, + "pk": 20, "fields": { - "submission_report": 30, + "submission_report": 20, "status": "OK", "score": "int:0000000000000000100", "max_score": "int:0000000000000000100", @@ -1050,32 +723,10 @@ }, { "model": "contests.scorereport", - "pk": 29, - "fields": { - "submission_report": 31, - "status": "OK", - "score": null, - "max_score": null, - "comment": null - } - }, - { - "model": "contests.scorereport", - "pk": 30, - "fields": { - "submission_report": 32, - "status": "WA", - "score": "int:0000000000000000066", - "max_score": "int:0000000000000000100", - "comment": null - } - }, - { - "model": "contests.scorereport", - "pk": 31, + "pk": 21, "fields": { - "submission_report": 33, - "status": "OK", + "submission_report": 21, + "status": "CE", "score": null, "max_score": null, "comment": null @@ -1083,32 +734,10 @@ }, { "model": "contests.scorereport", - "pk": 32, - "fields": { - "submission_report": 34, - "status": "WA", - "score": "int:0000000000000000016", - "max_score": "int:0000000000000000100", - "comment": null - } - }, - { - "model": "contests.scorereport", - "pk": 33, - "fields": { - "submission_report": 35, - "status": "WA", - "score": "int:0000000000000000066", - "max_score": "int:0000000000000000100", - "comment": null - } - }, - { - "model": "contests.scorereport", - "pk": 34, + "pk": 22, "fields": { - "submission_report": 36, - "status": "OK", + "submission_report": 22, + "status": "CE", "score": null, "max_score": null, "comment": null @@ -1116,20 +745,9 @@ }, { "model": "contests.scorereport", - "pk": 35, - "fields": { - "submission_report": 37, - "status": "OK", - "score": "int:0000000000000000100", - "max_score": "int:0000000000000000100", - "comment": null - } - }, - { - "model": "contests.scorereport", - "pk": 36, + "pk": 23, "fields": { - "submission_report": 38, + "submission_report": 23, "status": "OK", "score": null, "max_score": null, @@ -1138,9 +756,9 @@ }, { "model": "contests.scorereport", - "pk": 37, + "pk": 24, "fields": { - "submission_report": 39, + "submission_report": 24, "status": "OK", "score": "int:0000000000000000100", "max_score": "int:0000000000000000100", @@ -1148,52 +766,30 @@ } }, { - "model": "contests.scorereport", - "pk": 38, - "fields": { - "submission_report": 40, - "status": "CE", - "score": null, - "max_score": null, - "comment": null - } - }, - { - "model": "contests.scorereport", - "pk": 39, - "fields": { - "submission_report": 41, - "status": "CE", - "score": null, - "max_score": null, - "comment": null - } - }, - { - "model": "contests.scorereport", - "pk": 40, + "model": "contests.userresultforproblem", + "pk": 1, "fields": { - "submission_report": 43, - "status": "OK", - "score": null, - "max_score": null, - "comment": null + "user": 2, + "problem_instance": 2, + "score": "float:00000000000100.00", + "status": "INI_OK", + "submission_report": null } }, { - "model": "contests.scorereport", - "pk": 41, + "model": "contests.userresultforproblem", + "pk": 2, "fields": { - "submission_report": 44, - "status": "WA", - "score": "int:0000000000000000000", - "max_score": "int:0000000000000000100", - "comment": null + "user": 5, + "problem_instance": 2, + "score": "float:00000000000000.00", + "status": "INI_OK", + "submission_report": null } }, { "model": "contests.userresultforproblem", - "pk": 5, + "pk": 3, "fields": { "user": 2, "problem_instance": 4, @@ -1204,7 +800,7 @@ }, { "model": "contests.userresultforproblem", - "pk": 11, + "pk": 4, "fields": { "user": 3, "problem_instance": 4, @@ -1215,9 +811,9 @@ }, { "model": "contests.userresultforproblem", - "pk": 12, + "pk": 5, "fields": { - "user": 3, + "user": 4, "problem_instance": 2, "score": "float:00000000000050.00", "status": "INI_OK", @@ -1226,9 +822,9 @@ }, { "model": "contests.userresultforproblem", - "pk": 15, + "pk": 6, "fields": { - "user": 4, + "user": 3, "problem_instance": 2, "score": "float:00000000000050.00", "status": "INI_OK", @@ -1237,22 +833,22 @@ }, { "model": "contests.userresultforproblem", - "pk": 16, + "pk": 7, "fields": { - "user": 4, + "user": 5, "problem_instance": 4, - "score": "float:00000000000050.00", - "status": "INI_OK", + "score": null, + "status": null, "submission_report": null } }, { "model": "contests.userresultforproblem", - "pk": 18, + "pk": 8, "fields": { - "user": 5, + "user": 4, "problem_instance": 4, - "score": "float:00000000000000.00", + "score": "float:00000000000050.00", "status": "INI_OK", "submission_report": null } @@ -1262,7 +858,7 @@ "pk": 1, "fields": { "user": 2, - "round": 2, + "round": 1, "score": "float:00000000000100.00" } }, @@ -1270,23 +866,32 @@ "model": "contests.userresultforround", "pk": 2, "fields": { - "user": 3, + "user": 5, + "round": 1, + "score": "float:00000000000000.00" + } + }, + { + "model": "contests.userresultforround", + "pk": 3, + "fields": { + "user": 2, "round": 2, "score": "float:00000000000100.00" } }, { "model": "contests.userresultforround", - "pk": 3, + "pk": 4, "fields": { "user": 3, - "round": 1, - "score": "float:00000000000050.00" + "round": 2, + "score": "float:00000000000100.00" } }, { "model": "contests.userresultforround", - "pk": 4, + "pk": 5, "fields": { "user": 4, "round": 1, @@ -1295,20 +900,29 @@ }, { "model": "contests.userresultforround", - "pk": 5, + "pk": 6, "fields": { - "user": 4, - "round": 2, + "user": 3, + "round": 1, "score": "float:00000000000050.00" } }, { "model": "contests.userresultforround", - "pk": 6, + "pk": 7, "fields": { "user": 5, "round": 2, - "score": "float:00000000000000.00" + "score": null + } + }, + { + "model": "contests.userresultforround", + "pk": 8, + "fields": { + "user": 4, + "round": 2, + "score": "float:00000000000050.00" } }, { @@ -1317,34 +931,34 @@ "fields": { "user": 2, "contest": "contest1", - "score": "float:00000000000100.00" + "score": "float:00000000000200.00" } }, { "model": "contests.userresultforcontest", "pk": 2, "fields": { - "user": 3, + "user": 5, "contest": "contest1", - "score": "float:00000000000150.00" + "score": "float:00000000000000.00" } }, { "model": "contests.userresultforcontest", "pk": 3, "fields": { - "user": 4, + "user": 3, "contest": "contest1", - "score": "float:00000000000100.00" + "score": "float:00000000000150.00" } }, { "model": "contests.userresultforcontest", "pk": 4, "fields": { - "user": 5, + "user": 4, "contest": "contest1", - "score": "float:00000000000000.00" + "score": "float:00000000000100.00" } } -] +] \ No newline at end of file diff --git a/oioioi/mp/tests.py b/oioioi/mp/tests.py index 48dea7083..2eee23075 100644 --- a/oioioi/mp/tests.py +++ b/oioioi/mp/tests.py @@ -1,25 +1,12 @@ import re from datetime import datetime # pylint: disable=E0611 -import urllib.parse - from django.contrib.admin.utils import quote -from django.contrib.auth.models import User -from django.core.files.base import ContentFile -from django.test import RequestFactory -from django.test.utils import override_settings from django.urls import reverse from django.utils.timezone import utc -from oioioi.base.tests import TestCase, fake_time, fake_timezone_now -from oioioi.contests.models import ( - Contest, - ProblemInstance, - Submission, - UserResultForProblem, -) +from oioioi.base.tests import TestCase, fake_time +from oioioi.contests.models import Contest from oioioi.mp.score import FloatScore -from oioioi.participants.models import Participant, TermsAcceptedPhrase -from oioioi.problems.models import Problem class TestFloatScore(TestCase): @@ -31,70 +18,6 @@ def test_float_score(self): self.assertEqual(FloatScore(45) * 0.6, 0.6 * FloatScore(45)) -# class TestPARoundTimes(TestCase): -# fixtures = ['test_users', 'test_pa_contest'] - -# def test_round_states(self): -# contest = Contest.objects.get() -# controller = contest.controller - -# not_last_submission = Submission.objects.get(id=6) -# # user's last submission -# not_my_submission = Submission.objects.get(id=10) -# user = User.objects.get(username='test_user') - -# def check_round_state(date, expected): -# request = RequestFactory().request() -# request.contest = contest -# request.user = user -# request.timestamp = date - -# self.assertTrue(self.client.login(username='test_user')) -# with fake_timezone_now(date): -# url = reverse( -# 'ranking', kwargs={'contest_id': 'c', 'key': A_PLUS_B_RANKING_KEY} -# ) -# response = self.client.get(url) -# if expected[0]: -# self.assertContains(response, 'taskA1') -# else: -# self.assertNotContains(response, 'taskA1') - -# self.assertEqual( -# expected[1], controller.can_see_source(request, not_my_submission) -# ) - -# self.assertEqual( -# False, controller.can_see_source(request, not_last_submission) -# ) - -# dates = [ -# datetime(2012, 6, 1, 0, 0, tzinfo=utc), -# datetime(2012, 8, 1, 0, 0, tzinfo=utc), -# datetime(2012, 10, 1, 0, 0, tzinfo=utc), -# ] - -# # 1) results date of round 1 -# # 2) public results date of round 1 -# # 3) public results date of all rounds -# # -# # ============== ============== -# # can: | see ranking | see solutions of other participants -# # ============== ============== -# # 1 -> | | | -# # | False | False | -# # 2 -> | | | -# # | True | False | -# # 3 -> | | | -# # | True | True | -# # | | | -# # ============== ============== -# expected = [[False, False], [True, False], [True, True]] - -# for date, exp in zip(dates, expected): -# check_round_state(date, exp) - - class TestMPRanking(TestCase): fixtures = ['test_mp_users', 'test_mp_contest'] @@ -102,29 +25,8 @@ def _ranking_url(self, key='c'): contest = Contest.objects.get() return reverse('ranking', kwargs={'contest_id': contest.id, 'key': key}) - # def test_divisions(self): - # def check_visibility(good_keys, response): - # division_for_pi = {1: 'A', 2: 'A', 3: 'B', 4: 'B', 5: 'NONE'} - # for key, div in division_for_pi.items(): - # p = ProblemInstance.objects.get(pk=key) - # if div in good_keys: - # self.assertContains(response, p.short_name) - # else: - # self.assertNotContains(response, p.short_name) - - # self.assertTrue(self.client.login(username='test_user')) - - # with fake_timezone_now(datetime(2013, 1, 1, 0, 0, tzinfo=utc)): - # response = self.client.get(self._ranking_url()) - # check_visibility(['B'], response) - # response = self.client.get(self._ranking_url()) - # check_visibility(['A', 'B'], response) - # # Round 3 is trial - # response = self.client.get(self._ranking_url(3)) - # check_visibility(['NONE'], response) - def test_no_zero_scores_in_ranking(self): - # self.assertTrue(self.client.login(username='test_user1')) + self.assertTrue(self.client.login(username='test_user1')) with fake_time(datetime(2013, 1, 1, 0, 0, tzinfo=utc)): response = self.client.get(self._ranking_url()) # Test User1 should be present in the ranking. @@ -134,7 +36,7 @@ def test_no_zero_scores_in_ranking(self): self.assertIsNone(re.search(b']*>Test User4', response.content)) def test_SubmissionScoreMultiplier_and_round_ordering(self): - # self.assertTrue(self.client.login(username='test_user1')) + self.assertTrue(self.client.login(username='test_user1')) with fake_time(datetime(2013, 1, 1, 0, 0, tzinfo=utc)): response = self.client.get(self._ranking_url()) # Test User1 scored 100.0 in both tasks. @@ -145,251 +47,21 @@ def test_SubmissionScoreMultiplier_and_round_ordering(self): ]*>]*>100.0''', response.content) ) - - -# class TestPARegistration(TestCase): -# fixtures = ['test_users', 'test_contest', 'test_terms_accepted_phrase'] - -# def setUp(self): -# contest = Contest.objects.get() -# contest.controller_name = 'oioioi.pa.controllers.PAContestController' -# contest.save() -# self.reg_data = { -# 'address': 'The Castle', -# 'postal_code': '31-337', -# 'city': 'Camelot', -# 't_shirt_size': 'L', -# 'job': 'AS', -# 'job_name': 'WSRH', -# 'terms_accepted': 't', -# } - -# def test_default_terms_accepted_phrase(self): -# TermsAcceptedPhrase.objects.get().delete() -# contest = Contest.objects.get() -# url = reverse('participants_register', kwargs={'contest_id': contest.id}) - -# self.assertTrue(self.client.login(username='test_user')) -# response = self.client.get(url) - -# self.assertContains( -# response, -# 'I declare that I have read the contest rules and ' -# 'the technical arrangements. I fully understand ' -# 'them and accept them unconditionally.', -# ) - -# def test_participants_registration(self): -# contest = Contest.objects.get() -# user = User.objects.get(username='test_user') -# url = reverse('participants_register', kwargs={'contest_id': contest.id}) -# self.assertTrue(self.client.login(username='test_user')) -# response = self.client.get(url) - -# self.assertContains(response, 'Postal code') -# self.assertContains(response, 'Test terms accepted') - -# user.first_name = 'Sir Lancelot' -# user.last_name = 'du Lac' -# user.save() - -# response = self.client.post(url, self.reg_data) -# self.assertEqual(302, response.status_code) - -# registration = PARegistration.objects.get(participant__user=user) -# self.assertEqual(registration.address, self.reg_data['address']) - -# def test_contest_info(self): -# contest = Contest.objects.get() -# user = User.objects.get(username='test_user') -# p = Participant(contest=contest, user=user) -# p.save() -# PARegistration(participant_id=p.id, **self.reg_data).save() -# url = reverse('contest_info', kwargs={'contest_id': contest.id}) -# data = self.client.get(url).json() -# self.assertEqual(data['users_count'], 1) - - -# class TestPAScorer(TestCase): -# t_results_ok = ( -# ( -# {'exec_time_limit': 100, 'max_score': 100}, -# {'result_code': 'OK', 'time_used': 0}, -# ), -# ( -# {'exec_time_limit': 100, 'max_score': 10}, -# {'result_code': 'OK', 'time_used': 99}, -# ), -# ( -# {'exec_time_limit': 1000, 'max_score': 0}, -# {'result_code': 'OK', 'time_used': 123}, -# ), -# ) - -# t_expected_ok = [ -# (IntegerScore(1), IntegerScore(1), 'OK'), -# (IntegerScore(1), IntegerScore(1), 'OK'), -# (IntegerScore(0), IntegerScore(0), 'OK'), -# ] - -# t_results_wrong = [ -# ( -# {'exec_time_limit': 100, 'max_score': 100}, -# {'result_code': 'WA', 'time_used': 75}, -# ), -# ( -# {'exec_time_limit': 100, 'max_score': 0}, -# {'result_code': 'RV', 'time_used': 75}, -# ), -# ] - -# t_expected_wrong = [ -# (IntegerScore(0), IntegerScore(1), 'WA'), -# (IntegerScore(0), IntegerScore(0), 'RV'), -# ] - -# def test_pa_test_scorer(self): -# results = list(map(utils.pa_test_scorer, *list(zip(*self.t_results_ok)))) -# self.assertEqual(self.t_expected_ok, results) - -# results = list(map(utils.pa_test_scorer, *list(zip(*self.t_results_wrong)))) -# self.assertEqual(self.t_expected_wrong, results) - - -# class TestPAResults(TestCase): -# fixtures = ['test_users', 'test_pa_contest'] - -# def test_pa_user_results(self): -# contest = Contest.objects.get() -# user = User.objects.get(username='test_user') -# old_results = sorted( -# [result.score for result in UserResultForProblem.objects.filter(user=user)] -# ) -# for pi in ProblemInstance.objects.all(): -# contest.controller.update_user_results(user, pi) -# new_results = sorted( -# [result.score for result in UserResultForProblem.objects.filter(user=user)] -# ) -# self.assertEqual(old_results, new_results) - - -# @override_settings( -# PROBLEM_PACKAGE_BACKENDS=('oioioi.problems.tests.DummyPackageBackend',) -# ) -# class TestPADivisions(TestCase): -# fixtures = ['test_users', 'test_contest'] - -# def test_prolem_upload(self): -# contest = Contest.objects.get() -# contest.controller_name = 'oioioi.pa.controllers.PAContestController' -# contest.save() - -# self.assertTrue(self.client.login(username='test_admin')) -# url = ( -# reverse('add_or_update_problem', kwargs={'contest_id': contest.id}) -# + '?' -# + urllib.parse.urlencode({'key': 'upload'}) -# ) - -# response = self.client.get(url) -# # "NONE" is the default division -# self.assertContains(response, '') - -# data = { -# 'package_file': ContentFile('eloziom', name='foo'), -# 'visibility': Problem.VISIBILITY_FRIENDS, -# 'division': 'A', -# } -# response = self.client.post(url, data, follow=True) -# self.assertEqual(response.status_code, 200) -# pid = PAProblemInstanceData.objects.get() -# problem = Problem.objects.get() -# self.assertEqual(pid.division, 'A') -# self.assertEqual(pid.problem_instance.problem, problem) - -# url = ( -# reverse('add_or_update_problem', kwargs={'contest_id': contest.id}) -# + '?' -# + urllib.parse.urlencode({'problem': problem.id, 'key': 'upload'}) -# ) -# response = self.client.get(url) -# self.assertContains(response, '') - - -# class TestPAContestInfo(TestCase): -# fixtures = ['test_users', 'test_pa_contest'] - -# def test_contest_info_anonymous(self): -# c = Contest.objects.get() -# url = reverse('contest_info', kwargs={'contest_id': c.id}) -# self.client.logout() -# response = self.client.get(url).json() -# self.assertEqual(response['users_count'], 2) - -# def test_cross_origin(self): -# c = Contest.objects.get() -# url = reverse('contest_info', kwargs={'contest_id': c.id}) -# response = self.client.get(url) -# self.assertEqual(response['Access-Control-Allow-Origin'], '*') - - -# class TestPASafeExecModes(TestCase): -# fixtures = ['test_pa_contests_safe_exec_mode'] - -# def test_pa_quals_controller_safe_exec_mode(self): -# c = Contest.objects.get(pk="quals") -# self.assertEqual(c.controller.get_safe_exec_mode(), 'cpu') - -# def test_pa_finals_controller_safe_exec_mode(self): -# c = Contest.objects.get(pk="finals") -# self.assertEqual(c.controller.get_safe_exec_mode(), 'cpu') - - -# class TestPAAdmin(TestCase): -# fixtures = [ -# 'test_users', -# 'test_contest', -# 'test_pa_registration', -# 'test_permissions', -# ] - -# def setUp(self): -# contest = Contest.objects.get() -# contest.controller_name = 'oioioi.pa.controllers.PAContestController' -# contest.save() - -# def test_terms_accepted_phrase_inline_admin_permissions(self): -# PARegistration.objects.all().delete() - -# # Logging as superuser. -# self.assertTrue(self.client.login(username='test_admin')) -# self.client.get('/c/c/') # 'c' becomes the current contest -# url = reverse('oioioiadmin:contests_contest_change', args=(quote('c'),)) - -# response = self.client.get(url) -# self.assertContains(response, 'Text asking participant to accept contest terms') - -# # Checks if the field is editable. -# self.assertContains(response, 'id_terms_accepted_phrase-0-text') - -# # Logging as contest admin. -# self.assertTrue(self.client.login(username='test_contest_admin')) -# self.client.get('/c/c/') # 'c' becomes the current contest -# url = reverse('oioioiadmin:contests_contest_change', args=(quote('c'),)) - -# response = self.client.get(url) -# self.assertContains(response, 'Text asking participant to accept contest terms') - -# # Checks if the field is editable. -# self.assertContains(response, 'id_terms_accepted_phrase-0-text') - -# def test_terms_accepted_phrase_inline_edit_restrictions(self): -# self.assertTrue(self.client.login(username='test_admin')) -# self.client.get('/c/c/') # 'c' becomes the current contest -# url = reverse('oioioiadmin:contests_contest_change', args=(quote('c'),)) - -# response = self.client.get(url) -# self.assertContains(response, 'Text asking participant to accept contest terms') - -# # Checks if the field is not editable. -# self.assertNotContains(response, 'id_terms_accepted_phrase-0-text') + # test_user2 scored 100.0 in both tasks, + # but sent the first one when the round was over - got 50.0. + self.assertTrue(re.search( + b''']*>test_user2 + ]*>150.0 + ]*>]*>100.0 + ]*>]*>50.0''', + response.content) + ) + # Test User3 scored 100.0 in both tasks, + # but sent both when the round was over - got 50.0 from each. + self.assertTrue(re.search( + b''']*>Test User3 + ]*>100.0 + ]*>]*>50.0 + ]*>]*>50.0''', + response.content) + ) From bd52921cc5091e6fc7e4258fee490ef1d1e1323a Mon Sep 17 00:00:00 2001 From: geoff128 Date: Thu, 5 Jan 2023 20:54:16 +0100 Subject: [PATCH 07/14] tests for SubmissionScoreMultiplier --- oioioi/mp/tests.py | 65 +++++++++------------------------------------- 1 file changed, 12 insertions(+), 53 deletions(-) diff --git a/oioioi/mp/tests.py b/oioioi/mp/tests.py index 2eee23075..f753fa491 100644 --- a/oioioi/mp/tests.py +++ b/oioioi/mp/tests.py @@ -1,11 +1,6 @@ import re -from datetime import datetime # pylint: disable=E0611 - -from django.contrib.admin.utils import quote -from django.urls import reverse -from django.utils.timezone import utc -from oioioi.base.tests import TestCase, fake_time -from oioioi.contests.models import Contest +from oioioi.base.tests import TestCase +from oioioi.contests.models import UserResultForProblem from oioioi.mp.score import FloatScore @@ -18,50 +13,14 @@ def test_float_score(self): self.assertEqual(FloatScore(45) * 0.6, 0.6 * FloatScore(45)) -class TestMPRanking(TestCase): - fixtures = ['test_mp_users', 'test_mp_contest'] - - def _ranking_url(self, key='c'): - contest = Contest.objects.get() - return reverse('ranking', kwargs={'contest_id': contest.id, 'key': key}) - - def test_no_zero_scores_in_ranking(self): - self.assertTrue(self.client.login(username='test_user1')) - with fake_time(datetime(2013, 1, 1, 0, 0, tzinfo=utc)): - response = self.client.get(self._ranking_url()) - # Test User1 should be present in the ranking. - print(response.content) - self.assertTrue(re.search(b']*>Test User1', response.content)) - # Test User4 scored 0 points. - self.assertIsNone(re.search(b']*>Test User4', response.content)) +class TestSubmissionScoreMultiplier(TestCase): + def _create_result(user, pi): + res = UserResultForProblem() + res.user = user + res.problem_instance = pi + return res - def test_SubmissionScoreMultiplier_and_round_ordering(self): - self.assertTrue(self.client.login(username='test_user1')) - with fake_time(datetime(2013, 1, 1, 0, 0, tzinfo=utc)): - response = self.client.get(self._ranking_url()) - # Test User1 scored 100.0 in both tasks. - self.assertTrue(re.search( - b''']*>Test User1 - ]*>200.0 - ]*>]*>100.0 - ]*>]*>100.0''', - response.content) - ) - # test_user2 scored 100.0 in both tasks, - # but sent the first one when the round was over - got 50.0. - self.assertTrue(re.search( - b''']*>test_user2 - ]*>150.0 - ]*>]*>100.0 - ]*>]*>50.0''', - response.content) - ) - # Test User3 scored 100.0 in both tasks, - # but sent both when the round was over - got 50.0 from each. - self.assertTrue(re.search( - b''']*>Test User3 - ]*>100.0 - ]*>]*>50.0 - ]*>]*>50.0''', - response.content) - ) + def test_results_scores(self): + for urfp in UserResultForProblem.objects.all(): + res = self._create_result(urfp.user, urfp.problem_instance) + self.assertEqual(res.score, urfp.score) From fba97a5aad947d547f81131709505752620baf17 Mon Sep 17 00:00:00 2001 From: geoff128 Date: Thu, 5 Jan 2023 22:48:53 +0100 Subject: [PATCH 08/14] more tests --- oioioi/mp/fixtures/test_mp_rankings.json | 17 +++++++ oioioi/mp/tests.py | 57 +++++++++++++++++++++++- 2 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 oioioi/mp/fixtures/test_mp_rankings.json diff --git a/oioioi/mp/fixtures/test_mp_rankings.json b/oioioi/mp/fixtures/test_mp_rankings.json new file mode 100644 index 000000000..053e31a55 --- /dev/null +++ b/oioioi/mp/fixtures/test_mp_rankings.json @@ -0,0 +1,17 @@ +[ + { + "model": "rankings.ranking", + "pk": 1, + "fields": { + "contest": "contest1", + "key": "regular#c", + "invalidation_date": "2023-01-05T12:20:06.700Z", + "last_recalculation_date": "2023-01-05T12:20:06.738Z", + "last_recalculation_duration": "00:00:00.019412", + "serialized_data": "gAN9cQAoWAQAAAByb3dzcQFdcQIofXEDKFgEAAAAdXNlcnEEY2RqYW5nby5kYi5tb2RlbHMuYmFzZQptb2RlbF91bnBpY2tsZQpxBVgEAAAAYXV0aHEGWAQAAABVc2VycQeGcQiFcQlScQp9cQsoWAYAAABfc3RhdGVxDGNkamFuZ28uZGIubW9kZWxzLmJhc2UKTW9kZWxTdGF0ZQpxDSmBcQ59cQ8oWAYAAABhZGRpbmdxEIlYAgAAAGRicRFYBwAAAGRlZmF1bHRxElgMAAAAZmllbGRzX2NhY2hlcRN9cRR1YlgCAAAAaWRxFUsCWAgAAABwYXNzd29yZHEWWFgAAABwYmtkZjJfc2hhMjU2JDI2MDAwMCQ5STVkSHVpcG81NWNKN0VZSkIyZDBRJHNsaXJadmYyelA4U1pyYUNSQ2VaQlRxMEsrV0hPc015M2hwa0Njd0pzaU09cRdYCgAAAGxhc3RfbG9naW5xGGNkYXRldGltZQpkYXRldGltZQpxGUMKB+cBBQwJAAtCFnEaY3B5dHoKX1VUQwpxGylScRyGcR1ScR5YDAAAAGlzX3N1cGVydXNlcnEfiVgIAAAAdXNlcm5hbWVxIFgKAAAAdGVzdF91c2VyMXEhWAoAAABmaXJzdF9uYW1lcSJYBAAAAFRlc3RxI1gJAAAAbGFzdF9uYW1lcSRYBQAAAFVzZXIxcSVYBQAAAGVtYWlscSZYFgAAAHRlc3RfdXNlcjFAZXhhbXBsZS5jb21xJ1gIAAAAaXNfc3RhZmZxKIlYCQAAAGlzX2FjdGl2ZXEpiFgLAAAAZGF0ZV9qb2luZWRxKmgZQwoH5wEFCzoPDVgIcStoHIZxLFJxLVgPAAAAX2RqYW5nb192ZXJzaW9ucS5YBgAAADMuMi4xNnEvdWJYBwAAAHJlc3VsdHNxMF1xMShoBVgIAAAAY29udGVzdHNxMlgUAAAAVXNlclJlc3VsdEZvclByb2JsZW1xM4ZxNIVxNVJxNn1xNyhoDGgNKYFxOH1xOShoE31xOihYEAAAAHByb2JsZW1faW5zdGFuY2VxO2gFaDJYDwAAAFByb2JsZW1JbnN0YW5jZXE8hnE9hXE+UnE/fXFAKGgMaA0pgXFBfXFCKGgTfXFDKFgHAAAAY29udGVzdHFEaAVoMlgHAAAAQ29udGVzdHFFhnFGhXFHUnFIfXFJKGgMaA0pgXFKfXFLKGgQiWgRaBJoE31xTHViaBVYCAAAAGNvbnRlc3QxcU1YBAAAAG5hbWVxTlgIAAAAY29udGVzdDFxT1gPAAAAY29udHJvbGxlcl9uYW1lcVBYKQAAAG9pb2lvaS5tcC5jb250cm9sbGVycy5NUENvbnRlc3RDb250cm9sbGVycVFYDQAAAGNyZWF0aW9uX2RhdGVxUmgZQwoH5wEFDAAZDj8gcVNoHIZxVFJxVVgZAAAAZGVmYXVsdF9zdWJtaXNzaW9uc19saW1pdHFWSwpYDQAAAGNvbnRhY3RfZW1haWxxV1gAAAAAcVhYEAAAAGp1ZGdpbmdfcHJpb3JpdHlxWUsKWA4AAABqdWRnaW5nX3dlaWdodHFaTegDWA0AAABlbmFibGVfZWRpdG9ycVuJaC5oL3ViWAUAAAByb3VuZHFcaAVoMlgFAAAAUm91bmRxXYZxXoVxX1JxYH1xYShoDGgNKYFxYn1xYyhoE31xZGgQiWgRaBJ1YmgVSwJYCgAAAGNvbnRlc3RfaWRxZVgIAAAAY29udGVzdDFxZmhOWAcAAABSb3VuZCAycWdYCgAAAHN0YXJ0X2RhdGVxaGgZQwoH5wEFDAoAAAAAcWloHIZxalJxa1gIAAAAZW5kX2RhdGVxbGgZQwoH5wEFDBQAAAAAcW1oHIZxblJxb1gMAAAAcmVzdWx0c19kYXRlcXBoGUMKB+cBBQwKAAAAAHFxaByGcXJScXNYEwAAAHB1YmxpY19yZXN1bHRzX2RhdGVxdE5YCAAAAGlzX3RyaWFscXWJaC5oL3VidWgQiWgRaBJ1YmgVSwRYCgAAAGNvbnRlc3RfaWRxdlgIAAAAY29udGVzdDFxd1gIAAAAcm91bmRfaWRxeEsCWAoAAABwcm9ibGVtX2lkcXlLAlgKAAAAc2hvcnRfbmFtZXF6WAQAAABzcXUxcXtYEQAAAHN1Ym1pc3Npb25zX2xpbWl0cXxLClgNAAAAbmVlZHNfcmVqdWRnZXF9iVgZAAAAX3ByZWZldGNoZWRfb2JqZWN0c19jYWNoZXF+fXF/aC5oL3ViWBEAAABzdWJtaXNzaW9uX3JlcG9ydHGATnVoEIloEWgSdWJoFUsDWAcAAAB1c2VyX2lkcYFLAlgTAAAAcHJvYmxlbV9pbnN0YW5jZV9pZHGCSwRYBQAA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", + "needs_recalculation": false, + "cooldown_date": "2023-01-05T12:20:11.734Z", + "recalc_in_progress": null + } + } +] \ No newline at end of file diff --git a/oioioi/mp/tests.py b/oioioi/mp/tests.py index f753fa491..af361c314 100644 --- a/oioioi/mp/tests.py +++ b/oioioi/mp/tests.py @@ -1,6 +1,11 @@ +from datetime import datetime import re -from oioioi.base.tests import TestCase -from oioioi.contests.models import UserResultForProblem + +from django.urls import reverse +from django.utils.timezone import utc + +from oioioi.base.tests import TestCase, fake_time, fake_timezone_now +from oioioi.contests.models import Contest, UserResultForProblem from oioioi.mp.score import FloatScore @@ -13,6 +18,54 @@ def test_float_score(self): self.assertEqual(FloatScore(45) * 0.6, 0.6 * FloatScore(45)) +class TestMPRanking(TestCase): + fixtures = ['test_mp_users', 'test_mp_contest', 'test_mp_rankings'] + + 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: + pattern = round_name + pattern_match = re.search(pattern, response.content) + + self.assertTrue(pattern_match) + + pos = pattern_match.start() + self.assertGreater( + pos, prev_pos, msg=('Round %s has incorrect position' % (round_name,)) + ) + prev_pos = pos + + def test_rounds_order(self): + self.assertTrue(self.client.login(username='test_user1')) + with fake_time(datetime(2023, 1, 6, 0, 0, tzinfo=utc)): + response = self.client.get(self._ranking_url()) + self._check_order(response, [b'Round 2', b'Round 1']) + + def test_columns_order(self): + self.assertTrue(self.client.login(username='test_user1')) + with fake_time(datetime(2023, 1, 6, 0, 0, tzinfo=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*' + ]) + + def test_no_zero_scores_in_ranking(self): + self.assertTrue(self.client.login(username='test_user1')) + with fake_time(datetime(2023, 1, 6, 0, 0, tzinfo=utc)): + response = self.client.get(self._ranking_url()) + # Test User should be present in the ranking. + self.assertTrue(re.search(b']*>Test User1', response.content)) + # Test User 4 scored 0 points - should not be present in the ranking. + self.assertFalse(re.search(b']*>Test User4', response.content)) + + class TestSubmissionScoreMultiplier(TestCase): def _create_result(user, pi): res = UserResultForProblem() From 2dbaa3ba13cd57f2297877255b71cf52a9ccf424 Mon Sep 17 00:00:00 2001 From: geoff128 Date: Sat, 7 Jan 2023 21:01:58 +0100 Subject: [PATCH 09/14] fixes for MP controller --- oioioi/default_settings.py | 1 - oioioi/mp/controllers.py | 4 +++- oioioi/mp/models.py | 3 ++- oioioi/mp/tests.py | 2 +- oioioi/mp/urls.py | 8 +------- oioioi/test_settings.py | 1 + 6 files changed, 8 insertions(+), 11 deletions(-) diff --git a/oioioi/default_settings.py b/oioioi/default_settings.py index a5525d21b..ef9311961 100755 --- a/oioioi/default_settings.py +++ b/oioioi/default_settings.py @@ -217,7 +217,6 @@ 'oioioi.workers', 'oioioi.quizzes', 'oioioi._locale', - 'oioioi.mp', 'djsupervisor', 'registration', diff --git a/oioioi/mp/controllers.py b/oioioi/mp/controllers.py index 61cda9fe6..292f46596 100644 --- a/oioioi/mp/controllers.py +++ b/oioioi/mp/controllers.py @@ -109,7 +109,9 @@ def ranking_controller(self): return MPRankingController(self.contest) def update_user_result_for_problem(self, result): - """ + """Submission that was sent while round was active - scored normally + Sent while round was over but SubmissionScoreMultiplier was active + - scored with given multiplier """ submissions = Submission.objects.filter( problem_instance=result.problem_instance, diff --git a/oioioi/mp/models.py b/oioioi/mp/models.py index 139834131..3e14c89d4 100644 --- a/oioioi/mp/models.py +++ b/oioioi/mp/models.py @@ -8,6 +8,7 @@ from oioioi.contests.models import Contest check_django_app_dependencies(__name__, ['oioioi.participants']) +check_django_app_dependencies(__name__, ['oioioi.contests']) class MPRegistration(RegistrationModel): @@ -29,4 +30,4 @@ class SubmissionScoreMultiplier(models.Model): Contest, verbose_name=_("contest"), on_delete=models.CASCADE ) multiplier = models.FloatField(_("multiplier")) - end_date = models.DateTimeField(_("end date")) \ No newline at end of file + end_date = models.DateTimeField(_("end date")) diff --git a/oioioi/mp/tests.py b/oioioi/mp/tests.py index af361c314..3514a507b 100644 --- a/oioioi/mp/tests.py +++ b/oioioi/mp/tests.py @@ -4,7 +4,7 @@ from django.urls import reverse from django.utils.timezone import utc -from oioioi.base.tests import TestCase, fake_time, fake_timezone_now +from oioioi.base.tests import TestCase, fake_time from oioioi.contests.models import Contest, UserResultForProblem from oioioi.mp.score import FloatScore diff --git a/oioioi/mp/urls.py b/oioioi/mp/urls.py index fe4bd1a90..36625177f 100644 --- a/oioioi/mp/urls.py +++ b/oioioi/mp/urls.py @@ -1,9 +1,3 @@ -from django.urls import re_path - -from oioioi.mp import views - app_name = 'mp' -contest_patterns = [ - #re_path(r'^contest_info/$', views.contest_info_view, name='contest_info') -] +contest_patterns = [] diff --git a/oioioi/test_settings.py b/oioioi/test_settings.py index 944121d50..1bd32c19d 100644 --- a/oioioi/test_settings.py +++ b/oioioi/test_settings.py @@ -58,6 +58,7 @@ 'oioioi.usergroups', 'oioioi.problemsharing', 'oioioi.usercontests', + 'oioioi.mp' ) + INSTALLED_APPS TEMPLATES[0]['OPTIONS']['context_processors'] += [ From 0f24a61ce2e8836c43b28786cf9d43a90b6f5675 Mon Sep 17 00:00:00 2001 From: geoff128 Date: Sun, 8 Jan 2023 11:52:55 +0100 Subject: [PATCH 10/14] fixes, some cleanup --- oioioi/mp/admin.py | 14 +++++++++----- oioioi/mp/controllers.py | 8 ++++---- oioioi/mp/templates/mp/registration.html | 2 +- oioioi/mp/views.py | 2 -- oioioi/test_settings.py | 2 +- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/oioioi/mp/admin.py b/oioioi/mp/admin.py index 9ba9cf843..e01a675be 100644 --- a/oioioi/mp/admin.py +++ b/oioioi/mp/admin.py @@ -2,7 +2,7 @@ from oioioi.base import admin from oioioi.contests.admin import ContestAdmin -from oioioi.contests.models import User +from oioioi.mp.controllers import MPContestController from oioioi.mp.forms import MPRegistrationForm from oioioi.mp.models import MPRegistration, SubmissionScoreMultiplier from oioioi.participants.admin import ParticipantAdmin @@ -36,20 +36,24 @@ def get_actions(self, request): return actions -class SubmissionScoreMultiplierInline(admin.StackedInline): +class SubmissionScoreMultiplierInline(admin.TabularInline): model = SubmissionScoreMultiplier extra = 0 category = _("Advanced") class SubmissionScoreMultiplierAdminMixin(object): - """Adds :class:`~oioioi.mp.SubmissionScoreMultiplier` fields to an - admin panel. + """Adds :class:`~oioioi.mp.models.SubmissionScoreMultiplier` to an admin panel + when contest controller is MPContestController. """ def __init__(self, *args, **kwargs): super(SubmissionScoreMultiplierAdminMixin, self).__init__(*args, **kwargs) - self.inlines = self.inlines + [SubmissionScoreMultiplierInline] + + def get_inlines(self, request, obj=None): + if hasattr(obj, 'controller') and isinstance(obj.controller, MPContestController): + self.inlines = self.inlines + [SubmissionScoreMultiplierInline] + return self.inlines ContestAdmin.mix_in(SubmissionScoreMultiplierAdminMixin) diff --git a/oioioi/mp/controllers.py b/oioioi/mp/controllers.py index 292f46596..462b44ad4 100644 --- a/oioioi/mp/controllers.py +++ b/oioioi/mp/controllers.py @@ -97,7 +97,7 @@ def can_change_terms_accepted_phrase(self, request): class MPContestController(ProgrammingContestController): - description = _("Mistrz Programowania") + description = _("Master of Programming") create_forum = False show_email_in_participants_data = True @@ -109,9 +109,9 @@ def ranking_controller(self): return MPRankingController(self.contest) def update_user_result_for_problem(self, result): - """Submission that was sent while round was active - scored normally - Sent while round was over but SubmissionScoreMultiplier was active - - scored with given multiplier + """Submissions sent during the round are scored as normal. + Submissions sent while the round was over but SubmissionScoreMultiplier was active + are scored with given multiplier. """ submissions = Submission.objects.filter( problem_instance=result.problem_instance, diff --git a/oioioi/mp/templates/mp/registration.html b/oioioi/mp/templates/mp/registration.html index 9ac3bb768..a45f5e9e7 100644 --- a/oioioi/mp/templates/mp/registration.html +++ b/oioioi/mp/templates/mp/registration.html @@ -11,7 +11,7 @@ {{ form.media.js }} {% endblock %} -{% block title %}{% trans "Register to the contest" %}{{ contest_name }}{% endblock %} +{% block title %}{% trans "Register to the contest" %} {{ contest_name }}{% endblock %} {% block main-content %}

{% trans "Register to the contest" %} {{ contest_name }}

diff --git a/oioioi/mp/views.py b/oioioi/mp/views.py index aa0a6d382..4c22a38de 100644 --- a/oioioi/mp/views.py +++ b/oioioi/mp/views.py @@ -1,7 +1,5 @@ -from django.contrib.auth.models import User from django.template.loader import render_to_string -from oioioi.base.utils import allow_cross_origin, jsonify from oioioi.contests.utils import is_contest_admin from oioioi.dashboard.registry import dashboard_headers_registry from oioioi.mp.controllers import MPRegistrationController diff --git a/oioioi/test_settings.py b/oioioi/test_settings.py index 1bd32c19d..95a8861e6 100644 --- a/oioioi/test_settings.py +++ b/oioioi/test_settings.py @@ -58,7 +58,7 @@ 'oioioi.usergroups', 'oioioi.problemsharing', 'oioioi.usercontests', - 'oioioi.mp' + 'oioioi.mp', ) + INSTALLED_APPS TEMPLATES[0]['OPTIONS']['context_processors'] += [ From 8fc84de1a6b8ee74b8a444804d218428e188db24 Mon Sep 17 00:00:00 2001 From: geoff128 Date: Sun, 8 Jan 2023 13:04:42 +0100 Subject: [PATCH 11/14] fixes --- oioioi/mp/admin.py | 14 +++++++------- oioioi/mp/controllers.py | 33 +++++++++++++++++---------------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/oioioi/mp/admin.py b/oioioi/mp/admin.py index e01a675be..3fd4fd66f 100644 --- a/oioioi/mp/admin.py +++ b/oioioi/mp/admin.py @@ -47,13 +47,13 @@ class SubmissionScoreMultiplierAdminMixin(object): when contest controller is MPContestController. """ - def __init__(self, *args, **kwargs): - super(SubmissionScoreMultiplierAdminMixin, self).__init__(*args, **kwargs) - - def get_inlines(self, request, obj=None): - if hasattr(obj, 'controller') and isinstance(obj.controller, MPContestController): - self.inlines = self.inlines + [SubmissionScoreMultiplierInline] - return self.inlines + def get_inlines(self, request, obj): + inlines = super().get_inlines(request, obj) + if hasattr(obj, 'controller') and isinstance( + obj.controller, MPContestController + ): + return inlines + [SubmissionScoreMultiplierInline] + return inlines ContestAdmin.mix_in(SubmissionScoreMultiplierAdminMixin) diff --git a/oioioi/mp/controllers.py b/oioioi/mp/controllers.py index 462b44ad4..38aafa72a 100644 --- a/oioioi/mp/controllers.py +++ b/oioioi/mp/controllers.py @@ -3,7 +3,6 @@ from django.template.response import TemplateResponse from django.utils.translation import gettext_lazy as _ - from oioioi.base.utils.query_helpers import Q_always_true from oioioi.base.utils.redirect import safe_redirect from oioioi.contests.models import Submission @@ -79,7 +78,7 @@ def registration_view(self, request): 'form': form, 'participant': participant, 'can_unregister': can_unregister, - 'contest_name': self.contest.name + 'contest_name': self.contest.name, } return TemplateResponse(request, self.registration_template, context) @@ -126,7 +125,7 @@ def update_user_result_for_problem(self, result): 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): @@ -135,7 +134,9 @@ def update_user_result_for_problem(self, result): score = score * ssm[0].multiplier else: score = None - if not best_submission or (score is not None and best_submission[1] < score): + if not best_submission or ( + score is not None and best_submission[1] < score + ): best_submission = [submission, score] result.score = best_submission[1] @@ -146,7 +147,7 @@ def can_submit(self, request, problem_instance, check_round_times=True): Participant can submit if: a. round is active OR - b. SubmissionScoreMultiplier exists and it's end_time is ahead + b. SubmissionScoreMultiplier exists and it's end_time is ahead """ if request.user.is_anonymous: return False @@ -156,16 +157,18 @@ def can_submit(self, request, problem_instance, check_round_times=True): return False rtimes = self.get_round_times(None, problem_instance.round) - round_over_contest_running = ( - rtimes.is_past(request.timestamp) and - SubmissionScoreMultiplier.objects.filter( - contest=problem_instance.contest, - end_date__gte=request.timestamp, + round_over_contest_running = rtimes.is_past( + request.timestamp + ) and SubmissionScoreMultiplier.objects.filter( + contest=problem_instance.contest, + end_date__gte=request.timestamp, + ) + return ( + super(MPContestController, self).can_submit( + request, problem_instance, check_round_times ) + or round_over_contest_running ) - return super(MPContestController, self).can_submit( - request, problem_instance, check_round_times - ) or round_over_contest_running class MPRankingController(DefaultRankingController): @@ -192,9 +195,7 @@ def _filter_pis_for_ranking(self, partial_key, queryset): def _render_ranking_page(self, key, data, page): request = self._fake_request(page) data['is_admin'] = self.is_admin_key(key) - return render_to_string( - 'mp/ranking.html', context=data, request=request - ) + return render_to_string('mp/ranking.html', context=data, request=request) def _allow_zero_score(self): return False From c103238b646ea917f0278067129ba697e611ddb5 Mon Sep 17 00:00:00 2001 From: geoff128 Date: Tue, 7 Mar 2023 21:39:47 +0100 Subject: [PATCH 12/14] Fix in MP for problems with no round --- oioioi/mp/controllers.py | 16 +++++++++++++++- oioioi/mp/fixtures/test_mp_contest.json | 22 ++++++++++++++++++++++ oioioi/mp/tests.py | 17 +++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/oioioi/mp/controllers.py b/oioioi/mp/controllers.py index 38aafa72a..40d67a320 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 @@ -139,8 +139,20 @@ def update_user_result_for_problem(self, result): ): best_submission = [submission, score] + try: + report = SubmissionReport.objects.get( + submission=best_submission[0], status='ACTIVE', kind='NORMAL' + ) + except SubmissionReport.DoesNotExist: + report = None + result.score = best_submission[1] result.status = best_submission[0].status + result.submission_report = report + else: + result.score = None + result.status = None + result.submission_report = None def can_submit(self, request, problem_instance, check_round_times=True): """Contest admin can always submit. @@ -155,6 +167,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/tests.py b/oioioi/mp/tests.py index 3514a507b..251f94962 100644 --- a/oioioi/mp/tests.py +++ b/oioioi/mp/tests.py @@ -66,6 +66,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=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() From a8d559da0e19d31617a41b609b2056044fe20fa5 Mon Sep 17 00:00:00 2001 From: geoff128 Date: Sun, 10 Dec 2023 11:47:02 +0100 Subject: [PATCH 13/14] Improve readability, some fixes. --- oioioi/mp/admin.py | 2 +- oioioi/mp/controllers.py | 68 +++++++++++++++++++++------------------- oioioi/mp/models.py | 6 ++-- oioioi/mp/tests.py | 21 +++++++------ 4 files changed, 51 insertions(+), 46 deletions(-) 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 0bf992252..b6be9a21c 100644 --- a/oioioi/mp/controllers.py +++ b/oioioi/mp/controllers.py @@ -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,40 +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] - - try: - report = SubmissionReport.objects.get( - submission=best_submission[0], status='ACTIVE', kind='NORMAL' - ) - except SubmissionReport.DoesNotExist: - report = None + 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.submission_report = report - else: - result.score = None - result.status = None - result.submission_report = None + 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. 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 b9b01ff08..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')) @@ -72,7 +75,7 @@ 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=utc)): + 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) From 1d70a06cf3d4602800f14611b84ca6389000f269 Mon Sep 17 00:00:00 2001 From: geoff128 Date: Sun, 10 Dec 2023 11:48:00 +0100 Subject: [PATCH 14/14] Fix colors in ranking. --- oioioi/base/templatetags/simple_filters.py | 3 +++ 1 file changed, 3 insertions(+) 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.