diff --git a/oioioi/acm/controllers.py b/oioioi/acm/controllers.py
index 851a8b4a5..4ff09629c 100644
--- a/oioioi/acm/controllers.py
+++ b/oioioi/acm/controllers.py
@@ -145,6 +145,12 @@ def _fill_user_result_for_problem(self, result, pi_submissions):
result.status = None
return None
+ def get_last_scored_submission(self, user, problem_instance, before=None, include_current=False):
+ """This function is not implemented for ACM contests because score difference
+ isn't shown for ACM contests.
+ """
+ raise NotImplementedError
+
def update_user_result_for_problem(self, result):
submissions = (
Submission.objects.filter(
@@ -201,6 +207,9 @@ def can_see_round(self, request_or_context, round, no_admin=False):
def get_default_safe_exec_mode(self):
return 'cpu'
+ def display_score_change(self):
+ return False
+
class ACMOpenContestController(ACMContestController):
description = _("ACM style contest (open)")
diff --git a/oioioi/contests/admin.py b/oioioi/contests/admin.py
index 917567b2d..13954948e 100644
--- a/oioioi/contests/admin.py
+++ b/oioioi/contests/admin.py
@@ -712,6 +712,8 @@ def get_list_display(self, request):
]
if request.contest:
list_display.remove('contest_display')
+ if request.contest.controller.display_score_change():
+ list_display.append('score_diff_display')
return list_display
def get_list_display_links(self, request, list_display):
@@ -848,6 +850,34 @@ def score_display(self, instance):
score_display.short_description = _("Score")
score_display.admin_order_field = 'score_with_nulls_smallest'
+ def score_diff_display(self, instance):
+ contest_controller = instance.problem_instance.contest.controller
+ pi_controller = instance.problem_instance.controller
+ if not contest_controller.display_score_change() or instance.kind != 'NORMAL':
+ return format_html('-')
+
+ try:
+ previous_submission = pi_controller.get_last_scored_submission(
+ instance.user,
+ instance.problem_instance,
+ before=instance.date,
+ )
+ except Submission.DoesNotExist:
+ previous_submission = None
+ try:
+ curr_submission = pi_controller.get_last_scored_submission(
+ instance.user,
+ instance.problem_instance,
+ before=instance.date,
+ include_current=True,
+ )
+ except Submission.DoesNotExist:
+ curr_submission = None
+ return contest_controller.render_score_change(previous_submission, curr_submission)
+
+ score_diff_display.short_description = _("Score change")
+ score_diff_display.admin_order_field = 'score'
+
def contest_display(self, instance):
return instance.problem_instance.contest
diff --git a/oioioi/contests/controllers.py b/oioioi/contests/controllers.py
index 48c1bc6b3..da94129a8 100644
--- a/oioioi/contests/controllers.py
+++ b/oioioi/contests/controllers.py
@@ -9,6 +9,7 @@
from django.db.models import Subquery
from django.template.loader import render_to_string
from django.urls import reverse
+from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext_noop
@@ -707,6 +708,10 @@ def update_submission_score(self, submission):
problem = submission.problem_instance.problem
problem.controller.update_submission_score(submission)
+ def get_last_scored_submission(self, user, problem_instance, before=None, include_current=False):
+ problem = problem_instance.problem
+ return problem.controller.get_last_scored_submission(user, problem_instance, before, include_current)
+
def update_user_result_for_problem(self, result):
problem = result.problem_instance.problem
problem.controller.update_user_result_for_problem(result)
@@ -974,6 +979,36 @@ def _is_partial_score(self, test_report):
def show_default_fields(self, problem_instance):
return problem_instance.problem.controller.show_default_fields(problem_instance)
+ def display_score_change(self):
+ """
+ Whether to display score change for a submission in submissions admin.
+ """
+ return True
+
+ def _calculate_score_change(self, before, after):
+ """
+ Calculate score difference between two scores.
+ """
+ if before is None or after is None:
+ return after
+ cls = type(before)
+ return cls(after.value - before.value)
+
+ def render_score_change(self, previous_submission, current_submission):
+ """
+ Calculates and renders score change between two submissions.
+ """
+ prev_score = previous_submission.score if previous_submission else None
+ curr_score = current_submission.score if current_submission else None
+ diff = self._calculate_score_change(prev_score, curr_score)
+ if diff is None:
+ return format_html('-')
+ if diff.value == 0:
+ return format_html('0')
+ if diff.value > 0:
+ return format_html('+{}', diff.value)
+ return format_html('{}', diff.value)
+
class PastRoundsHiddenContestControllerMixin(object):
"""ContestController mixin that hides past rounds
diff --git a/oioioi/contests/tests/tests.py b/oioioi/contests/tests/tests.py
index 4998ee1c0..b74998588 100755
--- a/oioioi/contests/tests/tests.py
+++ b/oioioi/contests/tests/tests.py
@@ -68,7 +68,7 @@
ProblemStatement,
)
from oioioi.programs.controllers import ProgrammingContestController
-from oioioi.programs.models import ModelProgramSubmission, Test
+from oioioi.programs.models import ModelProgramSubmission, Test, ProgramSubmission
from oioioi.programs.tests import SubmitFileMixin
from oioioi.simpleui.views import (
contest_dashboard_redirect as simpleui_contest_dashboard,
@@ -4200,3 +4200,65 @@ def test_score_badge(self):
self.assertIn('badge-warning', self._get_badge_for_problem(response.content, 'zad2'))
self.assertIn('badge-danger', self._get_badge_for_problem(response.content, 'zad3'))
+
+class TestScoreDiffDisplay(TestCase):
+ fixtures = [
+ 'test_users',
+ 'test_contest',
+ 'test_full_package',
+ 'test_problem_instance',
+ ]
+
+ contest_controller = 'oioioi.contests.controllers.ContestController'
+
+ def _change_contest_controller(self, controller_name):
+ contest = Contest.objects.get()
+ contest.controller_name = controller_name
+ contest.save()
+
+ def _get_score_diff(self, submission):
+ contest = Contest.objects.get()
+ url = reverse('oioioiadmin:contests_submission_changelist', kwargs={'contest_id': contest.id})
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+
+ soup = bs4.BeautifulSoup(response.content, 'html.parser')
+ submissions_table = soup.find('table', {'id': 'result_list'})
+ tbody = submissions_table.find('tbody')
+ for tr in tbody.find_all('tr'):
+ s_id = tr.find('th', {'class': 'field-id'}).text
+ if s_id == str(submission.id):
+ return tr.find('td', {'class': 'field-score_diff_display'}).text
+
+ def _create_submission(self, score, expected_diff):
+ problem_instance = ProblemInstance.objects.get(pk=1)
+ user = User.objects.get(username='test_admin')
+ ps = ProgramSubmission.objects.create(
+ source_file=ContentFile(b'int main() {}', name='main.cpp'),
+ problem_instance=problem_instance,
+ user=user,
+ score=score,
+ status='OK',
+ )
+ ps.save()
+ self.assertEqual(expected_diff, self._get_score_diff(ps))
+ return ps
+
+ def setUp(self):
+ self.client.force_login(User.objects.get(username='test_admin'))
+ self._change_contest_controller(self.contest_controller)
+ Submission.objects.all().delete()
+
+
+class TestScoreDiffSimpleContest(TestScoreDiffDisplay):
+ contest_controller = 'oioioi.programs.controllers.ProgrammingContestController'
+
+ def test(self):
+ self._create_submission(IntegerScore(25), '+25')
+ s1 = self._create_submission(IntegerScore(50), '+25')
+ s2 = self._create_submission(IntegerScore(100), '+50')
+ self._create_submission(IntegerScore(0), '-100')
+ s1.kind = 'IGNORED'
+ s1.save()
+ self.assertEqual(self._get_score_diff(s2), '+75')
+ self.assertEqual(self._get_score_diff(s1), '-')
diff --git a/oioioi/mp/controllers.py b/oioioi/mp/controllers.py
index b36493291..156aba41d 100644
--- a/oioioi/mp/controllers.py
+++ b/oioioi/mp/controllers.py
@@ -122,43 +122,65 @@ def _get_score_for_submission(self, submission, ssm):
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
- are scored with given multiplier.
- """
+ def get_last_scored_submission(self, user, problem_instance, before=None, include_current=False, ssm=None):
submissions = Submission.objects.filter(
- problem_instance=result.problem_instance,
- user=result.user,
+ problem_instance=problem_instance,
+ user=user,
kind='NORMAL',
score__isnull=False,
)
+ if before:
+ if include_current:
+ submissions = submissions.filter(date__lte=before)
+ else:
+ submissions = submissions.filter(date__lt=before)
best_submission = None
best_submission_score = None
- try:
- ssm = SubmissionScoreMultiplier.objects.get(
- contest=result.problem_instance.contest
- )
- except SubmissionScoreMultiplier.DoesNotExist:
- ssm = None
+ if ssm is None:
+ try:
+ ssm = SubmissionScoreMultiplier.objects.get(
+ contest=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
+ return best_submission
+ 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
+ are scored with given multiplier.
+ """
try:
- report = SubmissionReport.objects.get(
- submission=best_submission, status='ACTIVE', kind='NORMAL'
+ ssm = SubmissionScoreMultiplier.objects.get(
+ contest=result.problem_instance.contest
)
- except SubmissionReport.DoesNotExist:
- report = None
+ except SubmissionScoreMultiplier.DoesNotExist:
+ ssm = None
+ best_submission = self.get_last_scored_submission(result.user, result.problem_instance, ssm=ssm)
+ if best_submission:
+ best_submission_score = self._get_score_for_submission(best_submission, ssm)
- result.score = best_submission_score
- result.status = best_submission.status if best_submission else None
- result.submission_report = report
+ try:
+ report = SubmissionReport.objects.get(
+ submission=best_submission, status='ACTIVE', kind='NORMAL'
+ )
+ except SubmissionReport.DoesNotExist:
+ report = None
+
+ result.score = best_submission_score
+ result.status = best_submission.status if best_submission else None
+ 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.
diff --git a/oioioi/mp/tests.py b/oioioi/mp/tests.py
index 0ac36489c..64cde38d8 100644
--- a/oioioi/mp/tests.py
+++ b/oioioi/mp/tests.py
@@ -5,6 +5,7 @@
from oioioi.base.tests import TestCase, fake_time
from oioioi.contests.models import Contest, UserResultForProblem
+from oioioi.contests.tests.tests import TestScoreDiffDisplay
from oioioi.mp.score import FloatScore
@@ -96,3 +97,17 @@ 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)
+
+
+class TestScoreDiffMPContest(TestScoreDiffDisplay):
+ contest_controller = 'oioioi.mp.controllers.MPContestController'
+
+ def test(self):
+ self._create_submission(FloatScore(25.5), '+25.5')
+ s1 = self._create_submission(FloatScore(50), '+24.5')
+ s2 = self._create_submission(FloatScore(100), '+50.0')
+ self._create_submission(FloatScore(0), '0')
+ s1.kind = 'IGNORED'
+ s1.save()
+ self.assertEqual(self._get_score_diff(s1), '-')
+ self.assertEqual(self._get_score_diff(s2), '+74.5')
diff --git a/oioioi/oi/controllers.py b/oioioi/oi/controllers.py
index 0ce27ea3b..b289bdc16 100644
--- a/oioioi/oi/controllers.py
+++ b/oioioi/oi/controllers.py
@@ -174,16 +174,24 @@ def can_see_stats(self, request):
def should_confirm_submission_receipt(self, request, submission):
return submission.kind == 'NORMAL' and request.user == submission.user
+ def get_last_scored_submission(self, user, problem_instance, before=None, include_current=False):
+ submissions = (
+ Submission.objects.filter(problem_instance=problem_instance)
+ .filter(user=user)
+ .filter(score__isnull=False)
+ .exclude(status='CE')
+ .filter(kind='NORMAL')
+ )
+ if before:
+ if include_current:
+ submissions = submissions.filter(date__lte=before)
+ else:
+ submissions = submissions.filter(date__lt=before)
+ return submissions.latest()
+
def update_user_result_for_problem(self, result):
try:
- latest_submission = (
- Submission.objects.filter(problem_instance=result.problem_instance)
- .filter(user=result.user)
- .filter(score__isnull=False)
- .exclude(status='CE')
- .filter(kind='NORMAL')
- .latest()
- )
+ latest_submission = self.get_last_scored_submission(result.user, result.problem_instance)
try:
report = SubmissionReport.objects.get(
submission=latest_submission, status='ACTIVE', kind='NORMAL'
@@ -241,18 +249,27 @@ class OIFinalOnsiteContestController(OIOnsiteContestController):
def can_see_submission_score(self, request, submission):
return True
- def update_user_result_for_problem(self, result):
+ def get_last_scored_submission(self, user, problem_instance, before=None, include_current=False):
submissions = (
- Submission.objects.filter(problem_instance=result.problem_instance)
- .filter(user=result.user)
+ Submission.objects.filter(problem_instance=problem_instance)
+ .filter(user=user)
.filter(score__isnull=False)
.exclude(status='CE')
.filter(kind='NORMAL')
)
-
+ if before:
+ if include_current:
+ submissions = submissions.filter(date__lte=before)
+ else:
+ submissions = submissions.filter(date__lt=before)
if submissions:
- max_submission = submissions.order_by('-score')[0]
+ return submissions.order_by('-score')[0]
+ return None
+ def update_user_result_for_problem(self, result):
+ max_submission = self.get_last_scored_submission(result.user, result.problem_instance)
+
+ if max_submission:
try:
report = SubmissionReport.objects.get(
submission=max_submission, status='ACTIVE', kind='NORMAL'
@@ -281,23 +298,30 @@ def reveal_score(self, request, submission):
super(BOIOnsiteContestController, self).reveal_score(request, submission)
self.update_user_results(submission.user, submission.problem_instance)
- def update_user_result_for_problem(self, result):
- try:
- submissions = (
- Submission.objects.filter(problem_instance=result.problem_instance)
- .filter(user=result.user)
+ def get_last_scored_submission(self, user, problem_instance, before=None, include_current=False):
+ submissions = (
+ Submission.objects.filter(problem_instance=problem_instance)
+ .filter(user=user)
.filter(score__isnull=False)
.exclude(status='CE')
.filter(kind='NORMAL')
)
+ if before:
+ if include_current:
+ submissions = submissions.filter(date__lte=before)
+ else:
+ submissions = submissions.filter(date__lt=before)
+ chosen_submission = submissions.latest()
+ revealed = submissions.filter(revealed__isnull=False)
+ if revealed:
+ max_revealed = revealed.order_by('-score')[0]
+ if max_revealed.score > chosen_submission.score:
+ chosen_submission = max_revealed
+ return chosen_submission
- chosen_submission = submissions.latest()
-
- revealed = submissions.filter(revealed__isnull=False)
- if revealed:
- max_revealed = revealed.order_by('-score')[0]
- if max_revealed.score > chosen_submission.score:
- chosen_submission = max_revealed
+ def update_user_result_for_problem(self, result):
+ try:
+ chosen_submission = self.get_last_scored_submission(result.user, result.problem_instance)
try:
report = SubmissionReport.objects.get(
diff --git a/oioioi/oi/tests.py b/oioioi/oi/tests.py
index c62a73b62..740be8e0e 100644
--- a/oioioi/oi/tests.py
+++ b/oioioi/oi/tests.py
@@ -12,6 +12,8 @@
from oioioi.contests.current_contest import ContestMode
from oioioi.contests.handlers import update_user_results
from oioioi.contests.models import Contest, ProblemInstance, Round
+from oioioi.contests.scores import IntegerScore
+from oioioi.contests.tests.tests import TestScoreDiffDisplay
from oioioi.evalmgr.tasks import create_environ
from oioioi.oi.management.commands import import_schools
from oioioi.oi.models import OIRegistration, School
@@ -616,3 +618,31 @@ def test_user_info_page(self):
self.assertContains(response, reg_data[k])
else:
self.assertNotContains(response, reg_data[k], status_code=403)
+
+
+class TestScoreDiffOIContest(TestScoreDiffDisplay):
+ contest_controller = 'oioioi.oi.controllers.OIContestController'
+
+ def test(self):
+ self._create_submission(IntegerScore(25), '+25')
+ s1 = self._create_submission(IntegerScore(50), '+25')
+ s2 = self._create_submission(IntegerScore(100), '+50')
+ self._create_submission(IntegerScore(0), '-100')
+ s1.kind = 'IGNORED'
+ s1.save()
+ self.assertEqual(self._get_score_diff(s2), '+75')
+ self.assertEqual(self._get_score_diff(s1), '-')
+
+
+class TestScoreDiffOIFinalsContest(TestScoreDiffDisplay):
+ contest_controller = 'oioioi.oi.controllers.OIFinalOnsiteContestController'
+
+ def test(self):
+ self._create_submission(IntegerScore(25), '+25')
+ s1 = self._create_submission(IntegerScore(50), '+25')
+ s2 = self._create_submission(IntegerScore(100), '+50')
+ self._create_submission(IntegerScore(0), '0')
+ s1.kind = 'IGNORED'
+ s1.save()
+ self.assertEqual(self._get_score_diff(s2), '+75')
+ self.assertEqual(self._get_score_diff(s1), '-')
diff --git a/oioioi/problems/controllers.py b/oioioi/problems/controllers.py
index 382d5d895..d82a731e0 100644
--- a/oioioi/problems/controllers.py
+++ b/oioioi/problems/controllers.py
@@ -238,6 +238,23 @@ def mixins_for_admin(self):
"""
return ()
+ def get_last_scored_submission(self, user, problem_instance, before=None, include_current=False):
+ """Helper function for :meth:`update_user_result_for_problem`
+ to get the last scored submission.
+ """
+ submissions = (
+ Submission.objects.filter(problem_instance=problem_instance)
+ .filter(user=user)
+ .filter(score__isnull=False)
+ .filter(kind='NORMAL')
+ )
+ if before:
+ if include_current:
+ submissions = submissions.filter(date__lte=before)
+ else:
+ submissions = submissions.filter(date__lt=before)
+ return submissions.latest()
+
def update_user_result_for_problem(self, result):
"""Updates a :class:`~oioioi.contests.models.UserResultForProblem`.
@@ -248,13 +265,7 @@ def update_user_result_for_problem(self, result):
Saving the ``result`` is a responsibility of the caller.
"""
try:
- latest_submission = (
- Submission.objects.filter(problem_instance=result.problem_instance)
- .filter(user=result.user)
- .filter(score__isnull=False)
- .filter(kind='NORMAL')
- .latest()
- )
+ latest_submission = self.get_last_scored_submission(result.user, result.problem_instance)
try:
report = SubmissionReport.objects.get(
submission=latest_submission, status='ACTIVE', kind='NORMAL'