Skip to content

Commit

Permalink
Add field to quarantine a submission
Browse files Browse the repository at this point in the history
  • Loading branch information
int-y1 committed Mar 2, 2025
1 parent 4d1b682 commit b029b01
Show file tree
Hide file tree
Showing 15 changed files with 131 additions and 21 deletions.
4 changes: 4 additions & 0 deletions dmoj/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/<slug:task_id>', 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'),
Expand Down
8 changes: 5 additions & 3 deletions judge/admin/submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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('<input type="button" disabled value="{0}"/>', _('Quarantined'))
elif obj.is_locked:
return format_html('<input type="button" disabled value="{0}"/>', _('Locked'))
else:
return format_html('<a class="button action-link" href="{1}">{0}</a>', _('Rejudge'),
Expand Down
18 changes: 18 additions & 0 deletions judge/migrations/0150_submission_is_quarantined.py
Original file line number Diff line number Diff line change
@@ -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'),
),
]
5 changes: 3 additions & 2 deletions judge/models/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,15 +243,16 @@ 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)
)
bonus_function = settings.DMOJ_PP_BONUS_FUNCTION
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()
)
Expand Down
3 changes: 2 additions & 1 deletion judge/models/submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions judge/performance_points.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 31 additions & 2 deletions judge/tasks/submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)
Expand Down
23 changes: 22 additions & 1 deletion judge/views/problem_manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
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
from django.views.generic import DetailView
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
Expand Down Expand Up @@ -56,13 +57,24 @@ 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')]
context['results'] = sorted(map(itemgetter(0), Submission.RESULT))
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'

Expand Down Expand Up @@ -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):
Expand Down
4 changes: 2 additions & 2 deletions judge/views/ranked_submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions judge/views/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
2 changes: 1 addition & 1 deletion templates/admin/judge/submission/change_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
<a style="display: none" title="{% trans "Rejudge" %}" href="{% url 'admin:judge_submission_rejudge' original.pk %}"
class="button rejudgelink action-link">
<i class="fa fa-lg fa-refresh"></i>
Expand Down
27 changes: 27 additions & 0 deletions templates/problem/manage_submission.html
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@
}
});

$('#quarantine-locked').click(function (e) {
e.preventDefault();
if (confirm(this.dataset.warning)) {
$(this).parents('form').submit();
}
});

var $use_id = $('#by-range-check');
var $id_start = $('#by-range-start');
var $id_end = $('#by-range-end');
Expand Down Expand Up @@ -174,5 +181,25 @@ <h3>{{ _('Rescore Everything') }}</h3>
</a>
</form>
</div>

<div class="pane">
<h3>{{ _('Quarantine Locked Submissions') }}</h3>
<p id="quarantine-warning">{% trans trimmed count=locked_count %}
This will quarantine {{ count }} submission.
{% pluralize %}
This will quarantine {{ count }} submissions.
{% endtrans %}</p>
<form action="{{ url('problem_submissions_quarantine_locked', problem.code) }}" method="post">
{% csrf_token %}
<a id="quarantine-locked" class="unselectable button full" href="#"
data-warning="{% trans trimmed count=locked_count %}
Are you sure you want to quarantine {{ count }} submission?
{% pluralize %}
Are you sure you want to quarantine {{ count }} submissions?
{% endtrans %}">
{{ _('Quarantine all locked submissions') }}
</a>
</form>
</div>
</div>
{% endblock %}
16 changes: 11 additions & 5 deletions templates/submission/row.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,22 @@
<i class="fa fa-eye fa-fw"></i><span class="label">{{ _('view') }}</span>
</a>
{% if perms.judge.rejudge_submission and can_edit %} ·
{% if not submission.is_locked %}
<a href="#" onclick="rejudge_submission({{ submission.id }}, event);return false">
<i class="fa fa-refresh fa-fw"></i><span class="label">{{ _('rejudge') }}</span>
</a>
{% else %}
{% if submission.is_quarantined %}
<i class="fa fa-refresh fa-fw grey-icon"></i>
<span class="label grey-label"
title="{{ _('This submission has been quarantined, and cannot be rejudged.') }}">
{{ _('quarantined') }}
</span>
{% elif submission.is_locked %}
<i class="fa fa-refresh fa-fw grey-icon"></i>
<span class="label grey-label"
title="{{ _('This submission has been locked, and cannot be rejudged.') }}">
{{ _('locked') }}
</span>
{% else %}
<a href="#" onclick="rejudge_submission({{ submission.id }}, event);return false">
<i class="fa fa-refresh fa-fw"></i><span class="label">{{ _('rejudge') }}</span>
</a>
{% endif %}
{% endif %}
{% if can_edit %} ·
Expand Down
2 changes: 1 addition & 1 deletion templates/submission/source.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
{% if request.user == submission.user.user or perms.judge.resubmit_other %}
<div><a href="{{ url('problem_submit', submission.problem.code, submission.id) }}">{{ _('Resubmit') }}</a></div>
{% 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) %}
<div>
<form action="{{ url('submission_rejudge') }}" method="post">
{% csrf_token %}
Expand Down
2 changes: 1 addition & 1 deletion templates/submission/status.html
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
{% if request.user == submission.user.user or perms.judge.resubmit_other %}
<div><a href="{{ url('problem_submit', submission.problem.code, submission.id) }}">{{ _('Resubmit') }}</a></div>
{% 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 %}
<script type="text/javascript">
window.confirm_and_rejudge = function (form) {
Expand Down

0 comments on commit b029b01

Please sign in to comment.