Skip to content
New issue

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

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

Already on GitHub? # 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