Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement submission locks #1391

Merged
merged 9 commits into from
May 21, 2020
41 changes: 38 additions & 3 deletions judge/admin/contest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from reversion.admin import VersionAdmin

from django_ace import AceWidget
from judge.models import Contest, ContestProblem, ContestSubmission, Profile, Rating
from judge.models import Contest, ContestProblem, ContestSubmission, Profile, Rating, Submission
from judge.ratings import rate_contest
from judge.utils.views import NoBatchDeleteMixin
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, AdminMartorWidget, \
Expand Down Expand Up @@ -111,7 +111,7 @@ class ContestAdmin(NoBatchDeleteMixin, VersionAdmin):
fieldsets = (
(None, {'fields': ('key', 'name', 'organizers')}),
(_('Settings'), {'fields': ('is_visible', 'use_clarifications', 'hide_problem_tags', 'hide_scoreboard',
'run_pretests_only')}),
'run_pretests_only', 'is_locked')}),
(_('Scheduling'), {'fields': ('start_time', 'end_time', 'time_limit')}),
(_('Details'), {'fields': ('description', 'og_image', 'logo_override_image', 'tags', 'summary')}),
(_('Format'), {'fields': ('format_name', 'format_config', 'problem_label_script')}),
Expand All @@ -120,7 +120,8 @@ class ContestAdmin(NoBatchDeleteMixin, VersionAdmin):
'organizations', 'view_contest_scoreboard')}),
(_('Justice'), {'fields': ('banned_users',)}),
)
list_display = ('key', 'name', 'is_visible', 'is_rated', 'start_time', 'end_time', 'time_limit', 'user_count')
list_display = ('key', 'name', 'is_visible', 'is_rated', 'is_locked', 'start_time', 'end_time', 'time_limit',
'user_count')
search_fields = ('key', 'name')
inlines = [ContestProblemInline]
actions_on_top = True
Expand All @@ -138,6 +139,10 @@ def get_actions(self, request):
for action in ('make_visible', 'make_hidden'):
actions[action] = self.get_action(action)

if request.user.has_perm('judge.contest_lock'):
for action in ('set_locked', 'set_unlocked'):
actions[action] = self.get_action(action)

return actions

def get_queryset(self, request):
Expand All @@ -151,6 +156,8 @@ def get_readonly_fields(self, request, obj=None):
readonly = []
if not request.user.has_perm('judge.contest_rating'):
readonly += ['is_rated', 'rate_all', 'rate_exclude']
if not request.user.has_perm('judge.contest_lock'):
readonly += ['is_locked']
if not request.user.has_perm('judge.contest_access_code'):
readonly += ['access_code']
if not request.user.has_perm('judge.create_private_contest'):
Expand All @@ -176,6 +183,9 @@ def save_model(self, request, obj, form, change):
self._rescore(obj.key)
self._rescored = True

if form.changed_data and 'is_locked' in form.changed_data:
self.set_is_locked(obj, form.cleaned_data['is_locked'])

def save_related(self, request, form, formsets, change):
super().save_related(request, form, formsets, change)
# Only rescored if we did not already do so in `save_model`
Expand Down Expand Up @@ -211,6 +221,31 @@ def make_hidden(self, request, queryset):
count) % count)
make_hidden.short_description = _('Mark contests as hidden')

def set_locked(self, request, queryset):
for row in queryset:
self.set_is_locked(row, True)
count = queryset.count()
self.message_user(request, ungettext('%d contest successfully locked.',
'%d contests successfully locked.',
count) % count)
set_locked.short_description = _('Lock contest submissions')

def set_unlocked(self, request, queryset):
for row in queryset:
self.set_is_locked(row, False)
count = queryset.count()
self.message_user(request, ungettext('%d contest successfully unlocked.',
'%d contests successfully unlocked.',
count) % count)
set_unlocked.short_description = _('Unlock contest submissions')

def set_is_locked(self, contest, is_locked):
with transaction.atomic():
contest.is_locked = is_locked
contest.save()
Submission.objects.filter(contest_object=contest,
contest__participation__virtual=0).update(is_locked=is_locked)

def get_urls(self):
return [
url(r'^rate/all/$', self.rate_all_view, name='judge_contest_rate_all'),
Expand Down
10 changes: 8 additions & 2 deletions judge/admin/submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ def get_formset(self, request, obj=None, **kwargs):

class SubmissionAdmin(admin.ModelAdmin):
readonly_fields = ('user', 'problem', 'date', 'judged_date')
fields = ('user', 'problem', 'date', 'judged_date', 'time', 'memory', 'points', 'language', 'status', 'result',
'case_points', 'case_total', 'judged_on', 'error')
fields = ('user', 'problem', 'date', 'judged_date', 'is_locked', '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 @@ -120,6 +120,12 @@ class SubmissionAdmin(admin.ModelAdmin):
actions_on_bottom = True
inlines = [SubmissionSourceInline, SubmissionTestCaseInline, ContestSubmissionInline]

def get_readonly_fields(self, request, obj=None):
fields = self.readonly_fields
if not request.user.has_perm('judge.lock_submission'):
fields += ('is_locked',)
return fields

def get_queryset(self, request):
queryset = Submission.objects.select_related('problem', 'user__user', 'language').only(
'problem__code', 'problem__name', 'user__user__username', 'language__name',
Expand Down
41 changes: 41 additions & 0 deletions judge/migrations/0107_submission_lock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Generated by Django 2.2.12 on 2020-05-13 20:58

from django.db import migrations, models
from django.utils import timezone


def updatecontestsubmissions(apps, schema_editor):
Contest = apps.get_model('judge', 'Contest')
Contest.objects.filter(end_time__lt=timezone.now()).update(is_locked=True)

Submission = apps.get_model('judge', 'Submission')
Submission.objects.filter(contest_object__is_locked=True, contest__participation__virtual=0).update(is_locked=True)


class Migration(migrations.Migration):

dependencies = [
('judge', '0106_user_data_download'),
]

operations = [
migrations.AlterModelOptions(
name='contest',
options={'permissions': (('see_private_contest', 'See private contests'), ('edit_own_contest', 'Edit own contests'), ('edit_all_contest', 'Edit all contests'), ('clone_contest', 'Clone contest'), ('moss_contest', 'MOSS contest'), ('contest_rating', 'Rate contests'), ('contest_access_code', 'Contest access codes'), ('create_private_contest', 'Create private contests'), ('change_contest_visibility', 'Change contest visibility'), ('contest_problem_label', 'Edit contest problem label script'), ('lock_contest', 'Change lock status of contest')), 'verbose_name': 'contest', 'verbose_name_plural': 'contests'},
),
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')), 'verbose_name': 'submission', 'verbose_name_plural': 'submissions'},
),
migrations.AddField(
model_name='contest',
name='is_locked',
field=models.BooleanField(default=False, help_text='Prevent submissions from this contest from being rejudged.', verbose_name='contest lock'),
),
migrations.AddField(
model_name='submission',
name='is_locked',
field=models.BooleanField(default=False, verbose_name='lock submission'),
),
migrations.RunPython(updatecontestsubmissions, reverse_code=migrations.RunPython.noop),
]
3 changes: 3 additions & 0 deletions judge/models/contest.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ class Contest(models.Model):
help_text='A custom Lua function to generate problem labels. Requires a '
'single function with an integer parameter, the zero-indexed '
'contest problem index, and returns a string, the label.')
is_locked = models.BooleanField(verbose_name=_('contest lock'), default=False,
help_text=_('Prevent submissions from this contest from being rejudged.'))

@cached_property
def format_class(self):
Expand Down Expand Up @@ -342,6 +344,7 @@ class Meta:
('create_private_contest', _('Create private contests')),
('change_contest_visibility', _('Change contest visibility')),
('contest_problem_label', _('Edit contest problem label script')),
('lock_contest', _('Change lock status of contest')),
)
verbose_name = _('contest')
verbose_name_plural = _('contests')
Expand Down
5 changes: 4 additions & 1 deletion judge/models/submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ class Submission(models.Model):
is_pretested = models.BooleanField(verbose_name=_('was ran on pretests only'), default=False)
contest_object = models.ForeignKey('Contest', verbose_name=_('contest'), null=True, blank=True,
on_delete=models.SET_NULL, related_name='+')
is_locked = models.BooleanField(verbose_name=_('lock submission'), default=False)

objects = TranslatedProblemForeignKeyQuerySet.as_manager()

Expand Down Expand Up @@ -114,7 +115,8 @@ def long_status(self):
return Submission.USER_DISPLAY_CODES.get(self.short_status, '')

def judge(self, *args, **kwargs):
judge_submission(self, *args, **kwargs)
if not self.is_locked:
judge_submission(self, *args, **kwargs)

judge.alters_data = True

Expand Down Expand Up @@ -194,6 +196,7 @@ class Meta:
('spam_submission', 'Submit without limit'),
('view_all_submission', 'View all submission'),
('resubmit_other', "Resubmit others' submission"),
('lock_submission', 'Change lock status of submission'),
)
verbose_name = _('submission')
verbose_name_plural = _('submissions')
Expand Down