Skip to content

Commit abbf77f

Browse files
committed
refactor: migrate problem authors/curators/testers to ProblemRole
1 parent c56fcb7 commit abbf77f

File tree

17 files changed

+305
-77
lines changed

17 files changed

+305
-77
lines changed

<desired bridge log path>

Whitespace-only changes.

judge/admin/problem.py

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
from operator import attrgetter
2-
31
from django import forms
42
from django.contrib import admin
53
from django.db import transaction
@@ -10,7 +8,8 @@
108
from django.utils.translation import gettext, gettext_lazy as _, ngettext
119
from reversion.admin import VersionAdmin
1210

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

2221
def __init__(self, *args, **kwargs):
2322
super(ProblemForm, self).__init__(*args, **kwargs)
24-
self.fields['authors'].widget.can_add_related = False
25-
self.fields['curators'].widget.can_add_related = False
2623
self.fields['suggester'].widget.can_add_related = False
27-
self.fields['testers'].widget.can_add_related = False
2824
self.fields['banned_users'].widget.can_add_related = False
2925
self.fields['change_message'].widget.attrs.update({
3026
'placeholder': gettext('Describe the changes you made (optional)'),
3127
})
3228

3329
class Meta:
3430
widgets = {
35-
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2'),
36-
'curators': AdminHeavySelect2MultipleWidget(data_view='profile_select2'),
3731
'suggester': AdminHeavySelect2Widget(data_view='profile_select2'),
38-
'testers': AdminHeavySelect2MultipleWidget(data_view='profile_select2'),
3932
'banned_users': AdminHeavySelect2MultipleWidget(data_view='profile_select2'),
4033
'organization': AdminHeavySelect2Widget(data_view='organization_select2'),
4134
'types': AdminSelect2MultipleWidget,
@@ -48,13 +41,18 @@ class ProblemCreatorListFilter(admin.SimpleListFilter):
4841
title = parameter_name = 'creator'
4942

5043
def lookups(self, request, model_admin):
51-
queryset = Profile.objects.exclude(authored_problems=None).values_list('user__username', flat=True)
44+
queryset = ProblemRole.objects.filter(
45+
role=ROLE_AUTHOR,
46+
).values_list('user__user__username', flat=True).distinct()
5247
return [(name, name) for name in queryset]
5348

5449
def queryset(self, request, queryset):
5550
if self.value() is None:
5651
return queryset
57-
return queryset.filter(authors__user__username=self.value())
52+
return queryset.filter(
53+
problem_roles__role=ROLE_AUTHOR,
54+
problem_roles__user__user__username=self.value(),
55+
)
5856

5957

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

120118

119+
class ProblemRoleInlineForm(ModelForm):
120+
class Meta:
121+
model = ProblemRole
122+
fields = ('user', 'role')
123+
widgets = {'user': AdminHeavySelect2Widget(data_view='profile_select2')}
124+
125+
126+
class ProblemRoleInline(admin.TabularInline):
127+
model = ProblemRole
128+
fields = ('user', 'role')
129+
extra = 0
130+
form = ProblemRoleInlineForm
131+
132+
121133
class ProblemAdmin(NoBatchDeleteMixin, VersionAdmin):
122134
fieldsets = (
123135
(None, {
124136
'fields': (
125-
'code', 'name', 'suggester', 'is_public', 'is_manually_managed', 'date', 'authors',
126-
'curators', 'testers', 'is_organization_private', 'organization', 'submission_source_visibility_mode',
137+
'code', 'name', 'suggester', 'is_public', 'is_manually_managed', 'date',
138+
'is_organization_private', 'organization', 'submission_source_visibility_mode',
127139
'testcase_visibility_mode', 'testcase_result_visibility_mode', 'allow_view_feedback',
128140
'is_full_markup', 'pdf_url', 'source', 'description', 'license',
129141
),
@@ -138,8 +150,9 @@ class ProblemAdmin(NoBatchDeleteMixin, VersionAdmin):
138150
)
139151
list_display = ['code', 'name', 'show_authors', 'points', 'is_public', 'show_public']
140152
ordering = ['code']
141-
search_fields = ('code', 'name', 'authors__user__username', 'curators__user__username')
142-
inlines = [LanguageLimitInline, ProblemClarificationInline, ProblemSolutionInline, ProblemTranslationInline]
153+
search_fields = ('code', 'name', 'problem_roles__user__user__username')
154+
inlines = [ProblemRoleInline, LanguageLimitInline, ProblemClarificationInline,
155+
ProblemSolutionInline, ProblemTranslationInline]
143156
list_max_show_all = 1000
144157
actions_on_top = True
145158
actions_on_bottom = True
@@ -173,7 +186,10 @@ def get_readonly_fields(self, request, obj=None):
173186

174187
@admin.display(description=_('authors'))
175188
def show_authors(self, obj):
176-
return ', '.join(map(attrgetter('user.username'), obj.authors.all()))
189+
return ', '.join(
190+
ProblemRole.objects.filter(problem=obj, role=ROLE_AUTHOR)
191+
.values_list('user__user__username', flat=True),
192+
)
177193

178194
@admin.display(description='')
179195
def show_public(self, obj):
@@ -203,7 +219,9 @@ def make_private(self, request, queryset):
203219
count) % count)
204220

205221
def get_queryset(self, request):
206-
return Problem.get_editable_problems(request.user).prefetch_related('authors__user').distinct()
222+
return Problem.get_editable_problems(request.user).prefetch_related(
223+
'problem_roles__user__user',
224+
).distinct()
207225

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

218236
def get_form(self, *args, **kwargs):
219-
form = super(ProblemAdmin, self).get_form(*args, **kwargs)
220-
form.base_fields['authors'].queryset = Profile.objects.all()
221-
return form
237+
return super(ProblemAdmin, self).get_form(*args, **kwargs)
222238

223239
def save_model(self, request, obj, form, change):
224240
super(ProblemAdmin, self).save_model(request, obj, form, change)

judge/admin/submission.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from django.contrib import admin, messages
66
from django.core.cache import cache
77
from django.core.exceptions import PermissionDenied
8-
from django.db.models import Q
8+
from django.db.models import Exists, OuterRef
99
from django.http import HttpResponseRedirect
1010
from django.shortcuts import get_object_or_404
1111
from django.urls import path, reverse
@@ -17,6 +17,7 @@
1717

1818
from judge.models import ContestParticipation, ContestProblem, ContestSubmission, Profile, Submission, \
1919
SubmissionSource, SubmissionTestCase
20+
from judge.models.role import ProblemRole, ROLE_AUTHOR, ROLE_CURATOR
2021
from judge.utils.raw_sql import use_straight_join
2122
from judge.widgets import AdminAceWidget
2223

@@ -143,8 +144,12 @@ def get_queryset(self, request):
143144
)
144145
use_straight_join(queryset)
145146
if not request.user.has_perm('judge.edit_all_problem'):
146-
id = request.profile.id
147-
queryset = queryset.filter(Q(problem__authors__id=id) | Q(problem__curators__id=id)).distinct()
147+
queryset = queryset.annotate(
148+
has_editor=Exists(ProblemRole.objects.filter(
149+
problem_id=OuterRef('problem_id'), user_id=request.profile.id,
150+
role__in=[ROLE_AUTHOR, ROLE_CURATOR],
151+
)),
152+
).filter(has_editor=True).distinct()
148153
return queryset
149154

150155
def has_add_permission(self, request):
@@ -173,8 +178,12 @@ def judge(self, request, queryset):
173178
level=messages.ERROR)
174179
return
175180
if not request.user.has_perm('judge.edit_all_problem'):
176-
id = request.profile.id
177-
queryset = queryset.filter(Q(problem__authors__id=id) | Q(problem__curators__id=id))
181+
queryset = queryset.annotate(
182+
has_editor=Exists(ProblemRole.objects.filter(
183+
problem_id=OuterRef('problem_id'), user_id=request.profile.id,
184+
role__in=[ROLE_AUTHOR, ROLE_CURATOR],
185+
)),
186+
).filter(has_editor=True)
178187
judged = len(queryset)
179188
for model in queryset:
180189
model.judge(rejudge=True, batch_rejudge=True, rejudge_user=request.user)

judge/forms.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -225,17 +225,13 @@ class Meta:
225225
model = Problem
226226
fields = ['is_public', 'code', 'name', 'time_limit', 'memory_limit', 'points', 'partial',
227227
'statement_file', 'source', 'types', 'group', 'submission_source_visibility_mode',
228-
'testcase_visibility_mode', 'description', 'testers']
228+
'testcase_visibility_mode', 'description']
229229
widgets = {
230230
'types': Select2MultipleWidget,
231231
'group': Select2Widget,
232232
'submission_source_visibility_mode': Select2Widget,
233233
'testcase_visibility_mode': Select2Widget,
234234
'description': MartorWidget(attrs={'data-markdownfy-url': reverse_lazy('problem_preview')}),
235-
'testers': HeavySelect2MultipleWidget(
236-
data_view='profile_select2',
237-
attrs={'style': 'width: 100%'},
238-
),
239235
}
240236
help_texts = {
241237
'is_public': _(
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
"""Single migration: create ProblemRole, copy problem authors/curators/testers data, remove old M2M fields."""
2+
import django.db.models.deletion
3+
from django.db import migrations, models
4+
5+
ROLE_AUTHOR = 'A'
6+
ROLE_CURATOR = 'C'
7+
ROLE_TESTER = 'T'
8+
9+
10+
def forwards(apps, schema_editor):
11+
Problem = apps.get_model('judge', 'Problem')
12+
ProblemRole = apps.get_model('judge', 'ProblemRole')
13+
14+
problem_roles = []
15+
for problem in Problem.objects.prefetch_related('authors', 'curators', 'testers').iterator():
16+
for author in problem.authors.all():
17+
problem_roles.append(ProblemRole(problem=problem, user=author, role=ROLE_AUTHOR))
18+
for curator in problem.curators.all():
19+
problem_roles.append(ProblemRole(problem=problem, user=curator, role=ROLE_CURATOR))
20+
for tester in problem.testers.all():
21+
problem_roles.append(ProblemRole(problem=problem, user=tester, role=ROLE_TESTER))
22+
ProblemRole.objects.bulk_create(problem_roles, ignore_conflicts=True)
23+
24+
25+
def backwards(apps, schema_editor):
26+
Problem = apps.get_model('judge', 'Problem')
27+
ProblemRole = apps.get_model('judge', 'ProblemRole')
28+
29+
for problem in Problem.objects.all().iterator():
30+
roles = ProblemRole.objects.filter(problem=problem)
31+
problem.authors.set(roles.filter(role=ROLE_AUTHOR).values_list('user', flat=True))
32+
problem.curators.set(roles.filter(role=ROLE_CURATOR).values_list('user', flat=True))
33+
problem.testers.set(roles.filter(role=ROLE_TESTER).values_list('user', flat=True))
34+
35+
36+
class Migration(migrations.Migration):
37+
dependencies = [
38+
('judge', '0220_migrate_contest_roles'),
39+
]
40+
41+
operations = [
42+
migrations.CreateModel(
43+
name='ProblemRole',
44+
fields=[
45+
(
46+
'id',
47+
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
48+
),
49+
(
50+
'role',
51+
models.CharField(
52+
choices=[('A', 'Author'), ('C', 'Curator'), ('T', 'Tester')],
53+
max_length=1,
54+
verbose_name='role',
55+
),
56+
),
57+
(
58+
'problem',
59+
models.ForeignKey(
60+
on_delete=django.db.models.deletion.CASCADE,
61+
related_name='problem_roles',
62+
to='judge.problem',
63+
verbose_name='problem',
64+
),
65+
),
66+
(
67+
'user',
68+
models.ForeignKey(
69+
on_delete=django.db.models.deletion.CASCADE,
70+
related_name='problem_roles',
71+
to='judge.profile',
72+
verbose_name='user',
73+
),
74+
),
75+
],
76+
options={
77+
'verbose_name': 'problem role',
78+
'verbose_name_plural': 'problem roles',
79+
'unique_together': {('problem', 'user', 'role')},
80+
},
81+
),
82+
migrations.RunPython(forwards, backwards),
83+
migrations.RemoveField(
84+
model_name='problem',
85+
name='authors',
86+
),
87+
migrations.RemoveField(
88+
model_name='problem',
89+
name='curators',
90+
),
91+
migrations.RemoveField(
92+
model_name='problem',
93+
name='testers',
94+
),
95+
]

judge/models/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
problem_directory_file
1212
from judge.models.profile import Badge, Organization, OrganizationMonthlyUsage, OrganizationRequest, \
1313
Profile, WebAuthnCredential
14-
from judge.models.role import ContestRole, ROLE_AUTHOR, ROLE_CURATOR, ROLE_TESTER
14+
from judge.models.role import ContestRole, ProblemRole, ROLE_AUTHOR, ROLE_CURATOR, ROLE_TESTER
1515
from judge.models.runtime import Judge, Language, RuntimeVersion
1616
from judge.models.submission import SUBMISSION_RESULT, Submission, SubmissionSource, SubmissionTestCase
1717
from judge.models.tag import Tag, TagData, TagGroup, TagProblem
@@ -23,6 +23,7 @@
2323
revisions.register(Contest, follow=['contest_problems'])
2424
revisions.register(ContestProblem)
2525
revisions.register(ContestRole)
26+
revisions.register(ProblemRole)
2627
revisions.register(Organization)
2728
revisions.register(BlogPost)
2829
revisions.register(Solution)

judge/models/contest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from judge import contest_format, event_poster as event
1919
from judge.models.problem import Problem
2020
from judge.models.profile import Organization, Profile
21-
from judge.models.role import ContestRole, ROLE_AUTHOR, ROLE_CURATOR, ROLE_TESTER, RoleQuerySetAdapter
21+
from judge.models.role import ContestRole, RoleQuerySetAdapter, ROLE_AUTHOR, ROLE_CURATOR, ROLE_TESTER
2222
from judge.models.submission import Submission
2323
from judge.ratings import rate_contest
2424
from judge.utils.unicode import utf8bytes

0 commit comments

Comments
 (0)