diff --git a/oioioi/contests/controllers.py b/oioioi/contests/controllers.py index 20774b23c..f16f93d7a 100644 --- a/oioioi/contests/controllers.py +++ b/oioioi/contests/controllers.py @@ -58,6 +58,34 @@ def export_entries(registry, values): return result +STATUS_CLASSES = { + "OK100": "badge-success", + "OK": "badge-success", + "INI_OK": "badge-success", + "TESTRUN_OK": "badge-success", + "OK75": "badge-warning", + "OK50": "badge-warning", + "OK25": "badge-danger", + "OK0": "badge-danger", + "ERR": "badge-danger", + "WA": "badge-danger", + "TLE": "badge-danger", + "RE": "badge-danger", + "CE": "badge-danger", + "MLE": "badge-danger", + "OLE": "badge-danger", + "SE": "badge-danger", + "RV": "badge-danger", + "INI_ERR": "badge-danger", + "MSE": "badge-danger", + "MCE": "badge-danger", +} + + +def get_badge_class(display_type): + return STATUS_CLASSES.get(display_type, "badge-secondary") + + def submission_template_context(request, submission): pi = submission.problem_instance controller = pi.controller @@ -108,12 +136,15 @@ def submission_template_context(request, submission): else: display_type = submission.status + badge_class = get_badge_class(display_type) + return { "submission": submission, "can_see_status": can_see_status, "can_see_score": can_see_score, "can_see_comment": can_see_comment, "display_type": display_type, + "badge_class": badge_class, "message": message, "link": link, "valid_kinds_for_submission": valid_kinds_for_submission, diff --git a/oioioi/contests/templates/contests/problems_list.html b/oioioi/contests/templates/contests/problems_list.html index e81e7e343..95e6998a6 100644 --- a/oioioi/contests/templates/contests/problems_list.html +++ b/oioioi/contests/templates/contests/problems_list.html @@ -2,6 +2,7 @@ {% load i18n %} {% load pagination_tags %} {% load format_data_range %} +{% load humanize %} {% block styles %} {{ block.super }} @@ -80,7 +81,7 @@

{% trans "Problems" %}

- {% for pi, statement_visible, round_time, problem_limits, result, submissions_left, submissions_limit, can_submit in problem_instances %} + {% for pi, statement_visible, round_time, problem_limits, result, submissions_left, submissions_limit, can_submit, last_submission in problem_instances %} {% if show_rounds %} {% ifchanged pi.round %} @@ -100,6 +101,13 @@

{% trans "Problems" %}

{% else %} {{ pi.problem.name }} {% endif %} + {% if show_status %} + {% if last_submission and last_submission.can_see_status %} + + {{ last_submission.message }} + + {% endif %} + {% endif %} {% if show_problems_limits %} diff --git a/oioioi/contests/tests/tests.py b/oioioi/contests/tests/tests.py index 9c15e4e7c..5c4a4d14e 100755 --- a/oioioi/contests/tests/tests.py +++ b/oioioi/contests/tests/tests.py @@ -3858,6 +3858,44 @@ def see_link_for_submission_on_problem_list(self, username, should_see): self.assertNotContains(response, expected_hyperlink) +class TestStatusOnProblemsList(TestCase): + fixtures = [ + "test_users", + "test_contest", + "test_full_package", + "test_problem_instance", + "test_submission", + ] + + def test_status_hidden_for_anonymous(self): + see_status_on_problems_list(self, None, False) + + def test_status_visible_for_user(self): + see_status_on_problems_list(self, "test_user", True, expected_status="OK") + + def test_status_hidden_for_admin(self): + see_status_on_problems_list(self, "test_admin", False) + + +def see_status_on_problems_list(self, username, should_see, expected_status=None): + if username is None: + self.client.logout() + else: + self.assertTrue(self.client.login(username=username)) + + contest = Contest.objects.get(pk="c") + problems_url = reverse("problems_list", kwargs={"contest_id": contest.id}) + + response = self.client.get(problems_url, follow=True) + # badge presence + if should_see: + if expected_status: + self.assertContains(response, expected_status) + else: + # No status header for anonymous users + self.assertNotContains(response, 'Status') + + class TestSubmissionsLinksOnListView(TestCase): fixtures = [ "test_users", diff --git a/oioioi/contests/views.py b/oioioi/contests/views.py index eca82f7c2..2785ed6c8 100755 --- a/oioioi/contests/views.py +++ b/oioioi/contests/views.py @@ -154,6 +154,39 @@ def problems_list_view(request): # 6) submissions_limit # 7) can_submit # Sorted by (start_date, end_date, round name, problem name) + # Preload user-related data to avoid N+1 queries + pi_ids = [pi.id for pi in problem_instances] + results_map = {} + last_submission_map = {} + if request.user.is_authenticated: + # Bulk fetch UserResultForProblem objects. We only keep those for which + # the user can see the submission score. + user_results_qs = ( + UserResultForProblem.objects.filter(user__id=request.user.id, problem_instance_id__in=pi_ids) + .select_related("submission_report__submission") + ) + for r in user_results_qs: + # Some controllers may hide score even if UserResultForProblem exists + if r and r.submission_report and controller.can_see_submission_score(request, r.submission_report.submission): + results_map[r.problem_instance_id] = r + + # Bulk fetch user's submissions for the problem instances and build a map + # of latest submission per problem instance. Submissions are ordered by + # date descending, so the first occurrence for a given problem_instance + # is the latest one. + submissions_qs = ( + Submission.objects.filter( + user__id=request.user.id, + problem_instance_id__in=pi_ids, + kind="NORMAL" # ignore ignored submissions + ) + .order_by("-date") + ) + for s in submissions_qs: + pid = s.problem_instance_id + if pid not in last_submission_map: + last_submission_map[pid] = s + problems_statements = sorted( [ ( @@ -161,21 +194,11 @@ def problems_list_view(request): controller.can_see_statement(request, pi), controller.get_round_times(request, pi.round), problems_limits.get(pi.pk, None), - # Because this view can be accessed by an anynomous user we can't - # use `user=request.user` (it would cause TypeError). Surprisingly - # using request.user.id is ok since for AnynomousUser id is set - # to None. - next( - ( - r - for r in UserResultForProblem.objects.filter(user__id=request.user.id, problem_instance=pi) - if r and r.submission_report and controller.can_see_submission_score(request, r.submission_report.submission) - ), - None, - ), + results_map.get(pi.id), pi.controller.get_submissions_left(request, pi), pi.controller.get_submissions_limit(request, pi), controller.can_submit(request, pi) and not is_contest_archived(request), + submission_template_context(request, last_submission_map[pi.id]) if pi.id in last_submission_map else None, ) for pi in problem_instances ], @@ -185,6 +208,7 @@ def problems_list_view(request): show_submissions_limit = any(p[6] for p in problems_statements) show_submit_button = any(p[7] for p in problems_statements) show_rounds = len(frozenset(pi.round_id for pi in problem_instances)) > 1 + show_status = request.user.is_authenticated # Always show status for authenticated users table_columns = 3 + int(show_problems_limits) + int(show_submissions_limit) + int(show_submit_button) return TemplateResponse( @@ -196,6 +220,7 @@ def problems_list_view(request): "show_rounds": show_rounds, "show_scores": request.user.is_authenticated, "show_submissions_limit": show_submissions_limit, + "show_status": show_status, "show_submit_button": show_submit_button, "table_columns": table_columns, "problems_on_page": getattr(settings, "PROBLEMS_ON_PAGE", 100),