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'