Skip to content

Commit 8db66a0

Browse files
authored
Merge branch 'DMOJ:master' into problem_authorship
2 parents b367497 + df99fe3 commit 8db66a0

File tree

68 files changed

+29740
-19530
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+29740
-19530
lines changed

.github/workflows/updatemessages.yml

+4-4
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
- name: Install requirements
2222
run: |
2323
sudo apt-get install gettext
24-
curl -O https://artifacts.crowdin.com/repo/deb/crowdin.deb
24+
wget https://artifacts.crowdin.com/repo/deb/crowdin3.deb -O crowdin.deb
2525
sudo dpkg -i crowdin.deb
2626
pip install -r requirements.txt
2727
pip install pymysql
@@ -32,10 +32,10 @@ jobs:
3232
python manage.py makemessages -l en -d djangojs
3333
- name: Download strings from Crowdin
3434
env:
35-
CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_TOKEN }}
35+
CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_V2_TOKEN }}
3636
run: |
3737
cat > crowdin.yaml <<EOF
38-
project_identifier: dmoj
38+
project_id: 142963
3939
4040
files:
4141
- source: /locale/en/LC_MESSAGES/django.po
@@ -53,7 +53,7 @@ jobs:
5353
zh-TW: zh_Hant
5454
sr-CS: sr_Latn
5555
EOF
56-
echo "api_key: ${CROWDIN_API_TOKEN}" >> crowdin.yaml
56+
echo "api_token: ${CROWDIN_API_TOKEN}" >> crowdin.yaml
5757
crowdin download
5858
rm crowdin.yaml
5959
- name: Cleanup

dmoj/settings.py

+4
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@
129129
}
130130
DMOJ_SELECT2_THEME = 'dmoj'
131131

132+
DMOJ_ENABLE_COMMENTS = True
133+
DMOJ_ENABLE_SOCIAL = True
134+
132135
MARKDOWN_STYLES = {}
133136
MARKDOWN_DEFAULT_STYLE = {}
134137

@@ -398,6 +401,7 @@
398401
('hr', _('Croatian')),
399402
('hu', _('Hungarian')),
400403
('ja', _('Japanese')),
404+
('kk', _('Kazakh')),
401405
('ko', _('Korean')),
402406
('pt', _('Brazilian Portuguese')),
403407
('ro', _('Romanian')),

dmoj/urls.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666

6767
path('2fa/', two_factor.TwoFactorLoginView.as_view(), name='login_2fa'),
6868
path('2fa/enable/', two_factor.TOTPEnableView.as_view(), name='enable_2fa'),
69-
path('2fa/refresh/', two_factor.TOTPRefreshView.as_view(), name='refresh_2fa'),
69+
path('2fa/edit/', two_factor.TOTPEditView.as_view(), name='edit_2fa'),
7070
path('2fa/disable/', two_factor.TOTPDisableView.as_view(), name='disable_2fa'),
7171
path('2fa/webauthn/attest/', two_factor.WebAuthnAttestationView.as_view(), name='webauthn_attest'),
7272
path('2fa/webauthn/assert/', two_factor.WebAuthnAttestView.as_view(), name='webauthn_assert'),

judge/jinja2/markdown/__init__.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,9 @@ def strip_paragraphs_tags(tree):
154154
parent = p.getparent()
155155
prev = p.getprevious()
156156
if prev is not None:
157-
prev.tail = (prev.tail or '') + p.text
157+
prev.tail = (prev.tail or '') + (p.text or '')
158158
else:
159-
parent.text = (parent.text or '') + p.text
159+
parent.text = (parent.text or '') + (p.text or '')
160160
parent.remove(p)
161161

162162

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 3.2.21 on 2024-01-06 04:33
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('judge', '0144_submission_index_cleanup'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='problemtestcase',
15+
name='batch_dependencies',
16+
field=models.TextField(blank=True, help_text='batch dependencies as a comma-separated list of integers', verbose_name='batch dependencies'),
17+
),
18+
]

judge/models/problem_data.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,6 @@ class ProblemTestCase(models.Model):
9393
output_limit = models.IntegerField(verbose_name=_('output limit length'), blank=True, null=True)
9494
checker = models.CharField(max_length=10, verbose_name=_('checker'), choices=CHECKERS, blank=True)
9595
checker_args = models.TextField(verbose_name=_('checker arguments'), blank=True,
96-
help_text=_('Checker arguments as a JSON object.'))
96+
help_text=_('checker arguments as a JSON object'))
97+
batch_dependencies = models.TextField(verbose_name=_('batch dependencies'), blank=True,
98+
help_text=_('batch dependencies as a comma-separated list of integers'))

judge/utils/problem_data.py

+18
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def __init__(self, problem, data, cases, files):
5757
def make_init(self):
5858
cases = []
5959
batch = None
60+
batch_count = 0
6061

6162
def end_batch():
6263
if not batch['batched']:
@@ -109,14 +110,31 @@ def make_checker(case):
109110
case.save(update_fields=('checker_args', 'is_pretest'))
110111
(batch['batched'] if batch else cases).append(data)
111112
elif case.type == 'S':
113+
batch_count += 1
112114
if batch:
113115
end_batch()
114116
if case.points is None:
115117
raise ProblemDataError(_('Batch start case #%d requires points.') % i)
118+
dependencies = []
119+
if case.batch_dependencies.strip():
120+
try:
121+
dependencies = list(map(int, case.batch_dependencies.split(',')))
122+
except ValueError:
123+
raise ProblemDataError(
124+
_('Dependencies must be a comma-separated list of integers for batch start case #%d.') % i,
125+
)
126+
for batch_number in dependencies:
127+
if batch_number >= batch_count:
128+
raise ProblemDataError(
129+
_('Dependencies must depend on previous batches for batch start case #%d.') % i,
130+
)
131+
elif batch_number < 1:
132+
raise ProblemDataError(_('Dependencies must be positive for batch start case #%d.') % i)
116133
batch = {
117134
'points': case.points,
118135
'batched': [],
119136
'is_pretest': case.is_pretest,
137+
'dependencies': dependencies,
120138
}
121139
if case.generator_args:
122140
batch['generator_args'] = case.generator_args.splitlines()

judge/views/blog.py

+2
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ def get_context_data(self, **kwargs):
107107
self.object.summary or self.object.content, 'blog')
108108
context['meta_description'] = metadata[0]
109109
context['og_image'] = self.object.og_image or metadata[1]
110+
context['enable_comments'] = settings.DMOJ_ENABLE_COMMENTS
111+
context['enable_social'] = settings.DMOJ_ENABLE_SOCIAL
110112

111113
return context
112114

judge/views/contests.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,8 @@ def get_context_data(self, **kwargs):
293293
problem_count=Count('id'),
294294
),
295295
)
296+
context['enable_comments'] = settings.DMOJ_ENABLE_COMMENTS
297+
context['enable_social'] = settings.DMOJ_ENABLE_SOCIAL
296298
return context
297299

298300

@@ -677,7 +679,8 @@ def base_contest_ranking_list(contest, problems, queryset):
677679
def contest_ranking_list(contest, problems):
678680
return base_contest_ranking_list(contest, problems, contest.users.filter(virtual=0)
679681
.prefetch_related('user__organizations')
680-
.order_by('is_disqualified', '-score', 'cumtime', 'tiebreaker'))
682+
.annotate(submission_cnt=Count('submission'))
683+
.order_by('is_disqualified', '-score', 'cumtime', 'tiebreaker', '-submission_cnt'))
681684

682685

683686
def get_contest_ranking_list(request, contest, participation=None, ranking_list=contest_ranking_list,

judge/views/problem.py

+2
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ def get_context_data(self, **kwargs):
127127
raise Http404()
128128
context['solution'] = solution
129129
context['has_solved_problem'] = self.object.id in self.get_completed_problems()
130+
context['enable_comments'] = settings.DMOJ_ENABLE_COMMENTS
130131
return context
131132

132133
def get_comment_page(self):
@@ -201,6 +202,7 @@ def get_context_data(self, **kwargs):
201202
context['description'], 'problem')
202203
context['meta_description'] = self.object.summary or metadata[0]
203204
context['og_image'] = self.object.og_image or metadata[1]
205+
context['enable_comments'] = settings.DMOJ_ENABLE_COMMENTS
204206

205207
context['vote_perm'] = self.object.vote_permission_for_user(user)
206208
if context['vote_perm'].can_vote():

judge/views/problem_data.py

+35-12
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import mimetypes
33
import os
44
from itertools import chain
5+
from typing import List
56
from zipfile import BadZipfile, ZipFile
67

78
from django.conf import settings
@@ -44,7 +45,19 @@ class ProblemDataForm(ModelForm):
4445
def clean_zipfile(self):
4546
if hasattr(self, 'zip_valid') and not self.zip_valid:
4647
raise ValidationError(_('Your zip file is invalid!'))
47-
return self.cleaned_data['zipfile']
48+
49+
zipfile = self.cleaned_data['zipfile']
50+
if zipfile and not zipfile.name.endswith('.zip'):
51+
raise ValidationError(_("Zip files must end in '.zip'"))
52+
53+
return zipfile
54+
55+
def clean_generator(self):
56+
generator = self.cleaned_data['generator']
57+
if generator and generator.name == 'init.yml':
58+
raise ValidationError(_('Generators must not be named init.yml.'))
59+
60+
return generator
4861

4962
clean_checker_args = checker_args_cleaner
5063

@@ -62,10 +75,11 @@ class ProblemCaseForm(ModelForm):
6275

6376
class Meta:
6477
model = ProblemTestCase
65-
fields = ('order', 'type', 'input_file', 'output_file', 'points',
66-
'is_pretest', 'output_limit', 'output_prefix', 'checker', 'checker_args', 'generator_args')
78+
fields = ('order', 'type', 'input_file', 'output_file', 'points', 'is_pretest', 'output_limit',
79+
'output_prefix', 'checker', 'checker_args', 'generator_args', 'batch_dependencies')
6780
widgets = {
6881
'generator_args': HiddenInput,
82+
'batch_dependencies': HiddenInput,
6983
'type': Select(attrs={'style': 'width: 100%'}),
7084
'points': NumberInput(attrs={'style': 'width: 4em'}),
7185
'output_prefix': NumberInput(attrs={'style': 'width: 4.5em'}),
@@ -157,7 +171,7 @@ def get_case_formset(self, files, post=False):
157171
return ProblemCaseFormSet(data=self.request.POST if post else None, prefix='cases', valid_files=files,
158172
queryset=ProblemTestCase.objects.filter(dataset_id=self.object.pk).order_by('order'))
159173

160-
def get_valid_files(self, data, post=False):
174+
def get_valid_files(self, data, post=False) -> List[str]:
161175
try:
162176
if post and 'problem-data-zipfile-clear' in self.request.POST:
163177
return []
@@ -166,26 +180,35 @@ def get_valid_files(self, data, post=False):
166180
elif data.zipfile:
167181
return ZipFile(data.zipfile.path).namelist()
168182
except BadZipfile:
169-
return []
183+
raise
170184
return []
171185

172186
def get_context_data(self, **kwargs):
173187
context = super(ProblemDataView, self).get_context_data(**kwargs)
188+
valid_files = []
174189
if 'data_form' not in context:
175190
context['data_form'] = self.get_data_form()
176-
valid_files = context['valid_files'] = self.get_valid_files(context['data_form'].instance)
177-
context['data_form'].zip_valid = valid_files is not False
178-
context['cases_formset'] = self.get_case_formset(valid_files)
179-
context['valid_files_json'] = mark_safe(json.dumps(context['valid_files']))
180-
context['valid_files'] = set(context['valid_files'])
191+
try:
192+
valid_files = self.get_valid_files(context['data_form'].instance)
193+
except BadZipfile:
194+
pass
195+
context['valid_files'] = set(valid_files)
196+
context['valid_files_json'] = mark_safe(json.dumps(valid_files))
197+
198+
context['cases_formset'] = self.get_case_formset(valid_files)
181199
context['all_case_forms'] = chain(context['cases_formset'], [context['cases_formset'].empty_form])
182200
return context
183201

184202
def post(self, request, *args, **kwargs):
185203
self.object = problem = self.get_object()
186204
data_form = self.get_data_form(post=True)
187-
valid_files = self.get_valid_files(data_form.instance, post=True)
188-
data_form.zip_valid = valid_files is not False
205+
try:
206+
valid_files = self.get_valid_files(data_form.instance, post=True)
207+
data_form.zip_valid = True
208+
except BadZipfile:
209+
valid_files = []
210+
data_form.zip_valid = False
211+
189212
cases_formset = self.get_case_formset(valid_files, post=True)
190213
if data_form.is_valid() and cases_formset.is_valid():
191214
data = data_form.save()

judge/views/two_factor.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ class TOTPEnableView(TOTPView):
5050
title = gettext_lazy('Enable Two-factor Authentication')
5151
form_class = TOTPEnableForm
5252
template_name = 'registration/totp_enable.html'
53-
is_refresh = False
53+
is_edit = False
5454

5555
def get(self, request, *args, **kwargs):
5656
profile = self.profile
@@ -76,9 +76,9 @@ def post(self, request, *args, **kwargs):
7676
def get_context_data(self, **kwargs):
7777
context = super(TOTPEnableView, self).get_context_data(**kwargs)
7878
context['totp_key'] = self.request.session['totp_enable_key']
79-
context['scratch_codes'] = [] if self.is_refresh else json.loads(self.profile.scratch_codes)
79+
context['scratch_codes'] = [] if self.is_edit else json.loads(self.profile.scratch_codes)
8080
context['qr_code'] = self.render_qr_code(self.request.user.username, context['totp_key'])
81-
context['is_refresh'] = self.is_refresh
81+
context['is_edit'] = self.is_edit
8282
context['is_hardcore'] = settings.DMOJ_2FA_HARDCORE
8383
return context
8484

@@ -106,9 +106,9 @@ def render_qr_code(cls, username, key):
106106
return 'data:image/png;base64,' + base64.b64encode(buf.getvalue()).decode('ascii')
107107

108108

109-
class TOTPRefreshView(TOTPEnableView):
110-
title = gettext_lazy('Refresh Two-factor Authentication')
111-
is_refresh = True
109+
class TOTPEditView(TOTPEnableView):
110+
title = gettext_lazy('Edit Two-factor Authentication')
111+
is_edit = True
112112

113113
def check_skip(self):
114114
return not self.profile.is_totp_enabled

0 commit comments

Comments
 (0)