Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added <desired bridge log path>
Empty file.
37 changes: 22 additions & 15 deletions judge/admin/contest.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from reversion.admin import VersionAdmin

from judge.models import Contest, ContestAnnouncement, ContestProblem, ContestSubmission, Profile, Rating, Submission
from judge.models.role import ContestRole, ROLE_AUTHOR, ROLE_CURATOR
from judge.ratings import rate_contest
from judge.utils.views import NoBatchDeleteMixin
from judge.widgets import AdminAceWidget, AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, \
Expand Down Expand Up @@ -124,9 +125,6 @@ def clean(self):

class Meta:
widgets = {
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2'),
'curators': AdminHeavySelect2MultipleWidget(data_view='profile_select2'),
'testers': AdminHeavySelect2MultipleWidget(data_view='profile_select2'),
'private_contestants': AdminHeavySelect2MultipleWidget(data_view='profile_select2'),
'organization': AdminHeavySelect2Widget(data_view='organization_select2'),
'tags': AdminSelect2MultipleWidget,
Expand All @@ -137,9 +135,23 @@ class Meta:
}


class ContestRoleInlineForm(ModelForm):
class Meta:
model = ContestRole
fields = ('user', 'role')
widgets = {'user': AdminHeavySelect2Widget(data_view='profile_select2')}


class ContestRoleInline(admin.TabularInline):
model = ContestRole
fields = ('user', 'role')
extra = 0
form = ContestRoleInlineForm


class ContestAdmin(NoBatchDeleteMixin, SortableAdminBase, VersionAdmin):
fieldsets = (
(None, {'fields': ('key', 'name', 'authors', 'curators', 'testers')}),
(None, {'fields': ('key', 'name')}),
(_('Settings'), {'fields': ('is_visible', 'use_clarifications', 'push_announcements', 'disallow_virtual',
'hide_problem_tags', 'hide_problem_authors', 'show_short_display',
'run_pretests_only', 'locked_after', 'scoreboard_visibility',
Expand All @@ -159,7 +171,7 @@ class ContestAdmin(NoBatchDeleteMixin, SortableAdminBase, VersionAdmin):
list_display = ('key', 'name', 'is_visible', 'is_rated', 'locked_after', 'start_time', 'end_time', 'time_limit',
'user_count')
search_fields = ('key', 'name')
inlines = [ContestProblemInline, ContestAnnouncementInline]
inlines = [ContestRoleInline, ContestProblemInline, ContestAnnouncementInline]
actions_on_top = True
actions_on_bottom = True
form = ContestForm
Expand All @@ -186,7 +198,11 @@ def get_queryset(self, request):
if request.user.has_perm('judge.edit_all_contest'):
return queryset
else:
return queryset.filter(Q(authors=request.profile) | Q(curators=request.profile)).distinct()
return queryset.filter(
contest_roles__user=request.profile,
).filter(
Q(contest_roles__role=ROLE_AUTHOR) | Q(contest_roles__role=ROLE_CURATOR),
).distinct()

def get_readonly_fields(self, request, obj=None):
readonly = []
Expand Down Expand Up @@ -358,18 +374,9 @@ def rate_view(self, request, id):
def get_form(self, request, obj=None, **kwargs):
form = super(ContestAdmin, self).get_form(request, obj, **kwargs)
if 'problem_label_script' in form.base_fields:
# form.base_fields['problem_label_script'] does not exist when the user has only view permission
# on the model.
form.base_fields['problem_label_script'].widget = AdminAceWidget(
mode='lua', theme=request.profile.resolved_ace_theme,
)

perms = ('edit_own_contest', 'edit_all_contest')
form.base_fields['curators'].queryset = Profile.objects.filter(
Q(user__is_superuser=True) |
Q(user__groups__permissions__codename__in=perms) |
Q(user__user_permissions__codename__in=perms),
).distinct()
return form


Expand Down
56 changes: 36 additions & 20 deletions judge/admin/problem.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from operator import attrgetter

from django import forms
from django.contrib import admin
from django.db import transaction
Expand All @@ -10,7 +8,8 @@
from django.utils.translation import gettext, gettext_lazy as _, ngettext
from reversion.admin import VersionAdmin

from judge.models import LanguageLimit, Problem, ProblemClarification, ProblemTranslation, Profile, Solution
from judge.models import LanguageLimit, Problem, ProblemClarification, ProblemTranslation, Solution
from judge.models.role import ProblemRole, ROLE_AUTHOR
from judge.utils.views import NoBatchDeleteMixin
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, AdminMartorWidget, \
AdminSelect2MultipleWidget, AdminSelect2Widget, CheckboxSelectMultipleWithSelectAll
Expand All @@ -21,21 +20,15 @@ class ProblemForm(ModelForm):

def __init__(self, *args, **kwargs):
super(ProblemForm, self).__init__(*args, **kwargs)
self.fields['authors'].widget.can_add_related = False
self.fields['curators'].widget.can_add_related = False
self.fields['suggester'].widget.can_add_related = False
self.fields['testers'].widget.can_add_related = False
self.fields['banned_users'].widget.can_add_related = False
self.fields['change_message'].widget.attrs.update({
'placeholder': gettext('Describe the changes you made (optional)'),
})

class Meta:
widgets = {
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2'),
'curators': AdminHeavySelect2MultipleWidget(data_view='profile_select2'),
'suggester': AdminHeavySelect2Widget(data_view='profile_select2'),
'testers': AdminHeavySelect2MultipleWidget(data_view='profile_select2'),
'banned_users': AdminHeavySelect2MultipleWidget(data_view='profile_select2'),
'organization': AdminHeavySelect2Widget(data_view='organization_select2'),
'types': AdminSelect2MultipleWidget,
Expand All @@ -48,13 +41,18 @@ class ProblemCreatorListFilter(admin.SimpleListFilter):
title = parameter_name = 'creator'

def lookups(self, request, model_admin):
queryset = Profile.objects.exclude(authored_problems=None).values_list('user__username', flat=True)
queryset = ProblemRole.objects.filter(
role=ROLE_AUTHOR,
).values_list('user__user__username', flat=True).distinct()
return [(name, name) for name in queryset]

def queryset(self, request, queryset):
if self.value() is None:
return queryset
return queryset.filter(authors__user__username=self.value())
return queryset.filter(
problem_roles__role=ROLE_AUTHOR,
problem_roles__user__user__username=self.value(),
)


class LanguageLimitInlineForm(ModelForm):
Expand Down Expand Up @@ -118,12 +116,26 @@ def has_permission_full_markup(self, request, obj=None):
has_add_permission = has_change_permission = has_delete_permission = has_permission_full_markup


class ProblemRoleInlineForm(ModelForm):
class Meta:
model = ProblemRole
fields = ('user', 'role')
widgets = {'user': AdminHeavySelect2Widget(data_view='profile_select2')}


class ProblemRoleInline(admin.TabularInline):
model = ProblemRole
fields = ('user', 'role')
extra = 0
form = ProblemRoleInlineForm


class ProblemAdmin(NoBatchDeleteMixin, VersionAdmin):
fieldsets = (
(None, {
'fields': (
'code', 'name', 'suggester', 'is_public', 'is_manually_managed', 'date', 'authors',
'curators', 'testers', 'is_organization_private', 'organization', 'submission_source_visibility_mode',
'code', 'name', 'suggester', 'is_public', 'is_manually_managed', 'date',
'is_organization_private', 'organization', 'submission_source_visibility_mode',
'testcase_visibility_mode', 'testcase_result_visibility_mode', 'allow_view_feedback',
'is_full_markup', 'pdf_url', 'source', 'description', 'license',
),
Expand All @@ -138,8 +150,9 @@ class ProblemAdmin(NoBatchDeleteMixin, VersionAdmin):
)
list_display = ['code', 'name', 'show_authors', 'points', 'is_public', 'show_public']
ordering = ['code']
search_fields = ('code', 'name', 'authors__user__username', 'curators__user__username')
inlines = [LanguageLimitInline, ProblemClarificationInline, ProblemSolutionInline, ProblemTranslationInline]
search_fields = ('code', 'name', 'problem_roles__user__user__username')
inlines = [ProblemRoleInline, LanguageLimitInline, ProblemClarificationInline,
ProblemSolutionInline, ProblemTranslationInline]
list_max_show_all = 1000
actions_on_top = True
actions_on_bottom = True
Expand Down Expand Up @@ -173,7 +186,10 @@ def get_readonly_fields(self, request, obj=None):

@admin.display(description=_('authors'))
def show_authors(self, obj):
return ', '.join(map(attrgetter('user.username'), obj.authors.all()))
return ', '.join(
ProblemRole.objects.filter(problem=obj, role=ROLE_AUTHOR)
.values_list('user__user__username', flat=True),
)

@admin.display(description='')
def show_public(self, obj):
Expand Down Expand Up @@ -203,7 +219,9 @@ def make_private(self, request, queryset):
count) % count)

def get_queryset(self, request):
return Problem.get_editable_problems(request.user).prefetch_related('authors__user').distinct()
return Problem.get_editable_problems(request.user).prefetch_related(
'problem_roles__user__user',
).distinct()

def has_change_permission(self, request, obj=None):
if obj is None:
Expand All @@ -216,9 +234,7 @@ def formfield_for_manytomany(self, db_field, request=None, **kwargs):
return super(ProblemAdmin, self).formfield_for_manytomany(db_field, request, **kwargs)

def get_form(self, *args, **kwargs):
form = super(ProblemAdmin, self).get_form(*args, **kwargs)
form.base_fields['authors'].queryset = Profile.objects.all()
return form
return super(ProblemAdmin, self).get_form(*args, **kwargs)

def save_model(self, request, obj, form, change):
super(ProblemAdmin, self).save_model(request, obj, form, change)
Expand Down
19 changes: 14 additions & 5 deletions judge/admin/submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from django.contrib import admin, messages
from django.core.cache import cache
from django.core.exceptions import PermissionDenied
from django.db.models import Q
from django.db.models import Exists, OuterRef
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import path, reverse
Expand All @@ -17,6 +17,7 @@

from judge.models import ContestParticipation, ContestProblem, ContestSubmission, Profile, Submission, \
SubmissionSource, SubmissionTestCase
from judge.models.role import ProblemRole, ROLE_AUTHOR, ROLE_CURATOR
from judge.utils.raw_sql import use_straight_join
from judge.widgets import AdminAceWidget

Expand Down Expand Up @@ -143,8 +144,12 @@ def get_queryset(self, request):
)
use_straight_join(queryset)
if not request.user.has_perm('judge.edit_all_problem'):
id = request.profile.id
queryset = queryset.filter(Q(problem__authors__id=id) | Q(problem__curators__id=id)).distinct()
queryset = queryset.annotate(
has_editor=Exists(ProblemRole.objects.filter(
problem_id=OuterRef('problem_id'), user_id=request.profile.id,
role__in=[ROLE_AUTHOR, ROLE_CURATOR],
)),
).filter(has_editor=True).distinct()
return queryset

def has_add_permission(self, request):
Expand Down Expand Up @@ -173,8 +178,12 @@ def judge(self, request, queryset):
level=messages.ERROR)
return
if not request.user.has_perm('judge.edit_all_problem'):
id = request.profile.id
queryset = queryset.filter(Q(problem__authors__id=id) | Q(problem__curators__id=id))
queryset = queryset.annotate(
has_editor=Exists(ProblemRole.objects.filter(
problem_id=OuterRef('problem_id'), user_id=request.profile.id,
role__in=[ROLE_AUTHOR, ROLE_CURATOR],
)),
).filter(has_editor=True)
judged = len(queryset)
for model in queryset:
model.judge(rejudge=True, batch_rejudge=True, rejudge_user=request.user)
Expand Down
6 changes: 1 addition & 5 deletions judge/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,17 +225,13 @@ class Meta:
model = Problem
fields = ['is_public', 'code', 'name', 'time_limit', 'memory_limit', 'points', 'partial',
'statement_file', 'source', 'types', 'group', 'submission_source_visibility_mode',
'testcase_visibility_mode', 'description', 'testers']
'testcase_visibility_mode', 'description']
widgets = {
'types': Select2MultipleWidget,
'group': Select2Widget,
'submission_source_visibility_mode': Select2Widget,
'testcase_visibility_mode': Select2Widget,
'description': MartorWidget(attrs={'data-markdownfy-url': reverse_lazy('problem_preview')}),
'testers': HeavySelect2MultipleWidget(
data_view='profile_select2',
attrs={'style': 'width: 100%'},
),
}
help_texts = {
'is_public': _(
Expand Down
8 changes: 4 additions & 4 deletions judge/jinja2/submission.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
from operator import attrgetter

from judge.models import SubmissionSourceAccess
from judge.models.role import ContestRole, ROLE_AUTHOR, ROLE_CURATOR
from . import registry


# TODO: maybe refactor this?
def get_editor_ids(contest):
return set(map(attrgetter('id'), contest.authors.all())) | set(map(attrgetter('id'), contest.curators.all()))
return set(ContestRole.objects.filter(
contest=contest, role__in=[ROLE_AUTHOR, ROLE_CURATOR],
).values_list('user_id', flat=True))


@registry.function
Expand Down
95 changes: 95 additions & 0 deletions judge/migrations/0220_migrate_contest_roles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"""Single migration: create ContestRole, copy contest authors/curators/testers data, remove old M2M fields."""
import django.db.models.deletion
from django.db import migrations, models

ROLE_AUTHOR = 'A'
ROLE_CURATOR = 'C'
ROLE_TESTER = 'T'


def forwards(apps, schema_editor):
Contest = apps.get_model('judge', 'Contest')
ContestRole = apps.get_model('judge', 'ContestRole')

contest_roles = []
for contest in Contest.objects.prefetch_related('authors', 'curators', 'testers').iterator():
for author in contest.authors.all():
contest_roles.append(ContestRole(contest=contest, user=author, role=ROLE_AUTHOR))
for curator in contest.curators.all():
contest_roles.append(ContestRole(contest=contest, user=curator, role=ROLE_CURATOR))
for tester in contest.testers.all():
contest_roles.append(ContestRole(contest=contest, user=tester, role=ROLE_TESTER))
ContestRole.objects.bulk_create(contest_roles, ignore_conflicts=True)


def backwards(apps, schema_editor):
Contest = apps.get_model('judge', 'Contest')
ContestRole = apps.get_model('judge', 'ContestRole')

for contest in Contest.objects.all().iterator():
roles = ContestRole.objects.filter(contest=contest)
contest.authors.set(roles.filter(role=ROLE_AUTHOR).values_list('user', flat=True))
contest.curators.set(roles.filter(role=ROLE_CURATOR).values_list('user', flat=True))
contest.testers.set(roles.filter(role=ROLE_TESTER).values_list('user', flat=True))


class Migration(migrations.Migration):
dependencies = [
('judge', '0219_problemdata_zipfile_size_alter_contest_authors_and_more'),
]

operations = [
migrations.CreateModel(
name='ContestRole',
fields=[
(
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
(
'role',
models.CharField(
choices=[('A', 'Author'), ('C', 'Curator'), ('T', 'Tester')],
max_length=1,
verbose_name='role',
),
),
(
'contest',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='contest_roles',
to='judge.contest',
verbose_name='contest',
),
),
(
'user',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='contest_roles',
to='judge.profile',
verbose_name='user',
),
),
],
options={
'verbose_name': 'contest role',
'verbose_name_plural': 'contest roles',
'unique_together': {('contest', 'user', 'role')},
},
),
migrations.RunPython(forwards, backwards),
migrations.RemoveField(
model_name='contest',
name='authors',
),
migrations.RemoveField(
model_name='contest',
name='curators',
),
migrations.RemoveField(
model_name='contest',
name='testers',
),
]
Loading
Loading