diff --git a/judge/admin/contest.py b/judge/admin/contest.py index 8293708..f815016 100644 --- a/judge/admin/contest.py +++ b/judge/admin/contest.py @@ -123,7 +123,6 @@ class ContestAdmin(VersionAdmin): (_('Justice'), {'fields': ('banned_users',)}), ) list_display = ('key', 'name', 'is_visible', 'is_rated', 'start_time', 'end_time', 'time_limit', 'user_count') - actions = ['make_visible', 'make_hidden'] inlines = [ContestProblemInline] actions_on_top = True actions_on_bottom = True @@ -132,6 +131,16 @@ class ContestAdmin(VersionAdmin): filter_horizontal = ['rate_exclude'] date_hierarchy = 'start_time' + def get_actions(self, request): + actions = super(ContestAdmin, self).get_actions(request) + + if request.user.has_perm('judge.change_contest_visibility') or \ + request.user.has_perm('judge.create_private_contest'): + for action in ('make_visible', 'make_hidden'): + actions[action] = self.get_action(action) + + return actions + def get_queryset(self, request): queryset = Contest.objects.all() if request.user.has_perm('judge.edit_all_contest'): @@ -147,8 +156,32 @@ class ContestAdmin(VersionAdmin): readonly += ['access_code'] if not request.user.has_perm('judge.create_private_contest'): readonly += ['is_private', 'private_contestants', 'is_organization_private', 'organizations'] + if not request.user.has_perm('judge.change_contest_visibility'): + readonly += ['is_visible'] return readonly + def save_model(self, request, obj, form, change): + # `is_visible` will not appear in `cleaned_data` if user cannot edit it + if form.cleaned_data.get('is_visible') and not request.user.has_perm('judge.change_contest_visibility'): + if not form.cleaned_data['is_private'] and not form.cleaned_data['is_organization_private']: + raise PermissionDenied + if not request.user.has_perm('judge.create_private_contest'): + raise PermissionDenied + + super().save_model(request, obj, form, change) + + # We need this flag because `save_related` deals with the inlines, but does not know if we have already rescored + self._rescored = False + if form.changed_data and any(f in form.changed_data for f in ('format_config', 'format_name')): + self._rescore(obj.key) + self._rescored = True + + 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` + if not self._rescored and any(formset.has_changed() for formset in formsets): + self._rescore(form.cleaned_data['key']) + def has_change_permission(self, request, obj=None): if not request.user.has_perm('judge.edit_own_contest'): return False @@ -156,7 +189,13 @@ class ContestAdmin(VersionAdmin): return True return obj.organizers.filter(id=request.profile.id).exists() + def _rescore(self, contest_key): + from judge.tasks import rescore_contest + transaction.on_commit(rescore_contest.s(contest_key).delay) + def make_visible(self, request, queryset): + if not request.user.has_perm('judge.change_contest_visibility'): + queryset = queryset.filter(Q(is_private=True) | Q(is_organization_private=True)) count = queryset.update(is_visible=True) self.message_user(request, ungettext('%d contest successfully marked as visible.', '%d contests successfully marked as visible.', @@ -164,7 +203,9 @@ class ContestAdmin(VersionAdmin): make_visible.short_description = _('Mark contests as visible') def make_hidden(self, request, queryset): - count = queryset.update(is_visible=False) + if not request.user.has_perm('judge.change_contest_visibility'): + queryset = queryset.filter(Q(is_private=True) | Q(is_organization_private=True)) + count = queryset.update(is_visible=True) self.message_user(request, ungettext('%d contest successfully marked as hidden.', '%d contests successfully marked as hidden.', count) % count) diff --git a/judge/migrations/0113_auto_20201228_0911.py b/judge/migrations/0113_auto_20201228_0911.py new file mode 100644 index 0000000..f08350a --- /dev/null +++ b/judge/migrations/0113_auto_20201228_0911.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.17 on 2020-12-28 02:11 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('judge', '0112_contest_view_contest_scoreboard'), + ] + + 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')), 'verbose_name': 'contest', 'verbose_name_plural': 'contests'}, + ), + ] diff --git a/judge/models/contest.py b/judge/models/contest.py index 07d3624..939e06d 100644 --- a/judge/models/contest.py +++ b/judge/models/contest.py @@ -254,6 +254,7 @@ class Contest(models.Model): ('contest_rating', _('Rate contests')), ('contest_access_code', _('Contest access codes')), ('create_private_contest', _('Create private contests')), + ('change_contest_visibility', _('Change contest visibility')), ) verbose_name = _('contest') verbose_name_plural = _('contests') diff --git a/judge/tasks/__init__.py b/judge/tasks/__init__.py index e2ad1f2..22c5253 100644 --- a/judge/tasks/__init__.py +++ b/judge/tasks/__init__.py @@ -1,3 +1,3 @@ from judge.tasks.demo import * -from judge.tasks.moss import * +from judge.tasks.contest import * from judge.tasks.submission import * diff --git a/judge/tasks/moss.py b/judge/tasks/contest.py similarity index 80% rename from judge/tasks/moss.py rename to judge/tasks/contest.py index 66a4119..0c36e23 100644 --- a/judge/tasks/moss.py +++ b/judge/tasks/contest.py @@ -7,7 +7,22 @@ from moss import MOSS from judge.models import Contest, ContestMoss, ContestParticipation, Submission from judge.utils.celery import Progress -__all__ = ('run_moss',) +__all__ = ('rescore_contest', 'run_moss') + + +@shared_task(bind=True) +def rescore_contest(self, contest_key): + contest = Contest.objects.get(key=contest_key) + participations = contest.users + + rescored = 0 + with Progress(self, participations.count(), stage=_('Recalculating contest scores')) as p: + for participation in participations.iterator(): + participation.recompute_results() + rescored += 1 + if rescored % 10 == 0: + p.done = rescored + return rescored @shared_task(bind=True)