Skip to content

Commit

Permalink
Add support for archiving a submission
Browse files Browse the repository at this point in the history
  • Loading branch information
int-y1 committed Mar 3, 2025
1 parent 4d1b682 commit 89cdf39
Show file tree
Hide file tree
Showing 15 changed files with 155 additions and 20 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('/archive/locked', problem_manage.ArchiveLockedSubmissionsView.as_view(),
name='problem_submissions_archive_locked'),
path('/archive/success/<slug:task_id>', problem_manage.archive_success,
name='problem_submissions_archive_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
10 changes: 7 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_archived', '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 All @@ -135,6 +135,8 @@ def get_readonly_fields(self, request, obj=None):
fields = self.readonly_fields
if not request.user.has_perm('judge.lock_submission'):
fields += ('locked_after',)
if not request.user.has_perm('judge.archive_submission'):
fields += ('is_archived',)
return fields

def get_queryset(self, request):
Expand Down Expand Up @@ -244,7 +246,9 @@ def language_column(self, obj):

@admin.display(description='')
def judge_column(self, obj):
if obj.is_locked:
if obj.is_archived:
return format_html('<input type="button" disabled value="{0}"/>', _('Archived'))
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
35 changes: 35 additions & 0 deletions judge/migrations/0150_submission_is_quarantined.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Generated by Django 4.2.17 on 2025-03-02 10:16

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('judge', '0149_add_organization_private_problems_permission'),
]

operations = [
migrations.AlterModelOptions(
name='submission',
options={
'permissions': (
('abort_any_submission', 'Abort any submission'),
('rejudge_submission', 'Rejudge the submission'),
('rejudge_submission_lot', 'Rejudge a lot of submissions'),
('spam_submission', 'Submit without limit'),
('view_all_submission', 'View all submission'),
('resubmit_other', "Resubmit others' submission"),
('lock_submission', 'Change lock status of submission'),
('archive_submission', 'Archive any submission'),
),
'verbose_name': 'submission',
'verbose_name_plural': 'submissions',
},
),
migrations.AddField(
model_name='submission',
name='is_archived',
field=models.BooleanField(default=False, verbose_name='is archived'),
),
]
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_archived=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_archived=False, submission__result='AC',
submission__case_points__gte=F('submission__case_total'))
.values('id').distinct().count()
)
Expand Down
4 changes: 3 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_archived = models.BooleanField(verbose_name=_('is archived'), 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_archived or self.is_locked):
if rejudge:
with revisions.create_revision(manage_manually=True):
if rejudge_user:
Expand Down Expand Up @@ -227,6 +228,7 @@ class Meta:
('view_all_submission', _('View all submission')),
('resubmit_other', _("Resubmit others' submission")),
('lock_submission', _('Change lock status of submission')),
('archive_submission', _('Archive any submission')),
)
verbose_name = _('submission')
verbose_name_plural = _('submissions')
Expand Down
2 changes: 2 additions & 0 deletions judge/performance_points.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,15 @@ 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_archived AND
judge_submission.points IS NOT NULL AND
judge_submission.user_id = %s)
GROUP BY judge_problem.id
HAVING MAX(judge_submission.points) > 0.0
) AS max_points_table
{join_type} judge_submission ON (
judge_submission.problem_id = max_points_table.problem_id AND
NOT judge_submission.is_archived AND
judge_submission.points = max_points_table.max_points AND
judge_submission.user_id = %s
)
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', 'archive_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_archived=True) \
.exclude(locked_after__lt=timezone.now()) \
.exclude(status__in=Submission.IN_PROGRESS_GRADING_STATUS)
return queryset


@shared_task(bind=True)
def archive_locked_submissions(self, problem_id):
submissions = \
Submission.objects.filter(problem_id=problem_id, is_archived=False, locked_after__lt=timezone.now())

with Progress(self, submissions.count(), stage=_('Modifying submissions')) as p:
archived = 0
for submission in submissions.iterator():
submission.is_archived = True
submission.save(update_fields=['is_archived'])
archived += 1
if archived % 10 == 0:
p.done = archived

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 archived


@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
25 changes: 24 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, archive_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,26 @@ 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_archived=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 ArchiveLockedSubmissionsView(PermissionRequiredMixin, ManageProblemSubmissionActionMixin, BaseDetailView):
permission_required = 'judge.archive_submission'

def perform_action(self):
status = archive_locked_submissions.delay(self.object.id)
return redirect_to_task_status(
status, message=_('Archiving all locked submissions for %s...') % (self.object.name,),
redirect=reverse('problem_submissions_archive_success', args=[self.object.code, status.id]),
)


class BaseRejudgeSubmissionsView(PermissionRequiredMixin, ManageProblemSubmissionActionMixin, BaseDetailView):
permission_required = 'judge.rejudge_submission_lot'

Expand Down Expand Up @@ -113,6 +127,15 @@ def perform_action(self):
)


def archive_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 archived.',
'%d submissions were successfully archived.', 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_archived 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_archived 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
2 changes: 1 addition & 1 deletion judge/views/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ 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,
result = Submission.objects.filter(user=self.object, is_archived=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') \
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_archived 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
29 changes: 29 additions & 0 deletions templates/problem/manage_submission.html
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@
}
});

$('#archive-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,27 @@ <h3>{{ _('Rescore Everything') }}</h3>
</a>
</form>
</div>

{% if request.user.has_perm('judge.archive_submission') %}
<div class="pane">
<h3>{{ _('Archive Locked Submissions') }}</h3>
<p id="archive-warning">{% trans trimmed count=locked_count %}
This will archive {{ count }} submission.
{% pluralize %}
This will archive {{ count }} submissions.
{% endtrans %}</p>
<form action="{{ url('problem_submissions_archive_locked', problem.code) }}" method="post">
{% csrf_token %}
<a id="archive-locked" class="unselectable button full" href="#"
data-warning="{% trans trimmed count=locked_count %}
Are you sure you want to archive {{ count }} submission?
{% pluralize %}
Are you sure you want to archive {{ count }} submissions?
{% endtrans %}">
{{ _('Archive all locked submissions') }}
</a>
</form>
</div>
{% endif %}
</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_archived %}
<i class="fa fa-refresh fa-fw grey-icon"></i>
<span class="label grey-label"
title="{{ _('This submission has been archived, and cannot be rejudged.') }}">
{{ _('archived') }}
</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_archived 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_archived or submission.is_locked) %}
{% compress js %}
<script type="text/javascript">
window.confirm_and_rejudge = function (form) {
Expand Down

0 comments on commit 89cdf39

Please sign in to comment.