From b029b013f4b7218785a2164349d828b2b629cafc Mon Sep 17 00:00:00 2001 From: int-y1 Date: Sun, 2 Mar 2025 04:01:12 -0500 Subject: [PATCH] Add field to quarantine a submission --- dmoj/urls.py | 4 +++ judge/admin/submission.py | 8 +++-- .../0150_submission_is_quarantined.py | 18 ++++++++++ judge/models/profile.py | 5 +-- judge/models/submission.py | 3 +- judge/performance_points.py | 1 + judge/tasks/submission.py | 33 +++++++++++++++++-- judge/views/problem_manage.py | 23 ++++++++++++- judge/views/ranked_submission.py | 4 +-- judge/views/user.py | 4 +-- .../admin/judge/submission/change_form.html | 2 +- templates/problem/manage_submission.html | 27 +++++++++++++++ templates/submission/row.html | 16 ++++++--- templates/submission/source.html | 2 +- templates/submission/status.html | 2 +- 15 files changed, 131 insertions(+), 21 deletions(-) create mode 100644 judge/migrations/0150_submission_is_quarantined.py diff --git a/dmoj/urls.py b/dmoj/urls.py index 5852a0094b..696e9ef778 100644 --- a/dmoj/urls.py +++ b/dmoj/urls.py @@ -131,6 +131,10 @@ def paged_list_view(view, name): path('/manage/submission', include([ path('', problem_manage.ManageProblemSubmissionView.as_view(), name='problem_manage_submissions'), + path('/quarantine/locked', problem_manage.QuarantineLockedSubmissionsView.as_view(), + name='problem_submissions_quarantine_locked'), + path('/quarantine/success/', problem_manage.quarantine_success, + name='problem_submissions_quarantine_success'), path('/rejudge', problem_manage.RejudgeSubmissionsView.as_view(), name='problem_submissions_rejudge'), path('/rejudge/preview', problem_manage.PreviewRejudgeSubmissionsView.as_view(), name='problem_submissions_rejudge_preview'), diff --git a/judge/admin/submission.py b/judge/admin/submission.py index 124853365a..2ac45cb576 100644 --- a/judge/admin/submission.py +++ b/judge/admin/submission.py @@ -120,8 +120,8 @@ def get_formset(self, request, obj=None, **kwargs): class SubmissionAdmin(VersionAdmin): readonly_fields = ('user', 'problem', 'date', 'judged_date') - fields = ('user', 'problem', 'date', 'judged_date', 'locked_after', 'time', 'memory', 'points', 'language', - 'status', 'result', 'case_points', 'case_total', 'judged_on', 'error') + fields = ('user', 'problem', 'date', 'judged_date', 'locked_after', 'is_quarantined', 'time', 'memory', 'points', + 'language', 'status', 'result', 'case_points', 'case_total', 'judged_on', 'error') actions = ('judge', 'recalculate_score') list_display = ('id', 'problem_code', 'problem_name', 'user_column', 'execution_time', 'pretty_memory', 'points', 'language_column', 'status', 'result', 'judge_column') @@ -244,7 +244,9 @@ def language_column(self, obj): @admin.display(description='') def judge_column(self, obj): - if obj.is_locked: + if obj.is_quarantined: + return format_html('', _('Quarantined')) + elif obj.is_locked: return format_html('', _('Locked')) else: return format_html('{0}', _('Rejudge'), diff --git a/judge/migrations/0150_submission_is_quarantined.py b/judge/migrations/0150_submission_is_quarantined.py new file mode 100644 index 0000000000..9fbc150bbc --- /dev/null +++ b/judge/migrations/0150_submission_is_quarantined.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.17 on 2025-03-02 06:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('judge', '0149_add_organization_private_problems_permission'), + ] + + operations = [ + migrations.AddField( + model_name='submission', + name='is_quarantined', + field=models.BooleanField(default=False, verbose_name='is quarantined'), + ), + ] diff --git a/judge/models/profile.py b/judge/models/profile.py index 859c30d47f..1263860497 100644 --- a/judge/models/profile.py +++ b/judge/models/profile.py @@ -243,7 +243,8 @@ def calculate_points(self, table=_pp_table): from judge.models import Problem public_problems = Problem.get_public_problems() data = ( - public_problems.filter(submission__user=self, submission__points__isnull=False) + public_problems.filter(submission__user=self, submission__is_quarantined=False, + submission__points__isnull=False) .annotate(max_points=Max('submission__points')).order_by('-max_points') .values_list('max_points', flat=True).filter(max_points__gt=0) ) @@ -251,7 +252,7 @@ def calculate_points(self, table=_pp_table): points = sum(data) entries = min(len(data), len(table)) problems = ( - public_problems.filter(submission__user=self, submission__result='AC', + public_problems.filter(submission__user=self, submission__is_quarantined=False, submission__result='AC', submission__case_points__gte=F('submission__case_total')) .values('id').distinct().count() ) diff --git a/judge/models/submission.py b/judge/models/submission.py index 5aa6c67659..ff4fd32dd7 100644 --- a/judge/models/submission.py +++ b/judge/models/submission.py @@ -88,6 +88,7 @@ class Submission(models.Model): contest_object = models.ForeignKey('Contest', verbose_name=_('contest'), null=True, blank=True, on_delete=models.SET_NULL, related_name='+', db_index=False) locked_after = models.DateTimeField(verbose_name=_('submission lock'), null=True, blank=True) + is_quarantined = models.BooleanField(verbose_name=_('is quarantined'), default=False) @classmethod def result_class_from_code(cls, result, case_points, case_total): @@ -121,7 +122,7 @@ def is_locked(self): return self.locked_after is not None and self.locked_after < timezone.now() def judge(self, *args, rejudge=False, force_judge=False, rejudge_user=None, **kwargs): - if force_judge or not self.is_locked: + if force_judge or not (self.is_quarantined or self.is_locked): if rejudge: with revisions.create_revision(manage_manually=True): if rejudge_user: diff --git a/judge/performance_points.py b/judge/performance_points.py index 30de63e19d..7faab7057a 100644 --- a/judge/performance_points.py +++ b/judge/performance_points.py @@ -37,6 +37,7 @@ def get_pp_breakdown(user, start=0, end=settings.DMOJ_PP_ENTRIES): INNER JOIN judge_submission ON (judge_problem.id = judge_submission.problem_id) WHERE (judge_problem.is_public AND NOT judge_problem.is_organization_private AND + NOT judge_submission.is_quarantined AND judge_submission.points IS NOT NULL AND judge_submission.user_id = %s) GROUP BY judge_problem.id diff --git a/judge/tasks/submission.py b/judge/tasks/submission.py index a190ba71ee..fbb0b617bf 100644 --- a/judge/tasks/submission.py +++ b/judge/tasks/submission.py @@ -7,7 +7,7 @@ from judge.models import Problem, Profile, Submission from judge.utils.celery import Progress -__all__ = ('apply_submission_filter', 'rejudge_problem_filter', 'rescore_problem') +__all__ = ('apply_submission_filter', 'quarantine_locked_submissions', 'rejudge_problem_filter', 'rescore_problem') def apply_submission_filter(queryset, id_range, languages, results): @@ -18,11 +18,40 @@ def apply_submission_filter(queryset, id_range, languages, results): queryset = queryset.filter(language_id__in=languages) if results: queryset = queryset.filter(result__in=results) - queryset = queryset.exclude(locked_after__lt=timezone.now()) \ + queryset = queryset.exclude(is_quarantined=True) \ + .exclude(locked_after__lt=timezone.now()) \ .exclude(status__in=Submission.IN_PROGRESS_GRADING_STATUS) return queryset +@shared_task(bind=True) +def quarantine_locked_submissions(self, problem_id): + submissions = \ + Submission.objects.filter(problem_id=problem_id, is_quarantined=False, locked_after__lt=timezone.now()) + + with Progress(self, submissions.count(), stage=_('Modifying submissions')) as p: + quarantined = 0 + for submission in submissions.iterator(): + submission.is_quarantined = True + submission.save(update_fields=['is_quarantined']) + quarantined += 1 + if quarantined % 10 == 0: + p.done = quarantined + + with Progress(self, submissions.values('user_id').distinct().count(), stage=_('Recalculating user points')) as p: + users = 0 + profiles = Profile.objects.filter(id__in=submissions.values_list('user_id', flat=True).distinct()) + for profile in profiles.iterator(): + profile._updating_stats_only = True + profile.calculate_points() + cache.delete('user_complete:%d' % profile.id) + cache.delete('user_attempted:%d' % profile.id) + users += 1 + if users % 10 == 0: + p.done = users + return quarantined + + @shared_task(bind=True) def rejudge_problem_filter(self, problem_id, id_range=None, languages=None, results=None, user_id=None): queryset = Submission.objects.filter(problem_id=problem_id) diff --git a/judge/views/problem_manage.py b/judge/views/problem_manage.py index 5857b0cc55..991740164c 100644 --- a/judge/views/problem_manage.py +++ b/judge/views/problem_manage.py @@ -5,6 +5,7 @@ from django.contrib.auth.mixins import PermissionRequiredMixin from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect from django.urls import reverse +from django.utils import timezone from django.utils.html import escape, format_html from django.utils.safestring import mark_safe from django.utils.translation import gettext as _, ngettext @@ -12,7 +13,7 @@ from django.views.generic.detail import BaseDetailView from judge.models import Language, Submission -from judge.tasks import apply_submission_filter, rejudge_problem_filter, rescore_problem +from judge.tasks import apply_submission_filter, quarantine_locked_submissions, rejudge_problem_filter, rescore_problem from judge.utils.celery import redirect_to_task_status from judge.utils.views import TitleMixin from judge.views.problem import ProblemMixin @@ -56,6 +57,8 @@ def get_content_title(self): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) + context['locked_count'] = \ + self.object.submission_set.filter(is_quarantined=False, locked_after__lt=timezone.now()).count() context['submission_count'] = self.object.submission_set.count() context['languages'] = [(lang_id, short_name or key) for lang_id, key, short_name in Language.objects.values_list('id', 'key', 'short_name')] @@ -63,6 +66,15 @@ def get_context_data(self, **kwargs): return context +class QuarantineLockedSubmissionsView(ManageProblemSubmissionActionMixin, BaseDetailView): + def perform_action(self): + status = quarantine_locked_submissions.delay(self.object.id) + return redirect_to_task_status( + status, message=_('Quarantining all locked submissions for %s...') % (self.object.name,), + redirect=reverse('problem_submissions_quarantine_success', args=[self.object.code, status.id]), + ) + + class BaseRejudgeSubmissionsView(PermissionRequiredMixin, ManageProblemSubmissionActionMixin, BaseDetailView): permission_required = 'judge.rejudge_submission_lot' @@ -113,6 +125,15 @@ def perform_action(self): ) +def quarantine_success(request, problem, task_id): + count = AsyncResult(task_id).result + if not isinstance(count, int): + raise Http404() + messages.success(request, ngettext('%d submission was successfully quarantined.', + '%d submissions were successfully quarantined.', count) % (count,)) + return HttpResponseRedirect(reverse('problem_manage_submissions', args=[problem])) + + def rejudge_success(request, problem, task_id): count = AsyncResult(task_id).result if not isinstance(count, int): diff --git a/judge/views/ranked_submission.py b/judge/views/ranked_submission.py index d2f9473fb7..3bc9779727 100644 --- a/judge/views/ranked_submission.py +++ b/judge/views/ranked_submission.py @@ -42,12 +42,12 @@ def get_queryset(self): FROM ( SELECT sub.user_id AS uid, MAX(sub.points) AS points FROM judge_submission AS sub {contest_join} - WHERE sub.problem_id = %s AND {points} > 0 {constraint} + WHERE sub.problem_id = %s AND NOT sub.is_quarantined AND {points} > 0 {constraint} GROUP BY sub.user_id ) AS highscore STRAIGHT_JOIN ( SELECT sub.user_id AS uid, sub.points, MIN(sub.time) as time FROM judge_submission AS sub {contest_join} - WHERE sub.problem_id = %s AND {points} > 0 {constraint} + WHERE sub.problem_id = %s AND NOT sub.is_quarantined AND {points} > 0 {constraint} GROUP BY sub.user_id, {points} ) AS fastest ON (highscore.uid = fastest.uid AND highscore.points = fastest.points) STRAIGHT_JOIN judge_submission AS sub diff --git a/judge/views/user.py b/judge/views/user.py index e38b0cd5ec..f1b0425276 100644 --- a/judge/views/user.py +++ b/judge/views/user.py @@ -203,8 +203,8 @@ class UserProblemsPage(UserPage): def get_context_data(self, **kwargs): context = super(UserProblemsPage, self).get_context_data(**kwargs) - result = Submission.objects.filter(user=self.object, points__gt=0, problem__is_public=True, - problem__is_organization_private=False) \ + result = Submission.objects.filter(user=self.object, is_quarantined=False, points__gt=0, + problem__is_public=True, problem__is_organization_private=False) \ .exclude(problem__in=self.get_completed_problems() if self.hide_solved else []) \ .values('problem__id', 'problem__code', 'problem__name', 'problem__points', 'problem__group__full_name') \ .distinct().annotate(points=Max('points')).order_by('problem__group__full_name', 'problem__code') diff --git a/templates/admin/judge/submission/change_form.html b/templates/admin/judge/submission/change_form.html index 96ebba97bf..188a982d3b 100644 --- a/templates/admin/judge/submission/change_form.html +++ b/templates/admin/judge/submission/change_form.html @@ -10,7 +10,7 @@ {% endblock extrahead %} {% block after_field_sets %}{{ block.super }} - {% if original and not original.is_locked %} + {% if original and not original.is_quarantined and not original.is_locked %} + +
+

{{ _('Quarantine Locked Submissions') }}

+

{% trans trimmed count=locked_count %} + This will quarantine {{ count }} submission. + {% pluralize %} + This will quarantine {{ count }} submissions. + {% endtrans %}

+
+ {% csrf_token %} + + {{ _('Quarantine all locked submissions') }} + +
+
{% endblock %} diff --git a/templates/submission/row.html b/templates/submission/row.html index fff8b93450..3bfb8495a3 100644 --- a/templates/submission/row.html +++ b/templates/submission/row.html @@ -54,16 +54,22 @@ {{ _('view') }} {% if perms.judge.rejudge_submission and can_edit %} · - {% if not submission.is_locked %} - - {{ _('rejudge') }} - - {% else %} + {% if submission.is_quarantined %} + + + {{ _('quarantined') }} + + {% elif submission.is_locked %} {{ _('locked') }} + {% else %} + + {{ _('rejudge') }} + {% endif %} {% endif %} {% if can_edit %} · diff --git a/templates/submission/source.html b/templates/submission/source.html index bcd1c2afc5..476973efb7 100644 --- a/templates/submission/source.html +++ b/templates/submission/source.html @@ -27,7 +27,7 @@ {% if request.user == submission.user.user or perms.judge.resubmit_other %}
{{ _('Resubmit') }}
{% endif %} - {% if perms.judge.rejudge_submission and not submission.is_locked %} + {% if perms.judge.rejudge_submission and not (submission.is_quarantined or submission.is_locked) %}
{% csrf_token %} diff --git a/templates/submission/status.html b/templates/submission/status.html index 410168283b..3195d45812 100644 --- a/templates/submission/status.html +++ b/templates/submission/status.html @@ -74,7 +74,7 @@ {% if request.user == submission.user.user or perms.judge.resubmit_other %}
{{ _('Resubmit') }}
{% endif %} - {% if perms.judge.rejudge_submission and not submission.is_locked %} + {% if perms.judge.rejudge_submission and not (submission.is_quarantined or submission.is_locked) %} {% compress js %}