diff --git a/.ci.settings.py b/.ci.settings.py index 637c918399..49b693ef46 100644 --- a/.ci.settings.py +++ b/.ci.settings.py @@ -2,11 +2,7 @@ STATICFILES_FINDERS += ('compressor.finders.CompressorFinder',) STATIC_ROOT = os.path.join(BASE_DIR, 'static') -CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache' - } -} +CACHES = {'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'}} DATABASES = { 'default': { diff --git a/django_ace/widgets.py b/django_ace/widgets.py index 2f521bf7f7..c0b90d7abd 100644 --- a/django_ace/widgets.py +++ b/django_ace/widgets.py @@ -11,8 +11,17 @@ class AceWidget(forms.Textarea): - def __init__(self, mode=None, theme=None, wordwrap=False, width='100%', height='300px', - no_ace_media=False, *args, **kwargs): + def __init__( + self, + mode=None, + theme=None, + wordwrap=False, + width='100%', + height='300px', + no_ace_media=False, + *args, + **kwargs + ): self.mode = mode self.theme = theme self.wordwrap = wordwrap @@ -47,13 +56,17 @@ def render(self, name, value, attrs=None, renderer=None): if self.wordwrap: ace_attrs['data-wordwrap'] = 'true' - attrs.update(style='width: 100%; min-width: 100%; max-width: 100%; resize: none') + attrs.update( + style='width: 100%; min-width: 100%; max-width: 100%; resize: none' + ) textarea = super(AceWidget, self).render(name, value, attrs) html = '
%s' % (flatatt(ace_attrs), textarea) # add toolbar - html = ('
' - '
%s
') % html + html = ( + '
' + '
%s
' + ) % html return mark_safe(html) diff --git a/dmoj/celery.py b/dmoj/celery.py index e1da640642..7f7ac1a0fd 100644 --- a/dmoj/celery.py +++ b/dmoj/celery.py @@ -7,6 +7,7 @@ app = Celery('dmoj') from django.conf import settings # noqa: E402, I202, django must be imported here + app.config_from_object(settings, namespace='CELERY') if hasattr(settings, 'CELERY_BROKER_URL_SECRET'): @@ -23,5 +24,10 @@ @task_failure.connect() def celery_failure_log(sender, task_id, exception, traceback, *args, **kwargs): - logger.error('Celery Task %s: %s on %s', sender.name, task_id, socket.gethostname(), # noqa: G201 - exc_info=(type(exception), exception, traceback)) + logger.error( + 'Celery Task %s: %s on %s', + sender.name, + task_id, + socket.gethostname(), # noqa: G201 + exc_info=(type(exception), exception, traceback), + ) diff --git a/dmoj/settings.py b/dmoj/settings.py index cea555a59e..231104c45e 100644 --- a/dmoj/settings.py +++ b/dmoj/settings.py @@ -46,7 +46,7 @@ # Refer to https://dmoj.ca/post/103-point-system-rework DMOJ_PP_STEP = 0.95 DMOJ_PP_ENTRIES = 100 -DMOJ_PP_BONUS_FUNCTION = lambda n: 300 * (1 - 0.997 ** n) # noqa: E731 +DMOJ_PP_BONUS_FUNCTION = lambda n: 300 * (1 - 0.997**n) # noqa: E731 ACE_URL = '//cdnjs.cloudflare.com/ajax/libs/ace/1.1.3' SELECT2_JS_URL = '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/select2.min.js' @@ -64,11 +64,26 @@ DMOJ_PROBLEM_MIN_MEMORY_LIMIT = 0 # kilobytes DMOJ_PROBLEM_MAX_MEMORY_LIMIT = 1048576 # kilobytes DMOJ_PROBLEM_MIN_PROBLEM_POINTS = 0 -DMOJ_PROBLEM_MIN_USER_POINTS_VOTE = 1 # when voting on problem, minimum point value user can select -DMOJ_PROBLEM_MAX_USER_POINTS_VOTE = 50 # when voting on problem, maximum point value user can select +DMOJ_PROBLEM_MIN_USER_POINTS_VOTE = ( + 1 # when voting on problem, minimum point value user can select +) +DMOJ_PROBLEM_MAX_USER_POINTS_VOTE = ( + 50 # when voting on problem, maximum point value user can select +) DMOJ_PROBLEM_HOT_PROBLEM_COUNT = 7 -DMOJ_PROBLEM_STATEMENT_DISALLOWED_CHARACTERS = {'“', '”', '‘', '’', '−', 'ff', 'fi', 'fl', 'ffi', 'ffl'} +DMOJ_PROBLEM_STATEMENT_DISALLOWED_CHARACTERS = { + '“', + '”', + '‘', + '’', + '−', + 'ff', + 'fi', + 'fl', + 'ffi', + 'ffl', +} DMOJ_RATING_COLORS = True DMOJ_EMAIL_THROTTLING = (10, 60) @@ -158,7 +173,9 @@ INLINE_JQUERY = True INLINE_FONTAWESOME = True JQUERY_JS = '//ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js' -FONTAWESOME_CSS = '//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css' +FONTAWESOME_CSS = ( + '//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css' +) DMOJ_CANONICAL = '' # Application definition @@ -358,7 +375,8 @@ 'trim_blocks': True, 'lstrip_blocks': True, 'translation_engine': 'judge.utils.safe_translations', - 'extensions': DEFAULT_EXTENSIONS + [ + 'extensions': DEFAULT_EXTENSIONS + + [ 'compressor.contrib.jinja2ext.CompressorExtension', 'judge.jinja2.DMOJExtension', 'judge.jinja2.spaceless.SpacelessExtension', @@ -410,15 +428,76 @@ ] BLEACH_USER_SAFE_TAGS = [ - 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', - 'b', 'i', 'strong', 'em', 'tt', 'del', 'kbd', 's', 'abbr', 'cite', 'mark', 'q', 'samp', 'small', - 'u', 'var', 'wbr', 'dfn', 'ruby', 'rb', 'rp', 'rt', 'rtc', 'sub', 'sup', 'time', 'data', - 'p', 'br', 'pre', 'span', 'div', 'blockquote', 'code', 'hr', - 'ul', 'ol', 'li', 'dd', 'dl', 'dt', 'address', 'section', 'details', 'summary', - 'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td', 'caption', 'colgroup', 'col', 'tfoot', - 'img', 'audio', 'video', 'source', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'b', + 'i', + 'strong', + 'em', + 'tt', + 'del', + 'kbd', + 's', + 'abbr', + 'cite', + 'mark', + 'q', + 'samp', + 'small', + 'u', + 'var', + 'wbr', + 'dfn', + 'ruby', + 'rb', + 'rp', + 'rt', + 'rtc', + 'sub', + 'sup', + 'time', + 'data', + 'p', + 'br', + 'pre', + 'span', + 'div', + 'blockquote', + 'code', + 'hr', + 'ul', + 'ol', + 'li', + 'dd', + 'dl', + 'dt', + 'address', + 'section', + 'details', + 'summary', + 'table', + 'thead', + 'tbody', + 'tfoot', + 'tr', + 'th', + 'td', + 'caption', + 'colgroup', + 'col', + 'tfoot', + 'img', + 'audio', + 'video', + 'source', 'a', - 'style', 'noscript', 'center', + 'style', + 'noscript', + 'center', ] BLEACH_USER_SAFE_ATTRS = { @@ -432,7 +511,18 @@ 'td': ['colspan', 'rowspan'], 'th': ['colspan', 'rowspan'], 'audio': ['autoplay', 'controls', 'crossorigin', 'muted', 'loop', 'preload', 'src'], - 'video': ['autoplay', 'controls', 'crossorigin', 'height', 'muted', 'loop', 'poster', 'preload', 'src', 'width'], + 'video': [ + 'autoplay', + 'controls', + 'crossorigin', + 'height', + 'muted', + 'loop', + 'poster', + 'preload', + 'src', + 'width', + ], 'source': ['src', 'srcset', 'type'], 'li': ['value'], } @@ -531,7 +621,9 @@ EVENT_DAEMON_POLL = '/channels/' EVENT_DAEMON_KEY = None EVENT_DAEMON_AMQP_EXCHANGE = 'dmoj-events' -EVENT_DAEMON_SUBMISSION_KEY = '6Sdmkx^%pk@GsifDfXcwX*Y7LRF%RGT8vmFpSxFBT$fwS7trc8raWfN#CSfQuKApx&$B#Gh2L7p%W!Ww' +EVENT_DAEMON_SUBMISSION_KEY = ( + '6Sdmkx^%pk@GsifDfXcwX*Y7LRF%RGT8vmFpSxFBT$fwS7trc8raWfN#CSfQuKApx&$B#Gh2L7p%W!Ww' +) # Internationalization # https://docs.djangoproject.com/en/3.2/topics/i18n/ diff --git a/dmoj/urls.py b/dmoj/urls.py index 08b7812ed7..2976a6a716 100644 --- a/dmoj/urls.py +++ b/dmoj/urls.py @@ -10,69 +10,164 @@ from django.views.generic import RedirectView from martor.views import markdown_search_user -from judge.feed import AtomBlogFeed, AtomCommentFeed, AtomProblemFeed, BlogFeed, CommentFeed, ProblemFeed +from judge.feed import ( + AtomBlogFeed, + AtomCommentFeed, + AtomProblemFeed, + BlogFeed, + CommentFeed, + ProblemFeed, +) from judge.sitemap import sitemaps -from judge.views import TitledTemplateView, api, blog, comment, contests, language, license, mailgun, organization, \ - preview, problem, problem_manage, ranked_submission, register, stats, status, submission, tasks, ticket, \ - two_factor, user, widgets -from judge.views.problem_data import ProblemDataView, ProblemSubmissionDiff, \ - problem_data_file, problem_init_view +from judge.views import ( + TitledTemplateView, + api, + blog, + comment, + contests, + language, + license, + mailgun, + organization, + preview, + problem, + problem_manage, + ranked_submission, + register, + stats, + status, + submission, + tasks, + ticket, + two_factor, + user, + widgets, +) +from judge.views.problem_data import ( + ProblemDataView, + ProblemSubmissionDiff, + problem_data_file, + problem_init_view, +) from judge.views.register import ActivationView, RegistrationView -from judge.views.select2 import AssigneeSelect2View, ClassSelect2View, CommentSelect2View, ContestSelect2View, \ - ContestUserSearchSelect2View, OrganizationSelect2View, ProblemSelect2View, TicketUserSelect2View, \ - UserSearchSelect2View, UserSelect2View +from judge.views.select2 import ( + AssigneeSelect2View, + ClassSelect2View, + CommentSelect2View, + ContestSelect2View, + ContestUserSearchSelect2View, + OrganizationSelect2View, + ProblemSelect2View, + TicketUserSelect2View, + UserSearchSelect2View, + UserSelect2View, +) from judge.views.widgets import martor_image_uploader admin.autodiscover() register_patterns = [ - path('activate/complete/', - TitledTemplateView.as_view(template_name='registration/activation_complete.html', - title=_('Activation Successful!')), - name='registration_activation_complete'), + path( + 'activate/complete/', + TitledTemplateView.as_view( + template_name='registration/activation_complete.html', + title=_('Activation Successful!'), + ), + name='registration_activation_complete', + ), # Let's use , because a bad activation key should still get to the view; # that way, it can return a sensible "invalid key" message instead of a confusing 404. - path('activate//', ActivationView.as_view(), name='registration_activate'), + path( + 'activate//', + ActivationView.as_view(), + name='registration_activate', + ), path('register/', RegistrationView.as_view(), name='registration_register'), - path('register/complete/', - TitledTemplateView.as_view(template_name='registration/registration_complete.html', - title=_('Registration Completed')), - name='registration_complete'), - path('register/closed/', - TitledTemplateView.as_view(template_name='registration/registration_closed.html', - title=_('Registration Not Allowed')), - name='registration_disallowed'), + path( + 'register/complete/', + TitledTemplateView.as_view( + template_name='registration/registration_complete.html', + title=_('Registration Completed'), + ), + name='registration_complete', + ), + path( + 'register/closed/', + TitledTemplateView.as_view( + template_name='registration/registration_closed.html', + title=_('Registration Not Allowed'), + ), + name='registration_disallowed', + ), path('login/', user.CustomLoginView.as_view(), name='auth_login'), path('logout/', user.UserLogoutView.as_view(), name='auth_logout'), - path('password/change/', user.CustomPasswordChangeView.as_view(), name='password_change'), - path('password/change/done/', auth_views.PasswordChangeDoneView.as_view( - template_name='registration/password_change_done.html', - ), name='password_change_done'), - path('password/reset/', user.CustomPasswordResetView.as_view(), name='password_reset'), - re_path(r'^password/reset/confirm/(?P[0-9A-Za-z]+)-(?P.+)/$', - auth_views.PasswordResetConfirmView.as_view( - template_name='registration/password_reset_confirm.html', - ), name='password_reset_confirm'), - path('password/reset/complete/', auth_views.PasswordResetCompleteView.as_view( - template_name='registration/password_reset_complete.html', - ), name='password_reset_complete'), - path('password/reset/done/', auth_views.PasswordResetDoneView.as_view( - template_name='registration/password_reset_done.html', - ), name='password_reset_done'), + path( + 'password/change/', + user.CustomPasswordChangeView.as_view(), + name='password_change', + ), + path( + 'password/change/done/', + auth_views.PasswordChangeDoneView.as_view( + template_name='registration/password_change_done.html', + ), + name='password_change_done', + ), + path( + 'password/reset/', user.CustomPasswordResetView.as_view(), name='password_reset' + ), + re_path( + r'^password/reset/confirm/(?P[0-9A-Za-z]+)-(?P.+)/$', + auth_views.PasswordResetConfirmView.as_view( + template_name='registration/password_reset_confirm.html', + ), + name='password_reset_confirm', + ), + path( + 'password/reset/complete/', + auth_views.PasswordResetCompleteView.as_view( + template_name='registration/password_reset_complete.html', + ), + name='password_reset_complete', + ), + path( + 'password/reset/done/', + auth_views.PasswordResetDoneView.as_view( + template_name='registration/password_reset_done.html', + ), + name='password_reset_done', + ), path('social/error/', register.social_auth_error, name='social_auth_error'), path('email/change/', user.EmailChangeRequestView.as_view(), name='email_change'), - path('email/change/activate//', - user.EmailChangeActivateView.as_view(), name='email_change_activate'), - + path( + 'email/change/activate//', + user.EmailChangeActivateView.as_view(), + name='email_change_activate', + ), path('2fa/', two_factor.TwoFactorLoginView.as_view(), name='login_2fa'), path('2fa/enable/', two_factor.TOTPEnableView.as_view(), name='enable_2fa'), path('2fa/edit/', two_factor.TOTPEditView.as_view(), name='edit_2fa'), path('2fa/disable/', two_factor.TOTPDisableView.as_view(), name='disable_2fa'), - path('2fa/webauthn/attest/', two_factor.WebAuthnAttestationView.as_view(), name='webauthn_attest'), - path('2fa/webauthn/assert/', two_factor.WebAuthnAttestView.as_view(), name='webauthn_assert'), - path('2fa/webauthn/delete/', two_factor.WebAuthnDeleteView.as_view(), name='webauthn_delete'), - path('2fa/scratchcode/generate/', user.generate_scratch_codes, name='generate_scratch_codes'), - + path( + '2fa/webauthn/attest/', + two_factor.WebAuthnAttestationView.as_view(), + name='webauthn_attest', + ), + path( + '2fa/webauthn/assert/', + two_factor.WebAuthnAttestView.as_view(), + name='webauthn_assert', + ), + path( + '2fa/webauthn/delete/', + two_factor.WebAuthnDeleteView.as_view(), + name='webauthn_delete', + ), + path( + '2fa/scratchcode/generate/', + user.generate_scratch_codes, + name='generate_scratch_codes', + ), path('api/token/generate/', user.generate_api_token, name='generate_api_token'), path('api/token/remove/', user.remove_api_token, name='remove_api_token'), ] @@ -85,305 +180,782 @@ def exception(request): def paged_list_view(view, name): - return include([ - path('', view.as_view(), name=name), - path('', view.as_view(), name=name), - ]) + return include( + [ + path('', view.as_view(), name=name), + path('', view.as_view(), name=name), + ] + ) urlpatterns = [ - path('', blog.PostList.as_view(template_name='home.html', title=_('Home')), kwargs={'page': 1}, name='home'), + path( + '', + blog.PostList.as_view(template_name='home.html', title=_('Home')), + kwargs={'page': 1}, + name='home', + ), path('500/', exception), path('admin/', admin.site.urls), path('i18n/', include('django.conf.urls.i18n')), path('accounts/', include(register_patterns)), path('', include('social_django.urls')), - path('problems/', problem.ProblemList.as_view(), name='problem_list'), path('problems/random/', problem.RandomProblem.as_view(), name='problem_random'), - - path('problem/', include([ - path('', problem.ProblemDetail.as_view(), name='problem_detail'), - path('/editorial', problem.ProblemSolution.as_view(), name='problem_editorial'), - path('/pdf', problem.ProblemPdfView.as_view(), name='problem_pdf'), - path('/pdf/', problem.ProblemPdfView.as_view(), name='problem_pdf'), - path('/clone', problem.ProblemClone.as_view(), name='problem_clone'), - path('/submit', problem.ProblemSubmit.as_view(), name='problem_submit'), - path('/resubmit/', problem.ProblemSubmit.as_view(), name='problem_submit'), - - path('/rank/', paged_list_view(ranked_submission.RankedSubmissions, 'ranked_submissions')), - path('/submissions/', paged_list_view(submission.ProblemSubmissions, 'chronological_submissions')), - path('/submissions//', paged_list_view(submission.UserProblemSubmissions, 'user_submissions')), - - path('/', lambda _, problem: HttpResponsePermanentRedirect(reverse('problem_detail', args=[problem]))), - - path('/test_data', ProblemDataView.as_view(), name='problem_data'), - path('/test_data/init', problem_init_view, name='problem_data_init'), - path('/test_data/diff', ProblemSubmissionDiff.as_view(), name='problem_submission_diff'), - path('/data/', problem_data_file, name='problem_data_file'), - - path('/tickets', ticket.ProblemTicketListView.as_view(), name='problem_ticket_list'), - path('/tickets/new', ticket.NewProblemTicketView.as_view(), name='new_problem_ticket'), - - path('/vote', problem.ProblemVote.as_view(), name='problem_vote'), - path('/vote/delete', problem.DeleteProblemVote.as_view(), name='delete_problem_vote'), - path('/vote/stats', problem.ProblemVoteStats.as_view(), name='problem_vote_stats'), - - path('/manage/submission', include([ - path('', problem_manage.ManageProblemSubmissionView.as_view(), name='problem_manage_submissions'), - path('/rejudge', problem_manage.RejudgeSubmissionsView.as_view(), name='problem_submissions_rejudge'), - path('/rejudge/preview', problem_manage.PreviewRejudgeSubmissionsView.as_view(), - name='problem_submissions_rejudge_preview'), - path('/rejudge/success/', problem_manage.rejudge_success, - name='problem_submissions_rejudge_success'), - path('/rescore/all', problem_manage.RescoreAllSubmissionsView.as_view(), - name='problem_submissions_rescore_all'), - path('/rescore/success/', problem_manage.rescore_success, - name='problem_submissions_rescore_success'), - ])), - ])), - + path( + 'problem/', + include( + [ + path('', problem.ProblemDetail.as_view(), name='problem_detail'), + path( + '/editorial', + problem.ProblemSolution.as_view(), + name='problem_editorial', + ), + path('/pdf', problem.ProblemPdfView.as_view(), name='problem_pdf'), + path( + '/pdf/', + problem.ProblemPdfView.as_view(), + name='problem_pdf', + ), + path('/clone', problem.ProblemClone.as_view(), name='problem_clone'), + path('/submit', problem.ProblemSubmit.as_view(), name='problem_submit'), + path( + '/resubmit/', + problem.ProblemSubmit.as_view(), + name='problem_submit', + ), + path( + '/rank/', + paged_list_view( + ranked_submission.RankedSubmissions, 'ranked_submissions' + ), + ), + path( + '/submissions/', + paged_list_view( + submission.ProblemSubmissions, 'chronological_submissions' + ), + ), + path( + '/submissions//', + paged_list_view( + submission.UserProblemSubmissions, 'user_submissions' + ), + ), + path( + '/', + lambda _, problem: HttpResponsePermanentRedirect( + reverse('problem_detail', args=[problem]) + ), + ), + path('/test_data', ProblemDataView.as_view(), name='problem_data'), + path('/test_data/init', problem_init_view, name='problem_data_init'), + path( + '/test_data/diff', + ProblemSubmissionDiff.as_view(), + name='problem_submission_diff', + ), + path('/data/', problem_data_file, name='problem_data_file'), + path( + '/tickets', + ticket.ProblemTicketListView.as_view(), + name='problem_ticket_list', + ), + path( + '/tickets/new', + ticket.NewProblemTicketView.as_view(), + name='new_problem_ticket', + ), + path('/vote', problem.ProblemVote.as_view(), name='problem_vote'), + path( + '/vote/delete', + problem.DeleteProblemVote.as_view(), + name='delete_problem_vote', + ), + path( + '/vote/stats', + problem.ProblemVoteStats.as_view(), + name='problem_vote_stats', + ), + path( + '/manage/submission', + include( + [ + path( + '', + problem_manage.ManageProblemSubmissionView.as_view(), + name='problem_manage_submissions', + ), + path( + '/rejudge', + problem_manage.RejudgeSubmissionsView.as_view(), + name='problem_submissions_rejudge', + ), + path( + '/rejudge/preview', + problem_manage.PreviewRejudgeSubmissionsView.as_view(), + name='problem_submissions_rejudge_preview', + ), + path( + '/rejudge/success/', + problem_manage.rejudge_success, + name='problem_submissions_rejudge_success', + ), + path( + '/rescore/all', + problem_manage.RescoreAllSubmissionsView.as_view(), + name='problem_submissions_rescore_all', + ), + path( + '/rescore/success/', + problem_manage.rescore_success, + name='problem_submissions_rescore_success', + ), + ] + ), + ), + ] + ), + ), path('submissions/', paged_list_view(submission.AllSubmissions, 'all_submissions')), - path('submissions/user//', paged_list_view(submission.AllUserSubmissions, 'all_user_submissions')), - - path('src/', submission.SubmissionSource.as_view(), name='submission_source'), - path('src//raw', submission.SubmissionSourceRaw.as_view(), name='submission_source_raw'), - - path('submission/', include([ - path('', submission.SubmissionStatus.as_view(), name='submission_status'), - path('/abort', submission.abort_submission, name='submission_abort'), - ])), - - path('users/', include([ - path('', user.users, name='user_list'), - path('', lambda request, page: - HttpResponsePermanentRedirect('%s?page=%s' % (reverse('user_list'), page))), - path('find', user.user_ranking_redirect, name='user_ranking_redirect'), - ])), - + path( + 'submissions/user//', + paged_list_view(submission.AllUserSubmissions, 'all_user_submissions'), + ), + path( + 'src/', + submission.SubmissionSource.as_view(), + name='submission_source', + ), + path( + 'src//raw', + submission.SubmissionSourceRaw.as_view(), + name='submission_source_raw', + ), + path( + 'submission/', + include( + [ + path( + '', submission.SubmissionStatus.as_view(), name='submission_status' + ), + path('/abort', submission.abort_submission, name='submission_abort'), + ] + ), + ), + path( + 'users/', + include( + [ + path('', user.users, name='user_list'), + path( + '', + lambda request, page: HttpResponsePermanentRedirect( + '%s?page=%s' % (reverse('user_list'), page) + ), + ), + path('find', user.user_ranking_redirect, name='user_ranking_redirect'), + ] + ), + ), path('user', user.UserDashboard.as_view(), name='user_dashboard'), path('edit/profile/', user.edit_profile, name='user_edit_profile'), path('data/prepare/', user.UserPrepareData.as_view(), name='user_prepare_data'), path('data/download/', user.UserDownloadData.as_view(), name='user_download_data'), - path('user/', include([ - path('', user.UserDashboard.as_view(), name='user_dashboard'), - path('/solved', include([ - path('', user.UserProblemsPage.as_view(), name='user_problems'), - path('/ajax', user.UserPerformancePointsAjax.as_view(), name='user_pp_ajax'), - ])), - path('/submissions/', paged_list_view(submission.AllUserSubmissions, 'all_user_submissions_old')), - path('/submissions/', lambda _, user: - HttpResponsePermanentRedirect(reverse('all_user_submissions', args=[user]))), - - path('/', lambda _, user: HttpResponsePermanentRedirect(reverse('user_dashboard', args=[user]))), - ])), - + path( + 'user/', + include( + [ + path('', user.UserDashboard.as_view(), name='user_dashboard'), + path( + '/solved', + include( + [ + path( + '', + user.UserProblemsPage.as_view(), + name='user_problems', + ), + path( + '/ajax', + user.UserPerformancePointsAjax.as_view(), + name='user_pp_ajax', + ), + ] + ), + ), + path( + '/submissions/', + paged_list_view( + submission.AllUserSubmissions, 'all_user_submissions_old' + ), + ), + path( + '/submissions/', + lambda _, user: HttpResponsePermanentRedirect( + reverse('all_user_submissions', args=[user]) + ), + ), + path( + '/', + lambda _, user: HttpResponsePermanentRedirect( + reverse('user_dashboard', args=[user]) + ), + ), + ] + ), + ), path('comments/upvote/', comment.upvote_comment, name='comment_upvote'), path('comments/downvote/', comment.downvote_comment, name='comment_downvote'), path('comments/hide/', comment.comment_hide, name='comment_hide'), - path('comments//', include([ - path('edit', comment.CommentEdit.as_view(), name='comment_edit'), - path('history/ajax', comment.CommentRevisionAjax.as_view(), name='comment_revision_ajax'), - path('edit/ajax', comment.CommentEditAjax.as_view(), name='comment_edit_ajax'), - path('votes/ajax', comment.CommentVotesAjax.as_view(), name='comment_votes_ajax'), - path('render', comment.CommentContent.as_view(), name='comment_content'), - ])), - - path('contests/',contests.ContestList.as_view(), name='contest_list'), # if broken add $ to end of regex and make regex + path( + 'comments//', + include( + [ + path('edit', comment.CommentEdit.as_view(), name='comment_edit'), + path( + 'history/ajax', + comment.CommentRevisionAjax.as_view(), + name='comment_revision_ajax', + ), + path( + 'edit/ajax', + comment.CommentEditAjax.as_view(), + name='comment_edit_ajax', + ), + path( + 'votes/ajax', + comment.CommentVotesAjax.as_view(), + name='comment_votes_ajax', + ), + path( + 'render', comment.CommentContent.as_view(), name='comment_content' + ), + ] + ), + ), + path( + 'contests/', contests.ContestList.as_view(), name='contest_list' + ), # if broken add $ to end of regex and make regex path('contests.ics', contests.ContestICal.as_view(), name='contest_ical'), - path('contests///', contests.ContestCalendar.as_view(), name='contest_calendar'), - re_path(r'^contests/tag/(?P[a-z-]+)', include([ - path('', contests.ContestTagDetail.as_view(), name='contest_tag'), - path('/ajax', contests.ContestTagDetailAjax.as_view(), name='contest_tag_ajax'), - ])), - - path('contest/', include([ - path('', contests.ContestDetail.as_view(), name='contest_view'), - path('/moss', contests.ContestMossView.as_view(), name='contest_moss'), - path('/moss/delete', contests.ContestMossDelete.as_view(), name='contest_moss_delete'), - path('/clone', contests.ContestClone.as_view(), name='contest_clone'), - path('/ranking/', contests.ContestRanking.as_view(), name='contest_ranking'), - path('/ranking/ajax', contests.contest_ranking_ajax, name='contest_ranking_ajax'), - path('/register', contests.ContestRegister.as_view(), name='contest_register'), - path('/join', contests.ContestJoin.as_view(), name='contest_join'), - path('/leave', contests.ContestLeave.as_view(), name='contest_leave'), - path('/stats', contests.ContestStats.as_view(), name='contest_stats'), - - path('/rank//', - paged_list_view(ranked_submission.ContestRankedSubmission, 'contest_ranked_submissions')), - - path('/submissions//', - paged_list_view(submission.UserAllContestSubmissions, 'contest_all_user_submissions')), - path('/submissions///', - paged_list_view(submission.UserContestSubmissions, 'contest_user_submissions')), - - path('/participations', contests.ContestParticipationList.as_view(), name='contest_participation_own'), - path('/participations/', - contests.ContestParticipationList.as_view(), name='contest_participation'), - path('/participation/disqualify', contests.ContestParticipationDisqualify.as_view(), - name='contest_participation_disqualify'), - - path('/', lambda _, contest: HttpResponsePermanentRedirect(reverse('contest_view', args=[contest]))), - ])), - - path('organizations/', organization.OrganizationList.as_view(), name='organization_list'), - path('organization/-', include([ - path('', organization.OrganizationHome.as_view(), name='organization_home'), - path('/users', organization.OrganizationUsers.as_view(), name='organization_users'), - path('/join', organization.JoinOrganization.as_view(), name='join_organization'), - path('/leave', organization.LeaveOrganization.as_view(), name='leave_organization'), - path('/edit', organization.EditOrganization.as_view(), name='edit_organization'), - path('/kick', organization.KickUserWidgetView.as_view(), name='organization_user_kick'), - - path('/request', organization.RequestJoinOrganization.as_view(), name='request_organization'), - path('/request/', organization.OrganizationRequestDetail.as_view(), - name='request_organization_detail'), - path('/requests/', include([ - path('pending', organization.OrganizationRequestView.as_view(), name='organization_requests_pending'), - path('log', organization.OrganizationRequestLog.as_view(), name='organization_requests_log'), - path('approved', organization.OrganizationRequestLog.as_view(states=('A',), tab='approved'), - name='organization_requests_approved'), - path('rejected', organization.OrganizationRequestLog.as_view(states=('R',), tab='rejected'), - name='organization_requests_rejected'), - ])), - - path('/class/-', include([ - path('', organization.ClassHome.as_view(), name='class_home'), - path('/join', organization.RequestJoinClass.as_view(), name='class_join'), - ])), - - path('/', lambda _, pk, slug: HttpResponsePermanentRedirect(reverse('organization_home', args=[pk, slug]))), - ])), - + path( + 'contests///', + contests.ContestCalendar.as_view(), + name='contest_calendar', + ), + re_path( + r'^contests/tag/(?P[a-z-]+)', + include( + [ + path('', contests.ContestTagDetail.as_view(), name='contest_tag'), + path( + '/ajax', + contests.ContestTagDetailAjax.as_view(), + name='contest_tag_ajax', + ), + ] + ), + ), + path( + 'contest/', + include( + [ + path('', contests.ContestDetail.as_view(), name='contest_view'), + path('/moss', contests.ContestMossView.as_view(), name='contest_moss'), + path( + '/moss/delete', + contests.ContestMossDelete.as_view(), + name='contest_moss_delete', + ), + path('/clone', contests.ContestClone.as_view(), name='contest_clone'), + path( + '/ranking/', + contests.ContestRanking.as_view(), + name='contest_ranking', + ), + path( + '/ranking/ajax', + contests.contest_ranking_ajax, + name='contest_ranking_ajax', + ), + path( + '/register', + contests.ContestRegister.as_view(), + name='contest_register', + ), + path('/join', contests.ContestJoin.as_view(), name='contest_join'), + path('/leave', contests.ContestLeave.as_view(), name='contest_leave'), + path('/stats', contests.ContestStats.as_view(), name='contest_stats'), + path( + '/rank//', + paged_list_view( + ranked_submission.ContestRankedSubmission, + 'contest_ranked_submissions', + ), + ), + path( + '/submissions//', + paged_list_view( + submission.UserAllContestSubmissions, + 'contest_all_user_submissions', + ), + ), + path( + '/submissions///', + paged_list_view( + submission.UserContestSubmissions, 'contest_user_submissions' + ), + ), + path( + '/participations', + contests.ContestParticipationList.as_view(), + name='contest_participation_own', + ), + path( + '/participations/', + contests.ContestParticipationList.as_view(), + name='contest_participation', + ), + path( + '/participation/disqualify', + contests.ContestParticipationDisqualify.as_view(), + name='contest_participation_disqualify', + ), + path( + '/', + lambda _, contest: HttpResponsePermanentRedirect( + reverse('contest_view', args=[contest]) + ), + ), + ] + ), + ), + path( + 'organizations/', + organization.OrganizationList.as_view(), + name='organization_list', + ), + path( + 'organization/-', + include( + [ + path( + '', + organization.OrganizationHome.as_view(), + name='organization_home', + ), + path( + '/users', + organization.OrganizationUsers.as_view(), + name='organization_users', + ), + path( + '/join', + organization.JoinOrganization.as_view(), + name='join_organization', + ), + path( + '/leave', + organization.LeaveOrganization.as_view(), + name='leave_organization', + ), + path( + '/edit', + organization.EditOrganization.as_view(), + name='edit_organization', + ), + path( + '/kick', + organization.KickUserWidgetView.as_view(), + name='organization_user_kick', + ), + path( + '/request', + organization.RequestJoinOrganization.as_view(), + name='request_organization', + ), + path( + '/request/', + organization.OrganizationRequestDetail.as_view(), + name='request_organization_detail', + ), + path( + '/requests/', + include( + [ + path( + 'pending', + organization.OrganizationRequestView.as_view(), + name='organization_requests_pending', + ), + path( + 'log', + organization.OrganizationRequestLog.as_view(), + name='organization_requests_log', + ), + path( + 'approved', + organization.OrganizationRequestLog.as_view( + states=('A',), tab='approved' + ), + name='organization_requests_approved', + ), + path( + 'rejected', + organization.OrganizationRequestLog.as_view( + states=('R',), tab='rejected' + ), + name='organization_requests_rejected', + ), + ] + ), + ), + path( + '/class/-', + include( + [ + path( + '', organization.ClassHome.as_view(), name='class_home' + ), + path( + '/join', + organization.RequestJoinClass.as_view(), + name='class_join', + ), + ] + ), + ), + path( + '/', + lambda _, pk, slug: HttpResponsePermanentRedirect( + reverse('organization_home', args=[pk, slug]) + ), + ), + ] + ), + ), path('runtimes/', language.LanguageList.as_view(), name='runtime_list'), path('runtimes/matrix/', status.version_matrix, name='version_matrix'), path('status/', status.status_all, name='status_all'), - - path('api/v2/', include([ - path('contests', api.api_v2.APIContestList.as_view()), - path('contest/', api.api_v2.APIContestDetail.as_view()), - path('problems', api.api_v2.APIProblemList.as_view()), - path('problem/', api.api_v2.APIProblemDetail.as_view()), - path('users', api.api_v2.APIUserList.as_view()), - path('user/', api.api_v2.APIUserDetail.as_view()), - path('submissions', api.api_v2.APISubmissionList.as_view()), - path('submission/', api.api_v2.APISubmissionDetail.as_view()), - path('organizations', api.api_v2.APIOrganizationList.as_view()), - path('participations', api.api_v2.APIContestParticipationList.as_view()), - path('languages', api.api_v2.APILanguageList.as_view()), - path('judges', api.api_v2.APIJudgeList.as_view()), - ])), - + path( + 'api/v2/', + include( + [ + path('contests', api.api_v2.APIContestList.as_view()), + path('contest/', api.api_v2.APIContestDetail.as_view()), + path('problems', api.api_v2.APIProblemList.as_view()), + path('problem/', api.api_v2.APIProblemDetail.as_view()), + path('users', api.api_v2.APIUserList.as_view()), + path('user/', api.api_v2.APIUserDetail.as_view()), + path('submissions', api.api_v2.APISubmissionList.as_view()), + path( + 'submission/', + api.api_v2.APISubmissionDetail.as_view(), + ), + path('organizations', api.api_v2.APIOrganizationList.as_view()), + path( + 'participations', api.api_v2.APIContestParticipationList.as_view() + ), + path('languages', api.api_v2.APILanguageList.as_view()), + path('judges', api.api_v2.APIJudgeList.as_view()), + ] + ), + ), path('blog/', paged_list_view(blog.PostList, 'blog_post_list')), path('post/-', blog.PostView.as_view(), name='blog_post'), - path('license/', license.LicenseDetail.as_view(), name='license'), - - path('mailgun/mail_activate/', mailgun.MailgunActivationView.as_view(), name='mailgun_activate'), - - path('widgets/', include([ - path('rejudge', widgets.rejudge_submission, name='submission_rejudge'), - path('single_submission', submission.single_submission, name='submission_single_query'), - path('submission_testcases', submission.SubmissionTestCaseQuery.as_view(), name='submission_testcases_query'), - path('status-table', status.status_table, name='status_table'), - - path('template', problem.LanguageTemplateAjax.as_view(), name='language_template_ajax'), - - path('select2/', include([ - path('user_search', UserSearchSelect2View.as_view(), name='user_search_select2_ajax'), - path('contest_users/', ContestUserSearchSelect2View.as_view(), - name='contest_user_search_select2_ajax'), - path('ticket_user', TicketUserSelect2View.as_view(), name='ticket_user_select2_ajax'), - path('ticket_assignee', AssigneeSelect2View.as_view(), name='ticket_assignee_select2_ajax'), - ])), - - path('preview/', include([ - path('default', preview.DefaultMarkdownPreviewView.as_view(), name='default_preview'), - path('problem', preview.ProblemMarkdownPreviewView.as_view(), name='problem_preview'), - path('blog', preview.BlogMarkdownPreviewView.as_view(), name='blog_preview'), - path('contest', preview.ContestMarkdownPreviewView.as_view(), name='contest_preview'), - path('comment', preview.CommentMarkdownPreviewView.as_view(), name='comment_preview'), - path('flatpage', preview.FlatPageMarkdownPreviewView.as_view(), name='flatpage_preview'), - path('profile', preview.ProfileMarkdownPreviewView.as_view(), name='profile_preview'), - path('organization', preview.OrganizationMarkdownPreviewView.as_view(), name='organization_preview'), - path('solution', preview.SolutionMarkdownPreviewView.as_view(), name='solution_preview'), - path('license', preview.LicenseMarkdownPreviewView.as_view(), name='license_preview'), - path('ticket', preview.TicketMarkdownPreviewView.as_view(), name='ticket_preview'), - ])), - - path('martor/', include([ - path('upload-image', martor_image_uploader, name='martor_image_uploader'), - path('search-user', markdown_search_user, name='martor_search_user'), - ])), - ])), - - path('feed/', include([ - path('problems/rss/', ProblemFeed(), name='problem_rss'), - path('problems/atom/', AtomProblemFeed(), name='problem_atom'), - path('comment/rss/', CommentFeed(), name='comment_rss'), - path('comment/atom/', AtomCommentFeed(), name='comment_atom'), - path('blog/rss/', BlogFeed(), name='blog_rss'), - path('blog/atom/', AtomBlogFeed(), name='blog_atom'), - ])), - - path('stats/', include([ - path('language/', include([ - path('', stats.language, name='language_stats'), - path('data/all/', stats.language_data, name='language_stats_data_all'), - path('data/ac/', stats.ac_language_data, name='language_stats_data_ac'), - path('data/status/', stats.status_data, name='stats_data_status'), - path('data/ac_rate/', stats.ac_rate, name='language_stats_data_ac_rate'), - ])), - ])), - - path('tickets/', include([ - path('', ticket.TicketList.as_view(), name='ticket_list'), - path('ajax', ticket.TicketListDataAjax.as_view(), name='ticket_ajax'), - ])), - - path('ticket/', include([ - path('', ticket.TicketView.as_view(), name='ticket'), - path('/ajax', ticket.TicketMessageDataAjax.as_view(), name='ticket_message_ajax'), - path('/open', ticket.TicketStatusChangeView.as_view(open=True), name='ticket_open'), - path('/close', ticket.TicketStatusChangeView.as_view(open=False), name='ticket_close'), - path('/notes', ticket.TicketNotesEditView.as_view(), name='ticket_notes'), - ])), - + path( + 'mailgun/mail_activate/', + mailgun.MailgunActivationView.as_view(), + name='mailgun_activate', + ), + path( + 'widgets/', + include( + [ + path('rejudge', widgets.rejudge_submission, name='submission_rejudge'), + path( + 'single_submission', + submission.single_submission, + name='submission_single_query', + ), + path( + 'submission_testcases', + submission.SubmissionTestCaseQuery.as_view(), + name='submission_testcases_query', + ), + path('status-table', status.status_table, name='status_table'), + path( + 'template', + problem.LanguageTemplateAjax.as_view(), + name='language_template_ajax', + ), + path( + 'select2/', + include( + [ + path( + 'user_search', + UserSearchSelect2View.as_view(), + name='user_search_select2_ajax', + ), + path( + 'contest_users/', + ContestUserSearchSelect2View.as_view(), + name='contest_user_search_select2_ajax', + ), + path( + 'ticket_user', + TicketUserSelect2View.as_view(), + name='ticket_user_select2_ajax', + ), + path( + 'ticket_assignee', + AssigneeSelect2View.as_view(), + name='ticket_assignee_select2_ajax', + ), + ] + ), + ), + path( + 'preview/', + include( + [ + path( + 'default', + preview.DefaultMarkdownPreviewView.as_view(), + name='default_preview', + ), + path( + 'problem', + preview.ProblemMarkdownPreviewView.as_view(), + name='problem_preview', + ), + path( + 'blog', + preview.BlogMarkdownPreviewView.as_view(), + name='blog_preview', + ), + path( + 'contest', + preview.ContestMarkdownPreviewView.as_view(), + name='contest_preview', + ), + path( + 'comment', + preview.CommentMarkdownPreviewView.as_view(), + name='comment_preview', + ), + path( + 'flatpage', + preview.FlatPageMarkdownPreviewView.as_view(), + name='flatpage_preview', + ), + path( + 'profile', + preview.ProfileMarkdownPreviewView.as_view(), + name='profile_preview', + ), + path( + 'organization', + preview.OrganizationMarkdownPreviewView.as_view(), + name='organization_preview', + ), + path( + 'solution', + preview.SolutionMarkdownPreviewView.as_view(), + name='solution_preview', + ), + path( + 'license', + preview.LicenseMarkdownPreviewView.as_view(), + name='license_preview', + ), + path( + 'ticket', + preview.TicketMarkdownPreviewView.as_view(), + name='ticket_preview', + ), + ] + ), + ), + path( + 'martor/', + include( + [ + path( + 'upload-image', + martor_image_uploader, + name='martor_image_uploader', + ), + path( + 'search-user', + markdown_search_user, + name='martor_search_user', + ), + ] + ), + ), + ] + ), + ), + path( + 'feed/', + include( + [ + path('problems/rss/', ProblemFeed(), name='problem_rss'), + path('problems/atom/', AtomProblemFeed(), name='problem_atom'), + path('comment/rss/', CommentFeed(), name='comment_rss'), + path('comment/atom/', AtomCommentFeed(), name='comment_atom'), + path('blog/rss/', BlogFeed(), name='blog_rss'), + path('blog/atom/', AtomBlogFeed(), name='blog_atom'), + ] + ), + ), + path( + 'stats/', + include( + [ + path( + 'language/', + include( + [ + path('', stats.language, name='language_stats'), + path( + 'data/all/', + stats.language_data, + name='language_stats_data_all', + ), + path( + 'data/ac/', + stats.ac_language_data, + name='language_stats_data_ac', + ), + path( + 'data/status/', + stats.status_data, + name='stats_data_status', + ), + path( + 'data/ac_rate/', + stats.ac_rate, + name='language_stats_data_ac_rate', + ), + ] + ), + ), + ] + ), + ), + path( + 'tickets/', + include( + [ + path('', ticket.TicketList.as_view(), name='ticket_list'), + path('ajax', ticket.TicketListDataAjax.as_view(), name='ticket_ajax'), + ] + ), + ), + path( + 'ticket/', + include( + [ + path('', ticket.TicketView.as_view(), name='ticket'), + path( + '/ajax', + ticket.TicketMessageDataAjax.as_view(), + name='ticket_message_ajax', + ), + path( + '/open', + ticket.TicketStatusChangeView.as_view(open=True), + name='ticket_open', + ), + path( + '/close', + ticket.TicketStatusChangeView.as_view(open=False), + name='ticket_close', + ), + path( + '/notes', ticket.TicketNotesEditView.as_view(), name='ticket_notes' + ), + ] + ), + ), path('sitemap.xml', sitemap, {'sitemaps': sitemaps}), - - path('judge-select2/', include([ - path('profile/', UserSelect2View.as_view(), name='profile_select2'), - path('organization/', OrganizationSelect2View.as_view(), name='organization_select2'), - path('class/', ClassSelect2View.as_view(), name='class_select2'), - path('problem/', ProblemSelect2View.as_view(), name='problem_select2'), - path('contest/', ContestSelect2View.as_view(), name='contest_select2'), - path('comment/', CommentSelect2View.as_view(), name='comment_select2'), - ])), - - path('tasks/', include([ - path('status/', tasks.task_status, name='task_status'), - path('ajax_status', tasks.task_status_ajax, name='task_status_ajax'), - path('success', tasks.demo_success), - path('failure', tasks.demo_failure), - path('progress', tasks.demo_progress), - ])), + path( + 'judge-select2/', + include( + [ + path('profile/', UserSelect2View.as_view(), name='profile_select2'), + path( + 'organization/', + OrganizationSelect2View.as_view(), + name='organization_select2', + ), + path('class/', ClassSelect2View.as_view(), name='class_select2'), + path('problem/', ProblemSelect2View.as_view(), name='problem_select2'), + path('contest/', ContestSelect2View.as_view(), name='contest_select2'), + path('comment/', CommentSelect2View.as_view(), name='comment_select2'), + ] + ), + ), + path( + 'tasks/', + include( + [ + path('status/', tasks.task_status, name='task_status'), + path('ajax_status', tasks.task_status_ajax, name='task_status_ajax'), + path('success', tasks.demo_success), + path('failure', tasks.demo_failure), + path('progress', tasks.demo_progress), + ] + ), + ), ] -favicon_paths = ['apple-touch-icon-180x180.png', 'apple-touch-icon-114x114.png', 'android-chrome-72x72.png', - 'apple-touch-icon-57x57.png', 'apple-touch-icon-72x72.png', 'apple-touch-icon.png', 'mstile-70x70.png', - 'android-chrome-36x36.png', 'apple-touch-icon-precomposed.png', 'apple-touch-icon-76x76.png', - 'apple-touch-icon-60x60.png', 'android-chrome-96x96.png', 'mstile-144x144.png', 'mstile-150x150.png', - 'safari-pinned-tab.svg', 'android-chrome-144x144.png', 'apple-touch-icon-152x152.png', - 'favicon-96x96.png', - 'favicon-32x32.png', 'favicon-16x16.png', 'android-chrome-192x192.png', 'android-chrome-48x48.png', - 'mstile-310x150.png', 'apple-touch-icon-144x144.png', 'browserconfig.xml', 'manifest.json', - 'apple-touch-icon-120x120.png', 'mstile-310x310.png'] +favicon_paths = [ + 'apple-touch-icon-180x180.png', + 'apple-touch-icon-114x114.png', + 'android-chrome-72x72.png', + 'apple-touch-icon-57x57.png', + 'apple-touch-icon-72x72.png', + 'apple-touch-icon.png', + 'mstile-70x70.png', + 'android-chrome-36x36.png', + 'apple-touch-icon-precomposed.png', + 'apple-touch-icon-76x76.png', + 'apple-touch-icon-60x60.png', + 'android-chrome-96x96.png', + 'mstile-144x144.png', + 'mstile-150x150.png', + 'safari-pinned-tab.svg', + 'android-chrome-144x144.png', + 'apple-touch-icon-152x152.png', + 'favicon-96x96.png', + 'favicon-32x32.png', + 'favicon-16x16.png', + 'android-chrome-192x192.png', + 'android-chrome-48x48.png', + 'mstile-310x150.png', + 'apple-touch-icon-144x144.png', + 'browserconfig.xml', + 'manifest.json', + 'apple-touch-icon-120x120.png', + 'mstile-310x310.png', +] static_lazy = lazy(static, str) for favicon in favicon_paths: - urlpatterns.append(path(favicon, RedirectView.as_view( - url=static_lazy('icons/' + favicon), - ))) + urlpatterns.append( + path( + favicon, + RedirectView.as_view( + url=static_lazy('icons/' + favicon), + ), + ) + ) handler404 = 'judge.views.error.error404' handler403 = 'judge.views.error.error403' diff --git a/dmoj/wsgi.py b/dmoj/wsgi.py index 6bec753460..c09eda8295 100644 --- a/dmoj/wsgi.py +++ b/dmoj/wsgi.py @@ -1,4 +1,5 @@ import os + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dmoj.settings') try: @@ -8,5 +9,8 @@ pymysql.install_as_MySQLdb() -from django.core.wsgi import get_wsgi_application # noqa: E402, django must be imported here +from django.core.wsgi import ( + get_wsgi_application, +) # noqa: E402, django must be imported here + application = get_wsgi_application() diff --git a/dmoj/wsgi_async.py b/dmoj/wsgi_async.py index ec114d1fd8..cc74147934 100644 --- a/dmoj/wsgi_async.py +++ b/dmoj/wsgi_async.py @@ -8,5 +8,8 @@ # noinspection PyUnresolvedReferences import dmoj_install_pymysql # noqa: E402, F401, I100, I202, imported for side effect -from django.core.wsgi import get_wsgi_application # noqa: E402, I100, I202, django must be imported here +from django.core.wsgi import ( + get_wsgi_application, +) # noqa: E402, I100, I202, django must be imported here + application = get_wsgi_application() diff --git a/dmoj_bridge_async.py b/dmoj_bridge_async.py index 376f8cf8d0..f224d0d67b 100644 --- a/dmoj_bridge_async.py +++ b/dmoj_bridge_async.py @@ -9,9 +9,12 @@ import dmoj_install_pymysql # noqa: E402, F401, I100, I202, imported for side effect import django # noqa: E402, F401, I100, I202, django must be imported here + django.setup() -from judge.bridge.daemon import judge_daemon # noqa: E402, I100, I202, django code must be imported here +from judge.bridge.daemon import ( + judge_daemon, +) # noqa: E402, I100, I202, django code must be imported here if __name__ == '__main__': judge_daemon() diff --git a/judge/admin/__init__.py b/judge/admin/__init__.py index aa3d475ccc..a27ce6517c 100644 --- a/judge/admin/__init__.py +++ b/judge/admin/__init__.py @@ -4,18 +4,52 @@ from django.contrib.flatpages.models import FlatPage from judge.admin.comments import CommentAdmin -from judge.admin.contest import ContestAdmin, ContestParticipationAdmin, ContestRegistrationAdmin, ContestTagAdmin -from judge.admin.interface import BlogPostAdmin, FlatPageAdmin, LicenseAdmin, LogEntryAdmin, NavigationBarAdmin -from judge.admin.organization import ClassAdmin, OrganizationAdmin, OrganizationRequestAdmin +from judge.admin.contest import ( + ContestAdmin, + ContestParticipationAdmin, + ContestRegistrationAdmin, + ContestTagAdmin, +) +from judge.admin.interface import ( + BlogPostAdmin, + FlatPageAdmin, + LicenseAdmin, + LogEntryAdmin, + NavigationBarAdmin, +) +from judge.admin.organization import ( + ClassAdmin, + OrganizationAdmin, + OrganizationRequestAdmin, +) from judge.admin.problem import ProblemAdmin, ProblemPointsVoteAdmin from judge.admin.profile import ProfileAdmin, UserAdmin from judge.admin.runtime import JudgeAdmin, LanguageAdmin from judge.admin.submission import SubmissionAdmin from judge.admin.taxon import ProblemGroupAdmin, ProblemTypeAdmin from judge.admin.ticket import TicketAdmin -from judge.models import BlogPost, Comment, CommentLock, Contest, ContestParticipation, \ - ContestRegistration, ContestTag, Judge, Language, License, MiscConfig, NavigationBar, \ - Organization, OrganizationRequest, Problem, ProblemGroup, ProblemType, Profile, Submission, Ticket +from judge.models import ( + BlogPost, + Comment, + CommentLock, + Contest, + ContestParticipation, + ContestRegistration, + ContestTag, + Judge, + Language, + License, + MiscConfig, + NavigationBar, + Organization, + OrganizationRequest, + Problem, + ProblemGroup, + ProblemType, + Profile, + Submission, + Ticket, +) admin.site.register(BlogPost, BlogPostAdmin) admin.site.register(Comment, CommentAdmin) diff --git a/judge/admin/comments.py b/judge/admin/comments.py index a0e28b5633..9b9a259f07 100644 --- a/judge/admin/comments.py +++ b/judge/admin/comments.py @@ -15,7 +15,9 @@ class Meta: widgets = { 'author': AdminHeavySelect2Widget(data_view='profile_select2'), 'parent': AdminHeavySelect2Widget(data_view='comment_select2'), - 'body': AdminMartorWidget(attrs={'data-markdownfy-url': reverse_lazy('comment_preview')}), + 'body': AdminMartorWidget( + attrs={'data-markdownfy-url': reverse_lazy('comment_preview')} + ), } @@ -40,16 +42,28 @@ def get_queryset(self, request): @admin.display(description=_('Hide comments')) def hide_comment(self, request, queryset): count = queryset.update(hidden=True) - self.message_user(request, ngettext('%d comment successfully hidden.', - '%d comments successfully hidden.', - count) % count) + self.message_user( + request, + ngettext( + '%d comment successfully hidden.', + '%d comments successfully hidden.', + count, + ) + % count, + ) @admin.display(description=_('Unhide comments')) def unhide_comment(self, request, queryset): count = queryset.update(hidden=False) - self.message_user(request, ngettext('%d comment successfully unhidden.', - '%d comments successfully unhidden.', - count) % count) + self.message_user( + request, + ngettext( + '%d comment successfully unhidden.', + '%d comments successfully unhidden.', + count, + ) + % count, + ) @admin.display(description=_('associated page'), ordering='page') def linked_page(self, obj): diff --git a/judge/admin/contest.py b/judge/admin/contest.py index 59c25d103a..d172b618d6 100644 --- a/judge/admin/contest.py +++ b/judge/admin/contest.py @@ -13,11 +13,24 @@ from reversion.admin import VersionAdmin from django_ace import AceWidget -from judge.models import Class, Contest, ContestProblem, ContestSubmission, Profile, Rating, Submission +from judge.models import ( + Class, + 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, \ - AdminSelect2MultipleWidget, AdminSelect2Widget +from judge.widgets import ( + AdminHeavySelect2MultipleWidget, + AdminHeavySelect2Widget, + AdminMartorWidget, + AdminSelect2MultipleWidget, + AdminSelect2Widget, +) class AdminHeavySelect2Widget(AdminHeavySelect2Widget): @@ -31,7 +44,8 @@ class ContestTagForm(ModelForm): label=_('Included contests'), queryset=Contest.objects.all(), required=False, - widget=AdminHeavySelect2MultipleWidget(data_view='contest_select2')) + widget=AdminHeavySelect2MultipleWidget(data_view='contest_select2'), + ) class ContestTagAdmin(admin.ModelAdmin): @@ -64,8 +78,16 @@ class ContestProblemInline(SortableInlineAdminMixin, admin.TabularInline): model = ContestProblem verbose_name = _('Problem') verbose_name_plural = _('Problems') - fields = ('problem', 'points', 'partial', 'is_pretested', 'max_submissions', 'output_prefix_override', 'order', - 'rejudge_column') + fields = ( + 'problem', + 'points', + 'partial', + 'is_pretested', + 'max_submissions', + 'output_prefix_override', + 'order', + 'rejudge_column', + ) readonly_fields = ('rejudge_column',) form = ContestProblemInlineForm @@ -73,8 +95,11 @@ class ContestProblemInline(SortableInlineAdminMixin, admin.TabularInline): def rejudge_column(self, obj): if obj.id is None: return '' - return format_html('{1}', - reverse('admin:judge_contest_rejudge', args=(obj.contest.id, obj.id)), _('Rejudge')) + return format_html( + '{1}', + reverse('admin:judge_contest_rejudge', args=(obj.contest.id, obj.id)), + _('Rejudge'), + ) class ContestForm(ModelForm): @@ -82,8 +107,9 @@ def __init__(self, *args, **kwargs): super(ContestForm, self).__init__(*args, **kwargs) if 'rate_exclude' in self.fields: if self.instance and self.instance.id: - self.fields['rate_exclude'].queryset = \ - Profile.objects.filter(contest_history__contest=self.instance).distinct() + self.fields['rate_exclude'].queryset = Profile.objects.filter( + contest_history__contest=self.instance + ).distinct() else: self.fields['rate_exclude'].queryset = Profile.objects.none() self.fields['banned_users'].widget.can_add_related = False @@ -91,7 +117,9 @@ def __init__(self, *args, **kwargs): def clean(self): cleaned_data = super(ContestForm, self).clean() - cleaned_data['banned_users'].filter(current_contest__contest=self.instance).update(current_contest=None) + cleaned_data['banned_users'].filter( + current_contest__contest=self.instance + ).update(current_contest=None) class Meta: widgets = { @@ -99,39 +127,121 @@ class Meta: 'curators': AdminHeavySelect2MultipleWidget(data_view='profile_select2'), 'testers': AdminHeavySelect2MultipleWidget(data_view='profile_select2'), 'spectators': AdminHeavySelect2MultipleWidget(data_view='profile_select2'), - 'private_contestants': AdminHeavySelect2MultipleWidget(data_view='profile_select2', - attrs={'style': 'width: 100%'}), - 'organizations': AdminHeavySelect2MultipleWidget(data_view='organization_select2'), + 'private_contestants': AdminHeavySelect2MultipleWidget( + data_view='profile_select2', attrs={'style': 'width: 100%'} + ), + 'organizations': AdminHeavySelect2MultipleWidget( + data_view='organization_select2' + ), 'classes': AdminHeavySelect2MultipleWidget(data_view='class_select2'), - 'join_organizations': AdminHeavySelect2MultipleWidget(data_view='organization_select2'), + 'join_organizations': AdminHeavySelect2MultipleWidget( + data_view='organization_select2' + ), 'tags': AdminSelect2MultipleWidget, - 'banned_users': AdminHeavySelect2MultipleWidget(data_view='profile_select2', - attrs={'style': 'width: 100%'}), - 'view_contest_scoreboard': AdminHeavySelect2MultipleWidget(data_view='profile_select2', - attrs={'style': 'width: 100%'}), - 'view_contest_submissions': AdminHeavySelect2MultipleWidget(data_view='profile_select2', - attrs={'style': 'width: 100%'}), - 'description': AdminMartorWidget(attrs={'data-markdownfy-url': reverse_lazy('contest_preview')}), + 'banned_users': AdminHeavySelect2MultipleWidget( + data_view='profile_select2', attrs={'style': 'width: 100%'} + ), + 'view_contest_scoreboard': AdminHeavySelect2MultipleWidget( + data_view='profile_select2', attrs={'style': 'width: 100%'} + ), + 'view_contest_submissions': AdminHeavySelect2MultipleWidget( + data_view='profile_select2', attrs={'style': 'width: 100%'} + ), + 'description': AdminMartorWidget( + attrs={'data-markdownfy-url': reverse_lazy('contest_preview')} + ), } class ContestAdmin(NoBatchDeleteMixin, VersionAdmin): fieldsets = ( - (None, {'fields': ('key', 'name', 'authors', 'curators', 'testers', 'tester_see_submissions', - 'tester_see_scoreboard', 'spectators')}), - (_('Settings'), {'fields': ('is_visible', 'use_clarifications', 'hide_problem_tags', 'hide_problem_authors', - 'show_short_display', 'run_pretests_only', 'locked_after', 'scoreboard_visibility', - 'points_precision')}), + ( + None, + { + 'fields': ( + 'key', + 'name', + 'authors', + 'curators', + 'testers', + 'tester_see_submissions', + 'tester_see_scoreboard', + 'spectators', + ) + }, + ), + ( + _('Settings'), + { + 'fields': ( + 'is_visible', + 'use_clarifications', + 'hide_problem_tags', + 'hide_problem_authors', + 'show_short_display', + 'run_pretests_only', + 'locked_after', + 'scoreboard_visibility', + 'points_precision', + ) + }, + ), (_('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')}), - (_('Rating'), {'fields': ('is_rated', 'rate_all', 'rating_floor', 'rating_ceiling', 'rate_exclude')}), - (_('Access'), {'fields': ('access_code', 'private_contestants', 'organizations', 'classes', - 'join_organizations', 'view_contest_scoreboard', 'view_contest_submissions')}), + ( + _('Details'), + { + 'fields': ( + 'description', + 'og_image', + 'logo_override_image', + 'tags', + 'summary', + ) + }, + ), + ( + _('Format'), + {'fields': ('format_name', 'format_config', 'problem_label_script')}, + ), + ( + _('Rating'), + { + 'fields': ( + 'is_rated', + 'rate_all', + 'rating_floor', + 'rating_ceiling', + 'rate_exclude', + ) + }, + ), + ( + _('Access'), + { + 'fields': ( + 'access_code', + 'private_contestants', + 'organizations', + 'classes', + 'join_organizations', + 'view_contest_scoreboard', + 'view_contest_submissions', + ) + }, + ), (_('Justice'), {'fields': ('banned_users',)}), ) - list_display = ('key', 'name', 'is_visible', 'is_rated', 'locked_after', 'start_time', 'end_time', 'time_limit', - 'user_count') + list_display = ( + 'key', + 'name', + 'is_visible', + 'is_rated', + 'locked_after', + 'start_time', + 'end_time', + 'time_limit', + 'user_count', + ) search_fields = ('key', 'name') inlines = [ContestProblemInline] actions_on_top = True @@ -144,8 +254,9 @@ class ContestAdmin(NoBatchDeleteMixin, VersionAdmin): 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'): + 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) @@ -160,7 +271,9 @@ 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( + Q(authors=request.profile) | Q(curators=request.profile) + ).distinct() def get_readonly_fields(self, request, obj=None): readonly = [] @@ -184,12 +297,18 @@ def save_model(self, request, obj, form, change): if 'private_contestants' in form.changed_data: obj.is_private = bool(form.cleaned_data['private_contestants']) if 'organizations' in form.changed_data or 'classes' in form.changed_data: - obj.is_organization_private = bool(form.cleaned_data['organizations'] or form.cleaned_data['classes']) + obj.is_organization_private = bool( + form.cleaned_data['organizations'] or form.cleaned_data['classes'] + ) if 'join_organizations' in form.cleaned_data: - obj.limit_join_organizations = bool(form.cleaned_data['join_organizations']) + obj.limit_join_organizations = bool( + form.cleaned_data['join_organizations'] + ) # `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 form.cleaned_data.get('is_visible') and not request.user.has_perm( + 'judge.change_contest_visibility' + ): if not obj.is_private and not obj.is_organization_private: raise PermissionDenied if not request.user.has_perm('judge.create_private_contest'): @@ -198,7 +317,9 @@ def save_model(self, request, obj, form, change): 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')): + 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 @@ -220,67 +341,111 @@ def has_change_permission(self, request, obj=None): def _rescore(self, contest_key): from judge.tasks import rescore_contest + transaction.on_commit(rescore_contest.s(contest_key).delay) @admin.display(description=_('Mark contests as visible')) 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)) + queryset = queryset.filter( + Q(is_private=True) | Q(is_organization_private=True) + ) count = queryset.update(is_visible=True) - self.message_user(request, ngettext('%d contest successfully marked as visible.', - '%d contests successfully marked as visible.', - count) % count) + self.message_user( + request, + ngettext( + '%d contest successfully marked as visible.', + '%d contests successfully marked as visible.', + count, + ) + % count, + ) @admin.display(description=_('Mark contests as hidden')) def make_hidden(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)) + queryset = queryset.filter( + Q(is_private=True) | Q(is_organization_private=True) + ) count = queryset.update(is_visible=True) - self.message_user(request, ngettext('%d contest successfully marked as hidden.', - '%d contests successfully marked as hidden.', - count) % count) + self.message_user( + request, + ngettext( + '%d contest successfully marked as hidden.', + '%d contests successfully marked as hidden.', + count, + ) + % count, + ) @admin.display(description=_('Lock contest submissions')) def set_locked(self, request, queryset): for row in queryset: self.set_locked_after(row, timezone.now()) count = queryset.count() - self.message_user(request, ngettext('%d contest successfully locked.', - '%d contests successfully locked.', - count) % count) + self.message_user( + request, + ngettext( + '%d contest successfully locked.', + '%d contests successfully locked.', + count, + ) + % count, + ) @admin.display(description=_('Unlock contest submissions')) def set_unlocked(self, request, queryset): for row in queryset: self.set_locked_after(row, None) count = queryset.count() - self.message_user(request, ngettext('%d contest successfully unlocked.', - '%d contests successfully unlocked.', - count) % count) + self.message_user( + request, + ngettext( + '%d contest successfully unlocked.', + '%d contests successfully unlocked.', + count, + ) + % count, + ) def set_locked_after(self, contest, locked_after): with transaction.atomic(): contest.locked_after = locked_after contest.save() - Submission.objects.filter(contest_object=contest, - contest__participation__virtual=0).update(locked_after=locked_after) + Submission.objects.filter( + contest_object=contest, contest__participation__virtual=0 + ).update(locked_after=locked_after) def get_urls(self): return [ path('rate/all/', self.rate_all_view, name='judge_contest_rate_all'), path('/rate/', self.rate_view, name='judge_contest_rate'), - path('/judge//', self.rejudge_view, name='judge_contest_rejudge'), + path( + '/judge//', + self.rejudge_view, + name='judge_contest_rejudge', + ), ] + super(ContestAdmin, self).get_urls() def rejudge_view(self, request, contest_id, problem_id): - queryset = ContestSubmission.objects.filter(problem_id=problem_id).select_related('submission') + queryset = ContestSubmission.objects.filter( + problem_id=problem_id + ).select_related('submission') for model in queryset: model.submission.judge(rejudge=True, rejudge_user=request.user) - self.message_user(request, ngettext('%d submission was successfully scheduled for rejudging.', - '%d submissions were successfully scheduled for rejudging.', - len(queryset)) % len(queryset)) - return HttpResponseRedirect(reverse('admin:judge_contest_change', args=(contest_id,))) + self.message_user( + request, + ngettext( + '%d submission was successfully scheduled for rejudging.', + '%d submissions were successfully scheduled for rejudging.', + len(queryset), + ) + % len(queryset), + ) + return HttpResponseRedirect( + reverse('admin:judge_contest_change', args=(contest_id,)) + ) def rate_all_view(self, request): if not request.user.has_perm('judge.contest_rating'): @@ -289,7 +454,9 @@ def rate_all_view(self, request): with connection.cursor() as cursor: cursor.execute('TRUNCATE TABLE `%s`' % Rating._meta.db_table) Profile.objects.update(rating=None) - for contest in Contest.objects.filter(is_rated=True, end_time__lte=timezone.now()).order_by('end_time'): + for contest in Contest.objects.filter( + is_rated=True, end_time__lte=timezone.now() + ).order_by('end_time'): rate_contest(contest) return HttpResponseRedirect(reverse('admin:judge_contest_changelist')) @@ -301,7 +468,9 @@ def rate_view(self, request, id): raise Http404() with transaction.atomic(): contest.rate() - return HttpResponseRedirect(request.META.get('HTTP_REFERER', reverse('admin:judge_contest_changelist'))) + return HttpResponseRedirect( + request.META.get('HTTP_REFERER', reverse('admin:judge_contest_changelist')) + ) def get_form(self, request, obj=None, **kwargs): form = super(ContestAdmin, self).get_form(request, obj, **kwargs) @@ -309,14 +478,15 @@ def get_form(self, request, obj=None, **kwargs): # 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 = AceWidget( - mode='lua', theme=request.profile.resolved_ace_theme, + 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), + Q(user__is_superuser=True) + | Q(user__groups__permissions__codename__in=perms) + | Q(user__user_permissions__codename__in=perms), ).distinct() form.base_fields['classes'].queryset = Class.get_visible_classes(request.user) return form @@ -332,7 +502,15 @@ class Meta: class ContestParticipationAdmin(admin.ModelAdmin): fields = ('contest', 'user', 'real_start', 'virtual', 'is_disqualified') - list_display = ('contest', 'username', 'show_virtual', 'real_start', 'score', 'cumtime', 'tiebreaker') + list_display = ( + 'contest', + 'username', + 'show_virtual', + 'real_start', + 'score', + 'cumtime', + 'tiebreaker', + ) actions = ['recalculate_results'] actions_on_bottom = actions_on_top = True search_fields = ('contest__key', 'contest__name', 'user__user__username') @@ -340,9 +518,20 @@ class ContestParticipationAdmin(admin.ModelAdmin): date_hierarchy = 'real_start' def get_queryset(self, request): - return super(ContestParticipationAdmin, self).get_queryset(request).only( - 'contest__name', 'contest__format_name', 'contest__format_config', - 'user__user__username', 'real_start', 'score', 'cumtime', 'tiebreaker', 'virtual', + return ( + super(ContestParticipationAdmin, self) + .get_queryset(request) + .only( + 'contest__name', + 'contest__format_name', + 'contest__format_config', + 'user__user__username', + 'real_start', + 'score', + 'cumtime', + 'tiebreaker', + 'virtual', + ) ) def save_model(self, request, obj, form, change): @@ -356,9 +545,15 @@ def recalculate_results(self, request, queryset): for participation in queryset: participation.recompute_results() count += 1 - self.message_user(request, ngettext('%d participation recalculated.', - '%d participations recalculated.', - count) % count) + self.message_user( + request, + ngettext( + '%d participation recalculated.', + '%d participations recalculated.', + count, + ) + % count, + ) @admin.display(description=_('username'), ordering='user__user__username') def username(self, obj): diff --git a/judge/admin/interface.py b/judge/admin/interface.py index f49c3a654c..8c09797231 100644 --- a/judge/admin/interface.py +++ b/judge/admin/interface.py @@ -11,7 +11,11 @@ from judge.dblock import LockModel from judge.models import BlogPost, NavigationBar -from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, AdminMartorWidget +from judge.widgets import ( + AdminHeavySelect2MultipleWidget, + AdminHeavySelect2Widget, + AdminMartorWidget, +) class NavigationBarAdmin(DraggableMPTTAdmin): @@ -36,7 +40,9 @@ def save_model(self, request, obj, form, change): def changelist_view(self, request, extra_context=None): self.__save_model_calls = 0 with NavigationBar.objects.disable_mptt_updates(): - result = super(NavigationBarAdmin, self).changelist_view(request, extra_context) + result = super(NavigationBarAdmin, self).changelist_view( + request, extra_context + ) if self.__save_model_calls: with LockModel(write=(NavigationBar,)): NavigationBar.objects.rebuild() @@ -45,7 +51,11 @@ def changelist_view(self, request, extra_context=None): class FlatpageForm(OldFlatpageForm): class Meta(OldFlatpageForm.Meta): - widgets = {'content': AdminMartorWidget(attrs={'data-markdownfy-url': reverse_lazy('flatpage_preview')})} + widgets = { + 'content': AdminMartorWidget( + attrs={'data-markdownfy-url': reverse_lazy('flatpage_preview')} + ) + } class FlatPageAdmin(VersionAdmin, OldFlatPageAdmin): @@ -61,15 +71,24 @@ def __init__(self, *args, **kwargs): class Meta: widgets = { - 'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}), - 'content': AdminMartorWidget(attrs={'data-markdownfy-url': reverse_lazy('blog_preview')}), - 'summary': AdminMartorWidget(attrs={'data-markdownfy-url': reverse_lazy('blog_preview')}), + 'authors': AdminHeavySelect2MultipleWidget( + data_view='profile_select2', attrs={'style': 'width: 100%'} + ), + 'content': AdminMartorWidget( + attrs={'data-markdownfy-url': reverse_lazy('blog_preview')} + ), + 'summary': AdminMartorWidget( + attrs={'data-markdownfy-url': reverse_lazy('blog_preview')} + ), } class BlogPostAdmin(VersionAdmin): fieldsets = ( - (None, {'fields': ('title', 'slug', 'authors', 'visible', 'sticky', 'publish_on')}), + ( + None, + {'fields': ('title', 'slug', 'authors', 'visible', 'sticky', 'publish_on')}, + ), (_('Content'), {'fields': ('content', 'og_image')}), (_('Summary'), {'classes': ('collapse',), 'fields': ('summary',)}), ) @@ -104,15 +123,25 @@ def __init__(self, *args, **kwargs): class Meta: widgets = { - 'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}), - 'problem': AdminHeavySelect2Widget(data_view='problem_select2', attrs={'style': 'width: 250px'}), - 'content': AdminMartorWidget(attrs={'data-markdownfy-url': reverse_lazy('solution_preview')}), + 'authors': AdminHeavySelect2MultipleWidget( + data_view='profile_select2', attrs={'style': 'width: 100%'} + ), + 'problem': AdminHeavySelect2Widget( + data_view='problem_select2', attrs={'style': 'width: 250px'} + ), + 'content': AdminMartorWidget( + attrs={'data-markdownfy-url': reverse_lazy('solution_preview')} + ), } class LicenseForm(ModelForm): class Meta: - widgets = {'text': AdminMartorWidget(attrs={'data-markdownfy-url': reverse_lazy('license_preview')})} + widgets = { + 'text': AdminMartorWidget( + attrs={'data-markdownfy-url': reverse_lazy('license_preview')} + ) + } class LicenseAdmin(admin.ModelAdmin): @@ -135,7 +164,14 @@ def queryset(self, request, queryset): class LogEntryAdmin(admin.ModelAdmin): - readonly_fields = ('user', 'content_type', 'object_id', 'object_repr', 'action_flag', 'change_message') + readonly_fields = ( + 'user', + 'content_type', + 'object_id', + 'object_repr', + 'action_flag', + 'change_message', + ) list_display = ('__str__', 'action_time', 'user', 'content_type', 'object_link') search_fields = ('object_repr', 'change_message') list_filter = (UserListFilter, 'content_type') @@ -158,8 +194,14 @@ def object_link(self, obj): else: ct = obj.content_type try: - link = format_html('{0}', obj.object_repr, - reverse('admin:%s_%s_change' % (ct.app_label, ct.model), args=(obj.object_id,))) + link = format_html( + '{0}', + obj.object_repr, + reverse( + 'admin:%s_%s_change' % (ct.app_label, ct.model), + args=(obj.object_id,), + ), + ) except NoReverseMatch: link = obj.object_repr return link diff --git a/judge/admin/organization.py b/judge/admin/organization.py index ff5fab5f0b..493b9a031c 100644 --- a/judge/admin/organization.py +++ b/judge/admin/organization.py @@ -18,7 +18,16 @@ class Meta: class ClassAdmin(VersionAdmin): - fields = ('name', 'slug', 'organization', 'is_active', 'access_code', 'admins', 'description', 'members') + fields = ( + 'name', + 'slug', + 'organization', + 'is_active', + 'access_code', + 'admins', + 'description', + 'members', + ) list_display = ('name', 'organization', 'is_active') prepopulated_fields = {'slug': ('name',)} form = ClassForm @@ -27,22 +36,26 @@ def get_queryset(self, request): queryset = super().get_queryset(request) if not request.user.has_perm('judge.edit_all_organization'): queryset = queryset.filter( - Q(admins__id=request.profile.id) | - Q(organization__admins__id=request.profile.id), + Q(admins__id=request.profile.id) + | Q(organization__admins__id=request.profile.id), ).distinct() return queryset def has_add_permission(self, request): - return (request.user.has_perm('judge.add_class') and - Organization.objects.filter(admins__id=request.profile.id).exists()) + return ( + request.user.has_perm('judge.add_class') + and Organization.objects.filter(admins__id=request.profile.id).exists() + ) def has_change_permission(self, request, obj=None): if not request.user.has_perm('judge.change_class'): return False if request.user.has_perm('judge.edit_all_organization') or obj is None: return True - return (obj.admins.filter(id=request.profile.id).exists() or - obj.organization.admins.filter(id=request.profile.id).exists()) + return ( + obj.admins.filter(id=request.profile.id).exists() + or obj.organization.admins.filter(id=request.profile.id).exists() + ) def get_readonly_fields(self, request, obj=None): fields = [] @@ -55,7 +68,9 @@ def get_readonly_fields(self, request, obj=None): def get_form(self, request, obj=None, change=False, **kwargs): form = super().get_form(request, obj, change, **kwargs) if 'organization' in form.base_fields: - form.base_fields['organization'].queryset = Organization.objects.filter(admins__id=request.profile.id) + form.base_fields['organization'].queryset = Organization.objects.filter( + admins__id=request.profile.id + ) return form @@ -63,14 +78,26 @@ class OrganizationForm(ModelForm): class Meta: widgets = { 'admins': AdminHeavySelect2MultipleWidget(data_view='profile_select2'), - 'about': AdminMartorWidget(attrs={'data-markdownfy-url': reverse_lazy('organization_preview')}), + 'about': AdminMartorWidget( + attrs={'data-markdownfy-url': reverse_lazy('organization_preview')} + ), } class OrganizationAdmin(VersionAdmin): readonly_fields = ('creation_date',) - fields = ('name', 'slug', 'short_name', 'is_open', 'class_required', 'about', 'logo_override_image', 'slots', - 'creation_date', 'admins') + fields = ( + 'name', + 'slug', + 'short_name', + 'is_open', + 'class_required', + 'about', + 'logo_override_image', + 'slots', + 'creation_date', + 'admins', + ) list_display = ('name', 'short_name', 'is_open', 'slots', 'show_public') prepopulated_fields = {'slug': ('name',)} actions_on_top = True @@ -79,8 +106,11 @@ class OrganizationAdmin(VersionAdmin): @admin.display(description='') def show_public(self, obj): - return format_html('{1}', - obj.get_absolute_url(), gettext('View on site')) + return format_html( + '{1}', + obj.get_absolute_url(), + gettext('View on site'), + ) def get_readonly_fields(self, request, obj=None): fields = self.readonly_fields diff --git a/judge/admin/problem.py b/judge/admin/problem.py index c78c0981f6..b5f0c40d29 100644 --- a/judge/admin/problem.py +++ b/judge/admin/problem.py @@ -10,15 +10,29 @@ from django.utils.translation import gettext, gettext_lazy as _, ngettext from reversion.admin import VersionAdmin -from judge.models import LanguageLimit, Problem, ProblemClarification, ProblemPointsVote, ProblemTranslation, Profile, \ - Solution +from judge.models import ( + LanguageLimit, + Problem, + ProblemClarification, + ProblemPointsVote, + ProblemTranslation, + Profile, + Solution, +) from judge.utils.views import NoBatchDeleteMixin -from judge.widgets import AdminHeavySelect2MultipleWidget, AdminMartorWidget, AdminSelect2MultipleWidget, \ - AdminSelect2Widget, CheckboxSelectMultipleWithSelectAll +from judge.widgets import ( + AdminHeavySelect2MultipleWidget, + AdminMartorWidget, + AdminSelect2MultipleWidget, + AdminSelect2Widget, + CheckboxSelectMultipleWithSelectAll, +) class ProblemForm(ModelForm): - change_message = forms.CharField(max_length=256, label=_('Edit reason'), required=False) + change_message = forms.CharField( + max_length=256, label=_('Edit reason'), required=False + ) def __init__(self, *args, **kwargs): super(ProblemForm, self).__init__(*args, **kwargs) @@ -26,22 +40,34 @@ def __init__(self, *args, **kwargs): self.fields['curators'].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)'), - }) + self.fields['change_message'].widget.attrs.update( + { + 'placeholder': gettext('Describe the changes you made (optional)'), + } + ) class Meta: widgets = { - 'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}), - 'curators': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}), - 'testers': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}), - 'banned_users': AdminHeavySelect2MultipleWidget(data_view='profile_select2', - attrs={'style': 'width: 100%'}), - 'organizations': AdminHeavySelect2MultipleWidget(data_view='organization_select2', - attrs={'style': 'width: 100%'}), + 'authors': AdminHeavySelect2MultipleWidget( + data_view='profile_select2', attrs={'style': 'width: 100%'} + ), + 'curators': AdminHeavySelect2MultipleWidget( + data_view='profile_select2', attrs={'style': 'width: 100%'} + ), + 'testers': AdminHeavySelect2MultipleWidget( + data_view='profile_select2', attrs={'style': 'width: 100%'} + ), + 'banned_users': AdminHeavySelect2MultipleWidget( + data_view='profile_select2', attrs={'style': 'width: 100%'} + ), + 'organizations': AdminHeavySelect2MultipleWidget( + data_view='organization_select2', attrs={'style': 'width: 100%'} + ), 'types': AdminSelect2MultipleWidget, 'group': AdminSelect2Widget, - 'description': AdminMartorWidget(attrs={'data-markdownfy-url': reverse_lazy('problem_preview')}), + 'description': AdminMartorWidget( + attrs={'data-markdownfy-url': reverse_lazy('problem_preview')} + ), } @@ -49,7 +75,9 @@ 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 = Profile.objects.exclude(authored_problems=None).values_list( + 'user__username', flat=True + ) return [(name, name) for name in queryset] def queryset(self, request, queryset): @@ -71,7 +99,11 @@ class LanguageLimitInline(admin.TabularInline): class ProblemClarificationForm(ModelForm): class Meta: - widgets = {'description': AdminMartorWidget(attrs={'data-markdownfy-url': reverse_lazy('comment_preview')})} + widgets = { + 'description': AdminMartorWidget( + attrs={'data-markdownfy-url': reverse_lazy('comment_preview')} + ) + } class ProblemClarificationInline(admin.StackedInline): @@ -88,8 +120,12 @@ def __init__(self, *args, **kwargs): class Meta: widgets = { - 'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}), - 'content': AdminMartorWidget(attrs={'data-markdownfy-url': reverse_lazy('solution_preview')}), + 'authors': AdminHeavySelect2MultipleWidget( + data_view='profile_select2', attrs={'style': 'width: 100%'} + ), + 'content': AdminMartorWidget( + attrs={'data-markdownfy-url': reverse_lazy('solution_preview')} + ), } @@ -102,7 +138,11 @@ class ProblemSolutionInline(admin.StackedInline): class ProblemTranslationForm(ModelForm): class Meta: - widgets = {'description': AdminMartorWidget(attrs={'data-markdownfy-url': reverse_lazy('problem_preview')})} + widgets = { + 'description': AdminMartorWidget( + attrs={'data-markdownfy-url': reverse_lazy('problem_preview')} + ) + } class ProblemTranslationInline(admin.StackedInline): @@ -114,21 +154,41 @@ class ProblemTranslationInline(admin.StackedInline): def has_permission_full_markup(self, request, obj=None): if not obj: return True - return request.user.has_perm('judge.problem_full_markup') or not obj.is_full_markup + return ( + request.user.has_perm('judge.problem_full_markup') or not obj.is_full_markup + ) - has_add_permission = has_change_permission = has_delete_permission = has_permission_full_markup + has_add_permission = ( + has_change_permission + ) = has_delete_permission = has_permission_full_markup class ProblemAdmin(NoBatchDeleteMixin, VersionAdmin): fieldsets = ( - (None, { - 'fields': ( - 'code', 'name', 'is_public', 'is_manually_managed', 'date', 'authors', 'curators', 'testers', - 'organizations', 'submission_source_visibility_mode', 'is_full_markup', - 'description', 'license', - ), - }), - (_('Social Media'), {'classes': ('collapse',), 'fields': ('og_image', 'summary')}), + ( + None, + { + 'fields': ( + 'code', + 'name', + 'is_public', + 'is_manually_managed', + 'date', + 'authors', + 'curators', + 'testers', + 'organizations', + 'submission_source_visibility_mode', + 'is_full_markup', + 'description', + 'license', + ), + }, + ), + ( + _('Social Media'), + {'classes': ('collapse',), 'fields': ('og_image', 'summary')}, + ), (_('Taxonomy'), {'fields': ('types', 'group')}), (_('Points'), {'fields': (('points', 'partial'), 'short_circuit')}), (_('Limits'), {'fields': ('time_limit', 'memory_limit')}), @@ -136,10 +196,27 @@ class ProblemAdmin(NoBatchDeleteMixin, VersionAdmin): (_('Justice'), {'fields': ('banned_users',)}), (_('History'), {'fields': ('change_message',)}), ) - list_display = ['code', 'name', 'show_authors', 'points', 'is_public', 'show_public'] + 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', + 'authors__user__username', + 'curators__user__username', + ) + inlines = [ + LanguageLimitInline, + ProblemClarificationInline, + ProblemSolutionInline, + ProblemTranslationInline, + ] list_max_show_all = 1000 actions_on_top = True actions_on_bottom = True @@ -180,39 +257,64 @@ def show_authors(self, obj): @admin.display(description='') def show_public(self, obj): - return format_html('{0}', gettext('View on site'), obj.get_absolute_url()) + return format_html( + '{0}', gettext('View on site'), obj.get_absolute_url() + ) def _rescore(self, request, problem_id): from judge.tasks import rescore_problem + transaction.on_commit(rescore_problem.s(problem_id).delay) @admin.display(description=_('Set publish date to now')) def update_publish_date(self, request, queryset): count = queryset.update(date=timezone.now()) - self.message_user(request, ngettext("%d problem's publish date successfully updated.", - "%d problems' publish date successfully updated.", - count) % count) + self.message_user( + request, + ngettext( + "%d problem's publish date successfully updated.", + "%d problems' publish date successfully updated.", + count, + ) + % count, + ) @admin.display(description=_('Mark problems as public')) def make_public(self, request, queryset): count = queryset.update(is_public=True) for problem_id in queryset.values_list('id', flat=True): self._rescore(request, problem_id) - self.message_user(request, ngettext('%d problem successfully marked as public.', - '%d problems successfully marked as public.', - count) % count) + self.message_user( + request, + ngettext( + '%d problem successfully marked as public.', + '%d problems successfully marked as public.', + count, + ) + % count, + ) @admin.display(description=_('Mark problems as private')) def make_private(self, request, queryset): count = queryset.update(is_public=False) for problem_id in queryset.values_list('id', flat=True): self._rescore(request, problem_id) - self.message_user(request, ngettext('%d problem successfully marked as private.', - '%d problems successfully marked as private.', - count) % count) + self.message_user( + request, + ngettext( + '%d problem successfully marked as private.', + '%d problems successfully marked as private.', + 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('authors__user') + .distinct() + ) def has_change_permission(self, request, obj=None): if obj is None: @@ -222,7 +324,9 @@ def has_change_permission(self, request, obj=None): def formfield_for_manytomany(self, db_field, request=None, **kwargs): if db_field.name == 'allowed_languages': kwargs['widget'] = CheckboxSelectMultipleWithSelectAll() - return super(ProblemAdmin, self).formfield_for_manytomany(db_field, request, **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) @@ -234,16 +338,18 @@ def save_model(self, request, obj, form, change): if form.changed_data and 'organizations' in form.changed_data: obj.is_organization_private = bool(form.cleaned_data['organizations']) super(ProblemAdmin, self).save_model(request, obj, form, change) - if ( - form.changed_data and - any(f in form.changed_data for f in ('is_public', 'organizations', 'points', 'partial')) + if form.changed_data and any( + f in form.changed_data + for f in ('is_public', 'organizations', 'points', 'partial') ): self._rescore(request, obj.id) def construct_change_message(self, request, form, *args, **kwargs): if form.cleaned_data.get('change_message'): return form.cleaned_data['change_message'] - return super(ProblemAdmin, self).construct_change_message(request, form, *args, **kwargs) + return super(ProblemAdmin, self).construct_change_message( + request, form, *args, **kwargs + ) class ProblemPointsVoteAdmin(admin.ModelAdmin): @@ -252,7 +358,9 @@ class ProblemPointsVoteAdmin(admin.ModelAdmin): readonly_fields = ('voter', 'problem', 'vote_time') def get_queryset(self, request): - return ProblemPointsVote.objects.filter(problem__in=Problem.get_editable_problems(request.user)) + return ProblemPointsVote.objects.filter( + problem__in=Problem.get_editable_problems(request.user) + ) def has_add_permission(self, request): return False diff --git a/judge/admin/profile.py b/judge/admin/profile.py index 5e267bf93e..fa2c829359 100644 --- a/judge/admin/profile.py +++ b/judge/admin/profile.py @@ -17,10 +17,16 @@ def __init__(self, *args, **kwargs): super(ProfileForm, self).__init__(*args, **kwargs) if 'current_contest' in self.base_fields: # form.fields['current_contest'] does not exist when the user has only view permission on the model. - self.fields['current_contest'].queryset = self.instance.contest_history.select_related('contest') \ - .only('contest__name', 'user_id', 'virtual') - self.fields['current_contest'].label_from_instance = \ - lambda obj: '%s v%d' % (obj.contest.name, obj.virtual) if obj.virtual else obj.contest.name + self.fields[ + 'current_contest' + ].queryset = self.instance.contest_history.select_related('contest').only( + 'contest__name', 'user_id', 'virtual' + ) + self.fields['current_contest'].label_from_instance = ( + lambda obj: '%s v%d' % (obj.contest.name, obj.virtual) + if obj.virtual + else obj.contest.name + ) class Meta: widgets = { @@ -28,7 +34,9 @@ class Meta: 'language': AdminSelect2Widget, 'ace_theme': AdminSelect2Widget, 'current_contest': AdminSelect2Widget, - 'about': AdminMartorWidget(attrs={'data-markdownfy-url': reverse_lazy('profile_preview')}), + 'about': AdminMartorWidget( + attrs={'data-markdownfy-url': reverse_lazy('profile_preview')} + ), } @@ -37,7 +45,11 @@ class TimezoneFilter(admin.SimpleListFilter): parameter_name = 'timezone' def lookups(self, request, model_admin): - return Profile.objects.values_list('timezone', 'timezone').distinct().order_by('timezone') + return ( + Profile.objects.values_list('timezone', 'timezone') + .distinct() + .order_by('timezone') + ) def queryset(self, request, queryset): if self.value() is None: @@ -55,12 +67,37 @@ def has_add_permission(self, request, obj=None): class ProfileAdmin(NoBatchDeleteMixin, VersionAdmin): - fields = ('user', 'display_rank', 'about', 'organizations', 'timezone', 'language', 'ace_theme', - 'math_engine', 'last_access', 'ip', 'mute', 'is_unlisted', 'is_banned_from_problem_voting', - 'username_display_override', 'notes', 'is_totp_enabled', 'user_script', 'current_contest') + fields = ( + 'user', + 'display_rank', + 'about', + 'organizations', + 'timezone', + 'language', + 'ace_theme', + 'math_engine', + 'last_access', + 'ip', + 'mute', + 'is_unlisted', + 'is_banned_from_problem_voting', + 'username_display_override', + 'notes', + 'is_totp_enabled', + 'user_script', + 'current_contest', + ) readonly_fields = ('user',) - list_display = ('admin_user_admin', 'email', 'is_totp_enabled', 'timezone_full', - 'date_joined', 'last_access', 'ip', 'show_public') + list_display = ( + 'admin_user_admin', + 'email', + 'is_totp_enabled', + 'timezone_full', + 'date_joined', + 'last_access', + 'ip', + 'show_public', + ) ordering = ('user__username',) search_fields = ('user__username', 'ip', 'user__email') list_filter = ('language', TimezoneFilter) @@ -101,8 +138,11 @@ def get_readonly_fields(self, request, obj=None): @admin.display(description='') def show_public(self, obj): - return format_html('{1}', - obj.get_absolute_url(), gettext('View on site')) + return format_html( + '{1}', + obj.get_absolute_url(), + gettext('View on site'), + ) @admin.display(description=_('user'), ordering='user__username') def admin_user_admin(self, obj): @@ -126,16 +166,23 @@ def recalculate_points(self, request, queryset): for profile in queryset: profile.calculate_points() count += 1 - self.message_user(request, ngettext('%d user had scores recalculated.', - '%d users had scores recalculated.', - count) % count) + self.message_user( + request, + ngettext( + '%d user had scores recalculated.', + '%d users had scores recalculated.', + count, + ) + % count, + ) def get_form(self, request, obj=None, **kwargs): form = super(ProfileAdmin, self).get_form(request, obj, **kwargs) if 'user_script' in form.base_fields: # form.base_fields['user_script'] does not exist when the user has only view permission on the model. form.base_fields['user_script'].widget = AceWidget( - mode='javascript', theme=request.profile.resolved_ace_theme, + mode='javascript', + theme=request.profile.resolved_ace_theme, ) return form diff --git a/judge/admin/runtime.py b/judge/admin/runtime.py index 3c756527c8..f1704bc784 100644 --- a/judge/admin/runtime.py +++ b/judge/admin/runtime.py @@ -19,8 +19,18 @@ class Meta: class LanguageAdmin(VersionAdmin): - fields = ('key', 'name', 'short_name', 'common_name', 'ace', 'pygments', 'info', 'extension', 'description', - 'template') + fields = ( + 'key', + 'name', + 'short_name', + 'common_name', + 'ace', + 'pygments', + 'info', + 'extension', + 'description', + 'template', + ) list_display = ('key', 'name', 'common_name', 'info') form = LanguageForm @@ -28,7 +38,8 @@ def get_form(self, request, obj=None, **kwargs): form = super(LanguageAdmin, self).get_form(request, obj, **kwargs) if obj is not None: form.base_fields['template'].widget = AceWidget( - mode=obj.ace, theme=request.profile.resolved_ace_theme, + mode=obj.ace, + theme=request.profile.resolved_ace_theme, ) return form @@ -36,8 +47,10 @@ def get_form(self, request, obj=None, **kwargs): class GenerateKeyTextInput(TextInput): def render(self, name, value, attrs=None, renderer=None): text = super(TextInput, self).render(name, value, attrs) - return mark_safe(text + format_html( - """\ + return mark_safe( + text + + format_html( + """\ {1} -""", name, _('Regenerate'))) +""", + name, + _('Regenerate'), + ) + ) class JudgeAdminForm(ModelForm): @@ -59,25 +76,52 @@ class Meta: class JudgeAdmin(VersionAdmin): form = JudgeAdminForm - readonly_fields = ('created', 'online', 'start_time', 'ping', 'load', 'last_ip', 'runtimes', 'problems', - 'is_disabled') + readonly_fields = ( + 'created', + 'online', + 'start_time', + 'ping', + 'load', + 'last_ip', + 'runtimes', + 'problems', + 'is_disabled', + ) fieldsets = ( (None, {'fields': ('name', 'auth_key', 'is_blocked', 'is_disabled')}), (_('Description'), {'fields': ('description',)}), - (_('Information'), {'fields': ('created', 'online', 'last_ip', 'start_time', 'ping', 'load')}), + ( + _('Information'), + {'fields': ('created', 'online', 'last_ip', 'start_time', 'ping', 'load')}, + ), (_('Capabilities'), {'fields': ('runtimes',)}), ) - list_display = ('name', 'online', 'is_disabled', 'start_time', 'ping', 'load', 'last_ip') + list_display = ( + 'name', + 'online', + 'is_disabled', + 'start_time', + 'ping', + 'load', + 'last_ip', + ) ordering = ['-online', 'name'] formfield_overrides = { TextField: {'widget': AdminMartorWidget}, } def get_urls(self): - return ([path('/disconnect/', self.disconnect_view, name='judge_judge_disconnect'), - path('/terminate/', self.terminate_view, name='judge_judge_terminate'), - path('/disable/', self.disable_view, name='judge_judge_disable')] + - super(JudgeAdmin, self).get_urls()) + return [ + path( + '/disconnect/', + self.disconnect_view, + name='judge_judge_disconnect', + ), + path( + '/terminate/', self.terminate_view, name='judge_judge_terminate' + ), + path('/disable/', self.disable_view, name='judge_judge_disable'), + ] + super(JudgeAdmin, self).get_urls() def disconnect_judge(self, id, force=False): judge = get_object_or_404(Judge, id=id) @@ -93,7 +137,9 @@ def terminate_view(self, request, id): def disable_view(self, request, id): judge = get_object_or_404(Judge, id=id) judge.toggle_disabled() - return HttpResponseRedirect(reverse('admin:judge_judge_change', args=(judge.id,))) + return HttpResponseRedirect( + reverse('admin:judge_judge_change', args=(judge.id,)) + ) def get_readonly_fields(self, request, obj=None): if obj is not None and obj.online: diff --git a/judge/admin/submission.py b/judge/admin/submission.py index aa3889d304..be009a68e9 100644 --- a/judge/admin/submission.py +++ b/judge/admin/submission.py @@ -14,14 +14,25 @@ from reversion.admin import VersionAdmin from django_ace import AceWidget -from judge.models import ContestParticipation, ContestProblem, ContestSubmission, Profile, Submission, \ - SubmissionSource, SubmissionTestCase +from judge.models import ( + ContestParticipation, + ContestProblem, + ContestSubmission, + Profile, + Submission, + SubmissionSource, + SubmissionTestCase, +) from judge.utils.raw_sql import use_straight_join class SubmissionStatusFilter(admin.SimpleListFilter): parameter_name = title = 'status' - __lookups = (('None', _('None')), ('NotDone', _('Not done')), ('EX', _('Exceptional'))) + Submission.STATUS + __lookups = ( + ('None', _('None')), + ('NotDone', _('Not done')), + ('EX', _('Exceptional')), + ) + Submission.STATUS __handles = set(map(itemgetter(0), Submission.STATUS)) def lookups(self, request, model_admin): @@ -68,7 +79,9 @@ class ContestSubmissionInline(admin.StackedInline): model = ContestSubmission def get_formset(self, request, obj=None, **kwargs): - kwargs['formfield_callback'] = partial(self.formfield_for_dbfield, request=request, obj=obj) + kwargs['formfield_callback'] = partial( + self.formfield_for_dbfield, request=request, obj=obj + ) return super(ContestSubmissionInline, self).get_formset(request, obj, **kwargs) def formfield_for_dbfield(self, db_field, **kwargs): @@ -76,25 +89,34 @@ def formfield_for_dbfield(self, db_field, **kwargs): label = None if submission: if db_field.name == 'participation': - kwargs['queryset'] = ContestParticipation.objects.filter(user=submission.user, - contest__problems=submission.problem) \ - .only('id', 'contest__name', 'virtual') + kwargs['queryset'] = ContestParticipation.objects.filter( + user=submission.user, contest__problems=submission.problem + ).only('id', 'contest__name', 'virtual') def label(obj): if obj.spectate: return gettext('%s (spectating)') % obj.contest.name if obj.virtual: - return gettext('%s (virtual %d)') % (obj.contest.name, obj.virtual) + return gettext('%s (virtual %d)') % ( + obj.contest.name, + obj.virtual, + ) return obj.contest.name + elif db_field.name == 'problem': - kwargs['queryset'] = ContestProblem.objects.filter(problem=submission.problem) \ - .only('id', 'problem__name', 'contest__name') + kwargs['queryset'] = ContestProblem.objects.filter( + problem=submission.problem + ).only('id', 'problem__name', 'contest__name') def label(obj): return pgettext('contest problem', '%(problem)s in %(contest)s') % { - 'problem': obj.problem.name, 'contest': obj.contest.name, + 'problem': obj.problem.name, + 'contest': obj.contest.name, } - field = super(ContestSubmissionInline, self).formfield_for_dbfield(db_field, **kwargs) + + field = super(ContestSubmissionInline, self).formfield_for_dbfield( + db_field, **kwargs + ) if label is not None: field.label_from_instance = label return field @@ -108,23 +130,54 @@ class SubmissionSourceInline(admin.StackedInline): def get_formset(self, request, obj=None, **kwargs): kwargs.setdefault('widgets', {})['source'] = AceWidget( - mode=obj and obj.language.ace, theme=request.profile.resolved_ace_theme, + mode=obj and obj.language.ace, + theme=request.profile.resolved_ace_theme, ) return super().get_formset(request, obj, **kwargs) class SubmissionAdmin(VersionAdmin): readonly_fields = ('user', 'problem', 'date', 'judged_date') - fields = ('user', 'problem', 'date', 'judged_date', 'locked_after', 'time', 'memory', 'points', 'language', - 'status', 'result', 'case_points', 'case_total', 'judged_on', 'error') + fields = ( + 'user', + 'problem', + 'date', + 'judged_date', + 'locked_after', + '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') + list_display = ( + 'id', + 'problem_code', + 'problem_name', + 'user_column', + 'execution_time', + 'pretty_memory', + 'points', + 'language_column', + 'status', + 'result', + 'judge_column', + ) list_filter = ('language', SubmissionStatusFilter, SubmissionResultFilter) search_fields = ('problem__code', 'problem__name', 'user__user__username') actions_on_top = True actions_on_bottom = True - inlines = [SubmissionSourceInline, SubmissionTestCaseInline, ContestSubmissionInline] + inlines = [ + SubmissionSourceInline, + SubmissionTestCaseInline, + ContestSubmissionInline, + ] def get_readonly_fields(self, request, obj=None): fields = self.readonly_fields @@ -133,14 +186,25 @@ def get_readonly_fields(self, request, obj=None): 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', - 'time', 'memory', 'points', 'status', 'result', + queryset = Submission.objects.select_related( + 'problem', 'user__user', 'language' + ).only( + 'problem__code', + 'problem__name', + 'user__user__username', + 'language__name', + 'time', + 'memory', + 'points', + 'status', + 'result', ) 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.filter( + Q(problem__authors__id=id) | Q(problem__curators__id=id) + ).distinct() return queryset def has_add_permission(self, request): @@ -154,58 +218,111 @@ def has_change_permission(self, request, obj=None): return obj.problem.is_editor(request.profile) def lookup_allowed(self, key, value): - return super(SubmissionAdmin, self).lookup_allowed(key, value) or key in ('problem__code',) + return super(SubmissionAdmin, self).lookup_allowed(key, value) or key in ( + 'problem__code', + ) @admin.display(description=_('Rejudge the selected submissions')) def judge(self, request, queryset): - if not request.user.has_perm('judge.rejudge_submission') or not request.user.has_perm('judge.edit_own_problem'): - self.message_user(request, gettext('You do not have the permission to rejudge submissions.'), - level=messages.ERROR) + if not request.user.has_perm( + 'judge.rejudge_submission' + ) or not request.user.has_perm('judge.edit_own_problem'): + self.message_user( + request, + gettext('You do not have the permission to rejudge submissions.'), + level=messages.ERROR, + ) return queryset = queryset.order_by('id') - if not request.user.has_perm('judge.rejudge_submission_lot') and \ - queryset.count() > settings.DMOJ_SUBMISSIONS_REJUDGE_LIMIT: - self.message_user(request, gettext('You do not have the permission to rejudge THAT many submissions.'), - level=messages.ERROR) + if ( + not request.user.has_perm('judge.rejudge_submission_lot') + and queryset.count() > settings.DMOJ_SUBMISSIONS_REJUDGE_LIMIT + ): + self.message_user( + request, + gettext( + 'You do not have the permission to rejudge THAT many submissions.' + ), + 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.filter( + Q(problem__authors__id=id) | Q(problem__curators__id=id) + ) judged = len(queryset) for model in queryset: model.judge(rejudge=True, batch_rejudge=True, rejudge_user=request.user) - self.message_user(request, ngettext('%d submission was successfully scheduled for rejudging.', - '%d submissions were successfully scheduled for rejudging.', - judged) % judged) + self.message_user( + request, + ngettext( + '%d submission was successfully scheduled for rejudging.', + '%d submissions were successfully scheduled for rejudging.', + judged, + ) + % judged, + ) @admin.display(description=_('Rescore the selected submissions')) def recalculate_score(self, request, queryset): if not request.user.has_perm('judge.rejudge_submission'): - self.message_user(request, gettext('You do not have the permission to rejudge submissions.'), - level=messages.ERROR) + self.message_user( + request, + gettext('You do not have the permission to rejudge submissions.'), + level=messages.ERROR, + ) return - submissions = list(queryset.defer(None).select_related(None).select_related('problem') - .only('points', 'case_points', 'case_total', 'problem__partial', 'problem__points')) + submissions = list( + queryset.defer(None) + .select_related(None) + .select_related('problem') + .only( + 'points', + 'case_points', + 'case_total', + 'problem__partial', + 'problem__points', + ) + ) for submission in submissions: - submission.points = round(submission.case_points / submission.case_total * submission.problem.points - if submission.case_total else 0, 1) - if not submission.problem.partial and submission.points < submission.problem.points: + submission.points = round( + submission.case_points + / submission.case_total + * submission.problem.points + if submission.case_total + else 0, + 1, + ) + if ( + not submission.problem.partial + and submission.points < submission.problem.points + ): submission.points = 0 submission.save() submission.update_contest() - for profile in Profile.objects.filter(id__in=queryset.values_list('user_id', flat=True).distinct()): + for profile in Profile.objects.filter( + id__in=queryset.values_list('user_id', flat=True).distinct() + ): profile.calculate_points() cache.delete('user_complete:%d' % profile.id) cache.delete('user_attempted:%d' % profile.id) for participation in ContestParticipation.objects.filter( - id__in=queryset.values_list('contest__participation_id')).prefetch_related('contest'): + id__in=queryset.values_list('contest__participation_id') + ).prefetch_related('contest'): participation.recompute_results() - self.message_user(request, ngettext('%d submission was successfully rescored.', - '%d submissions were successfully rescored.', - len(submissions)) % len(submissions)) + self.message_user( + request, + ngettext( + '%d submission was successfully rescored.', + '%d submissions were successfully rescored.', + len(submissions), + ) + % len(submissions), + ) @admin.display(description=_('problem code'), ordering='problem__code') def problem_code(self, obj): @@ -240,10 +357,15 @@ def language_column(self, obj): @admin.display(description='') def judge_column(self, obj): if obj.is_locked: - return format_html('', _('Locked')) + return format_html( + '', _('Locked') + ) else: - return format_html('', _('Rejudge'), - reverse('admin:judge_submission_rejudge', args=(obj.id,))) + return format_html( + '', + _('Rejudge'), + reverse('admin:judge_submission_rejudge', args=(obj.id,)), + ) def get_urls(self): return [ @@ -251,11 +373,14 @@ def get_urls(self): ] + super(SubmissionAdmin, self).get_urls() def judge_view(self, request, id): - if not request.user.has_perm('judge.rejudge_submission') or not request.user.has_perm('judge.edit_own_problem'): + if not request.user.has_perm( + 'judge.rejudge_submission' + ) or not request.user.has_perm('judge.edit_own_problem'): raise PermissionDenied() submission = get_object_or_404(Submission, id=id) - if not request.user.has_perm('judge.edit_all_problem') and \ - not submission.problem.is_editor(request.profile): + if not request.user.has_perm( + 'judge.edit_all_problem' + ) and not submission.problem.is_editor(request.profile): raise PermissionDenied() submission.judge(rejudge=True, rejudge_user=request.user) return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/')) diff --git a/judge/admin/taxon.py b/judge/admin/taxon.py index aa245d7dc7..d0a69370eb 100644 --- a/judge/admin/taxon.py +++ b/judge/admin/taxon.py @@ -12,7 +12,8 @@ class ProblemGroupForm(ModelForm): queryset=Problem.objects.all(), required=False, help_text=_('These problems are included in this group of problems.'), - widget=AdminHeavySelect2MultipleWidget(data_view='problem_select2')) + widget=AdminHeavySelect2MultipleWidget(data_view='problem_select2'), + ) class ProblemGroupAdmin(admin.ModelAdmin): @@ -25,7 +26,9 @@ def save_model(self, request, obj, form, change): obj.save() def get_form(self, request, obj=None, **kwargs): - self.form.base_fields['problems'].initial = [o.pk for o in obj.problem_set.all()] if obj else [] + self.form.base_fields['problems'].initial = ( + [o.pk for o in obj.problem_set.all()] if obj else [] + ) return super(ProblemGroupAdmin, self).get_form(request, obj, **kwargs) @@ -35,7 +38,8 @@ class ProblemTypeForm(ModelForm): queryset=Problem.objects.all(), required=False, help_text=_('These problems are included in this type of problems.'), - widget=AdminHeavySelect2MultipleWidget(data_view='problem_select2')) + widget=AdminHeavySelect2MultipleWidget(data_view='problem_select2'), + ) class ProblemTypeAdmin(admin.ModelAdmin): @@ -48,5 +52,7 @@ def save_model(self, request, obj, form, change): obj.save() def get_form(self, request, obj=None, **kwargs): - self.form.base_fields['problems'].initial = [o.pk for o in obj.problem_set.all()] if obj else [] + self.form.base_fields['problems'].initial = ( + [o.pk for o in obj.problem_set.all()] if obj else [] + ) return super(ProblemTypeAdmin, self).get_form(request, obj, **kwargs) diff --git a/judge/admin/ticket.py b/judge/admin/ticket.py index 737bee53cd..4507fe5498 100644 --- a/judge/admin/ticket.py +++ b/judge/admin/ticket.py @@ -4,14 +4,22 @@ from django.urls import reverse_lazy from judge.models import TicketMessage -from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, AdminMartorWidget +from judge.widgets import ( + AdminHeavySelect2MultipleWidget, + AdminHeavySelect2Widget, + AdminMartorWidget, +) class TicketMessageForm(ModelForm): class Meta: widgets = { - 'user': AdminHeavySelect2Widget(data_view='profile_select2', attrs={'style': 'width: 100%'}), - 'body': AdminMartorWidget(attrs={'data-markdownfy-url': reverse_lazy('ticket_preview')}), + 'user': AdminHeavySelect2Widget( + data_view='profile_select2', attrs={'style': 'width: 100%'} + ), + 'body': AdminMartorWidget( + attrs={'data-markdownfy-url': reverse_lazy('ticket_preview')} + ), } @@ -24,13 +32,25 @@ class TicketMessageInline(StackedInline): class TicketForm(ModelForm): class Meta: widgets = { - 'user': AdminHeavySelect2Widget(data_view='profile_select2', attrs={'style': 'width: 100%'}), - 'assignees': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}), + 'user': AdminHeavySelect2Widget( + data_view='profile_select2', attrs={'style': 'width: 100%'} + ), + 'assignees': AdminHeavySelect2MultipleWidget( + data_view='profile_select2', attrs={'style': 'width: 100%'} + ), } class TicketAdmin(ModelAdmin): - fields = ('title', 'time', 'user', 'assignees', 'content_type', 'object_id', 'notes') + fields = ( + 'title', + 'time', + 'user', + 'assignees', + 'content_type', + 'object_id', + 'notes', + ) readonly_fields = ('time',) list_display = ('title', 'user', 'time', 'linked_item') inlines = [TicketMessageInline] diff --git a/judge/bridge/base_handler.py b/judge/bridge/base_handler.py index e3ed9c574a..85ae2685b1 100644 --- a/judge/bridge/base_handler.py +++ b/judge/bridge/base_handler.py @@ -70,8 +70,12 @@ def timeout(self, timeout): def read_sized_packet(self, size, initial=None): if size > MAX_ALLOWED_PACKET_SIZE: - logger.log(logging.WARNING if self._got_packet else logging.INFO, - 'Disconnecting client due to too-large message size (%d bytes): %s', size, self.client_address) + logger.log( + logging.WARNING if self._got_packet else logging.INFO, + 'Disconnecting client due to too-large message size (%d bytes): %s', + size, + self.client_address, + ) raise Disconnect() buffer = [] @@ -149,7 +153,9 @@ def handle(self): tag = self.read_size() self._initial_tag = size_pack.pack(tag) if self.client_address[0] in self.proxies and self._initial_tag == b'PROX': - proxy, _, remainder = self.read_proxy_header(self._initial_tag).partition(b'\r\n') + proxy, _, remainder = self.read_proxy_header( + self._initial_tag + ).partition(b'\r\n') self.parse_proxy_protocol(proxy) while remainder: @@ -157,8 +163,8 @@ def handle(self): self.read_sized_packet(self.read_size(remainder)) break - size = size_pack.unpack(remainder[:size_pack.size])[0] - remainder = remainder[size_pack.size:] + size = size_pack.unpack(remainder[: size_pack.size])[0] + remainder = remainder[size_pack.size :] if len(remainder) <= size: self.read_sized_packet(size, remainder) break @@ -174,17 +180,28 @@ def handle(self): return except zlib.error: if self._got_packet: - logger.warning('Encountered zlib error during packet handling, disconnecting client: %s', - self.client_address, exc_info=True) + logger.warning( + 'Encountered zlib error during packet handling, disconnecting client: %s', + self.client_address, + exc_info=True, + ) else: - logger.info('Potentially wrong protocol (zlib error): %s: %r', self.client_address, self._initial_tag, - exc_info=True) + logger.info( + 'Potentially wrong protocol (zlib error): %s: %r', + self.client_address, + self._initial_tag, + exc_info=True, + ) except socket.timeout: if self._got_packet: logger.info('Socket timed out: %s', self.client_address) self.on_timeout() else: - logger.info('Potentially wrong protocol: %s: %r', self.client_address, self._initial_tag) + logger.info( + 'Potentially wrong protocol: %s: %r', + self.client_address, + self._initial_tag, + ) except socket.error as e: # When a gevent socket is shutdown, gevent cancels all waits, causing recv to raise cancel_wait_ex. if e.__class__.__name__ == 'cancel_wait_ex': diff --git a/judge/bridge/daemon.py b/judge/bridge/daemon.py index ce8a702dbd..048aa4aecc 100644 --- a/judge/bridge/daemon.py +++ b/judge/bridge/daemon.py @@ -20,12 +20,17 @@ def reset_judges(): def judge_daemon(): reset_judges() - Submission.objects.filter(status__in=Submission.IN_PROGRESS_GRADING_STATUS) \ - .update(status='IE', result='IE', error=None) + Submission.objects.filter(status__in=Submission.IN_PROGRESS_GRADING_STATUS).update( + status='IE', result='IE', error=None + ) judges = JudgeList() - judge_server = Server(settings.BRIDGED_JUDGE_ADDRESS, partial(JudgeHandler, judges=judges)) - django_server = Server(settings.BRIDGED_DJANGO_ADDRESS, partial(DjangoHandler, judges=judges)) + judge_server = Server( + settings.BRIDGED_JUDGE_ADDRESS, partial(JudgeHandler, judges=judges) + ) + django_server = Server( + settings.BRIDGED_DJANGO_ADDRESS, partial(DjangoHandler, judges=judges) + ) threading.Thread(target=django_server.serve_forever).start() threading.Thread(target=judge_server.serve_forever).start() diff --git a/judge/bridge/django_handler.py b/judge/bridge/django_handler.py index cdde06e0dd..515eeee7b5 100644 --- a/judge/bridge/django_handler.py +++ b/judge/bridge/django_handler.py @@ -28,7 +28,9 @@ def send(self, data): def on_packet(self, packet): packet = json.loads(packet) try: - result = self.handlers.get(packet.get('name', None), self.on_malformed)(packet) + result = self.handlers.get(packet.get('name', None), self.on_malformed)( + packet + ) except Exception: logger.exception('Error in packet handling (Django-facing)') result = {'name': 'bad-request'} @@ -48,7 +50,10 @@ def on_submission(self, data): return {'name': 'submission-received', 'submission-id': id} def on_termination(self, data): - return {'name': 'submission-received', 'judge-aborted': self.judges.abort(data['submission-id'])} + return { + 'name': 'submission-received', + 'judge-aborted': self.judges.abort(data['submission-id']), + } def on_disconnect_request(self, data): judge_id = data['judge-id'] diff --git a/judge/bridge/echo_test_client.py b/judge/bridge/echo_test_client.py index 8fec692aaf..762edfddcc 100644 --- a/judge/bridge/echo_test_client.py +++ b/judge/bridge/echo_test_client.py @@ -19,13 +19,14 @@ def zlibify(data): def dezlibify(data, skip_head=True): if skip_head: - data = data[size_pack.size:] + data = data[size_pack.size :] return zlib.decompress(data).decode('utf-8') def main(): global host, port import argparse + parser = argparse.ArgumentParser() parser.add_argument('-l', '--host', default='localhost') parser.add_argument('-p', '--port', default=9999, type=int) @@ -58,8 +59,8 @@ def main(): result = b'' while len(result) < size_pack.size: result += s4.recv(1024) - size = size_pack.unpack(result[:size_pack.size])[0] - result = result[size_pack.size:] + size = size_pack.unpack(result[: size_pack.size])[0] + result = result[size_pack.size :] while len(result) < size: result += s4.recv(1024) print('Received', end=' ') diff --git a/judge/bridge/echo_test_server.py b/judge/bridge/echo_test_server.py index 59e21fa223..412688c8fa 100644 --- a/judge/bridge/echo_test_server.py +++ b/judge/bridge/echo_test_server.py @@ -11,7 +11,10 @@ def on_timeout(self): def on_packet(self, data): self.timeout = None - print('Data from %s: %r' % (self.client_address, data[:30] if len(data) > 30 else data)) + print( + 'Data from %s: %r' + % (self.client_address, data[:30] if len(data) > 30 else data) + ) self.send(data) def on_disconnect(self): diff --git a/judge/bridge/judge_handler.py b/judge/bridge/judge_handler.py index 073de27262..b13b94e04f 100644 --- a/judge/bridge/judge_handler.py +++ b/judge/bridge/judge_handler.py @@ -13,14 +13,25 @@ from judge import event_poster as event from judge.bridge.base_handler import ZlibPacketHandler, proxy_list from judge.caching import finished_submission -from judge.models import Judge, Language, LanguageLimit, Problem, RuntimeVersion, Submission, SubmissionTestCase +from judge.models import ( + Judge, + Language, + LanguageLimit, + Problem, + RuntimeVersion, + Submission, + SubmissionTestCase, +) logger = logging.getLogger('judge.bridge') json_log = logging.getLogger('judge.json.bridge') UPDATE_RATE_LIMIT = 5 UPDATE_RATE_TIME = 0.5 -SubmissionData = namedtuple('SubmissionData', 'time memory short_circuit pretests_only contest_no attempt_no user_id') +SubmissionData = namedtuple( + 'SubmissionData', + 'time memory short_circuit pretests_only contest_no attempt_no user_id', +) def _ensure_connection(): @@ -81,16 +92,32 @@ def on_connect(self): def on_disconnect(self): self._stop_ping.set() if self._working: - logger.error('Judge %s disconnected while handling submission %s', self.name, self._working) + logger.error( + 'Judge %s disconnected while handling submission %s', + self.name, + self._working, + ) self.judges.remove(self) if self.name is not None: self._disconnected() - logger.info('Judge disconnected from: %s with name %s', self.client_address, self.name) + logger.info( + 'Judge disconnected from: %s with name %s', self.client_address, self.name + ) - json_log.info(self._make_json_log(action='disconnect', info='judge disconnected')) + json_log.info( + self._make_json_log(action='disconnect', info='judge disconnected') + ) if self._working: - Submission.objects.filter(id=self._working).update(status='IE', result='IE', error='') - json_log.error(self._make_json_log(sub=self._working, action='close', info='IE due to shutdown on grading')) + Submission.objects.filter(id=self._working).update( + status='IE', result='IE', error='' + ) + json_log.error( + self._make_json_log( + sub=self._working, + action='close', + info='IE due to shutdown on grading', + ) + ) def _authenticate(self, id, key): try: @@ -100,11 +127,19 @@ def _authenticate(self, id, key): if not hmac.compare_digest(judge.auth_key, key): logger.warning('Judge authentication failure: %s', self.client_address) - json_log.warning(self._make_json_log(action='auth', judge=id, info='judge failed authentication')) + json_log.warning( + self._make_json_log( + action='auth', judge=id, info='judge failed authentication' + ) + ) return False if judge.is_blocked: - json_log.warning(self._make_json_log(action='auth', judge=id, info='judge authenticated but is blocked')) + json_log.warning( + self._make_json_log( + action='auth', judge=id, info='judge authenticated but is blocked' + ) + ) return False return True @@ -124,15 +159,29 @@ def _connected(self): versions = [] for lang in judge.runtimes.all(): versions += [ - RuntimeVersion(language=lang, name=name, version='.'.join(map(str, version)), priority=idx, judge=judge) + RuntimeVersion( + language=lang, + name=name, + version='.'.join(map(str, version)), + priority=idx, + judge=judge, + ) for idx, (name, version) in enumerate(self.executors[lang.key]) ] RuntimeVersion.objects.bulk_create(versions) judge.last_ip = self.client_address[0] judge.save() - self.judge_address = '[%s]:%s' % (self.client_address[0], self.client_address[1]) - json_log.info(self._make_json_log(action='auth', info='judge successfully authenticated', - executors=list(self.executors.keys()))) + self.judge_address = '[%s]:%s' % ( + self.client_address[0], + self.client_address[1], + ) + json_log.info( + self._make_json_log( + action='auth', + info='judge successfully authenticated', + executors=list(self.executors.keys()), + ) + ) def _disconnected(self): Judge.objects.filter(id=self.judge.id).update(online=False) @@ -140,10 +189,16 @@ def _disconnected(self): def _update_ping(self): try: - Judge.objects.filter(name=self.name).update(ping=self.latency, load=self.load) + Judge.objects.filter(name=self.name).update( + ping=self.latency, load=self.load + ) except Exception as e: # What can I do? I don't want to tie this to MySQL. - if e.__class__.__name__ == 'OperationalError' and e.__module__ == '_mysql_exceptions' and e.args[0] == 2006: + if ( + e.__class__.__name__ == 'OperationalError' + and e.__module__ == '_mysql_exceptions' + and e.args[0] == 2006 + ): db.connection.close() def send(self, data): @@ -172,8 +227,11 @@ def on_handshake(self, packet): self._connected() def can_judge(self, problem, executor, judge_id=None): - return problem in self.problems and executor in self.executors and \ - ((not judge_id and not self.is_disabled) or self.name == judge_id) + return ( + problem in self.problems + and executor in self.executors + and ((not judge_id and not self.is_disabled) or self.name == judge_id) + ) @property def working(self): @@ -183,25 +241,60 @@ def get_related_submission_data(self, submission): _ensure_connection() try: - pid, time, memory, short_circuit, lid, is_pretested, sub_date, uid, part_virtual, part_id = ( - Submission.objects.filter(id=submission) - .values_list('problem__id', 'problem__time_limit', 'problem__memory_limit', - 'problem__short_circuit', 'language__id', 'is_pretested', 'date', 'user__id', - 'contest__participation__virtual', 'contest__participation__id')).get() + ( + pid, + time, + memory, + short_circuit, + lid, + is_pretested, + sub_date, + uid, + part_virtual, + part_id, + ) = ( + Submission.objects.filter(id=submission).values_list( + 'problem__id', + 'problem__time_limit', + 'problem__memory_limit', + 'problem__short_circuit', + 'language__id', + 'is_pretested', + 'date', + 'user__id', + 'contest__participation__virtual', + 'contest__participation__id', + ) + ).get() except Submission.DoesNotExist: logger.error('Submission vanished: %s', submission) - json_log.error(self._make_json_log( - sub=self._working, action='request', - info='submission vanished when fetching info', - )) + json_log.error( + self._make_json_log( + sub=self._working, + action='request', + info='submission vanished when fetching info', + ) + ) return - attempt_no = Submission.objects.filter(problem__id=pid, contest__participation__id=part_id, user__id=uid, - date__lt=sub_date).exclude(status__in=('CE', 'IE')).count() + 1 + attempt_no = ( + Submission.objects.filter( + problem__id=pid, + contest__participation__id=part_id, + user__id=uid, + date__lt=sub_date, + ) + .exclude(status__in=('CE', 'IE')) + .count() + + 1 + ) try: - time, memory = (LanguageLimit.objects.filter(problem__id=pid, language__id=lid) - .values_list('time_limit', 'memory_limit').get()) + time, memory = ( + LanguageLimit.objects.filter(problem__id=pid, language__id=lid) + .values_list('time_limit', 'memory_limit') + .get() + ) except LanguageLimit.DoesNotExist: pass @@ -226,25 +319,29 @@ def submit(self, id, problem, language, source): data = self.get_related_submission_data(id) self._working = id self._no_response_job = threading.Timer(20, self._kill_if_no_response) - self.send({ - 'name': 'submission-request', - 'submission-id': id, - 'problem-id': problem, - 'language': language, - 'source': source, - 'time-limit': data.time, - 'memory-limit': data.memory, - 'short-circuit': data.short_circuit, - 'meta': { - 'pretests-only': data.pretests_only, - 'in-contest': data.contest_no, - 'attempt-no': data.attempt_no, - 'user': data.user_id, - }, - }) + self.send( + { + 'name': 'submission-request', + 'submission-id': id, + 'problem-id': problem, + 'language': language, + 'source': source, + 'time-limit': data.time, + 'memory-limit': data.memory, + 'short-circuit': data.short_circuit, + 'meta': { + 'pretests-only': data.pretests_only, + 'in-contest': data.contest_no, + 'attempt-no': data.attempt_no, + 'user': data.user_id, + }, + } + ) def _kill_if_no_response(self): - logger.error('Judge failed to acknowledge submission: %s: %s', self.name, self._working) + logger.error( + 'Judge failed to acknowledge submission: %s: %s', self.name, self._working + ) self.close() def on_timeout(self): @@ -261,18 +358,36 @@ def on_submission_processing(self, packet): json_log.info(self._make_json_log(packet, action='processing')) else: logger.warning('Unknown submission: %s', id) - json_log.error(self._make_json_log(packet, action='processing', info='unknown submission')) + json_log.error( + self._make_json_log( + packet, action='processing', info='unknown submission' + ) + ) def on_submission_wrong_acknowledge(self, packet, expected, got): - json_log.error(self._make_json_log(packet, action='processing', info='wrong-acknowledge', expected=expected)) - Submission.objects.filter(id=expected).update(status='IE', result='IE', error=None) - Submission.objects.filter(id=got, status='QU').update(status='IE', result='IE', error=None) + json_log.error( + self._make_json_log( + packet, action='processing', info='wrong-acknowledge', expected=expected + ) + ) + Submission.objects.filter(id=expected).update( + status='IE', result='IE', error=None + ) + Submission.objects.filter(id=got, status='QU').update( + status='IE', result='IE', error=None + ) def on_submission_acknowledged(self, packet): if not packet.get('submission-id', None) == self._working: - logger.error('Wrong acknowledgement: %s: %s, expected: %s', self.name, packet.get('submission-id', None), - self._working) - self.on_submission_wrong_acknowledge(packet, self._working, packet.get('submission-id', None)) + logger.error( + 'Wrong acknowledgement: %s: %s, expected: %s', + self.name, + packet.get('submission-id', None), + self._working, + ) + self.on_submission_wrong_acknowledge( + packet, self._working, packet.get('submission-id', None) + ) self.close() logger.info('Submission acknowledged: %d', self._working) if self._no_response_job: @@ -307,7 +422,9 @@ def on_packet(self, data): # not being malicious or simply malformed. THIS IS A SERVER! def _packet_exception(self): - json_log.exception(self._make_json_log(sub=self._working, info='packet processing exception')) + json_log.exception( + self._make_json_log(sub=self._working, info='packet processing exception') + ) def _submission_is_batch(self, id): if not Submission.objects.filter(id=id).update(batch=True): @@ -320,23 +437,40 @@ def on_supported_problems(self, packet): if not self.working: self.judges.update_problems(self) - self.judge.problems.set(Problem.objects.filter(code__in=list(self.problems.keys()))) - json_log.info(self._make_json_log(action='update-problems', count=len(self.problems))) + self.judge.problems.set( + Problem.objects.filter(code__in=list(self.problems.keys())) + ) + json_log.info( + self._make_json_log(action='update-problems', count=len(self.problems)) + ) def on_grading_begin(self, packet): logger.info('%s: Grading has begun on: %s', self.name, packet['submission-id']) self.batch_id = None if Submission.objects.filter(id=packet['submission-id']).update( - status='G', is_pretested=packet['pretested'], current_testcase=1, - batch=False, judged_date=timezone.now()): - SubmissionTestCase.objects.filter(submission_id=packet['submission-id']).delete() - event.post('sub_%s' % Submission.get_id_secret(packet['submission-id']), {'type': 'grading-begin'}) + status='G', + is_pretested=packet['pretested'], + current_testcase=1, + batch=False, + judged_date=timezone.now(), + ): + SubmissionTestCase.objects.filter( + submission_id=packet['submission-id'] + ).delete() + event.post( + 'sub_%s' % Submission.get_id_secret(packet['submission-id']), + {'type': 'grading-begin'}, + ) self._post_update_submission(packet['submission-id'], 'grading-begin') json_log.info(self._make_json_log(packet, action='grading-begin')) else: logger.warning('Unknown submission: %s', packet['submission-id']) - json_log.error(self._make_json_log(packet, action='grading-begin', info='unknown submission')) + json_log.error( + self._make_json_log( + packet, action='grading-begin', info='unknown submission' + ) + ) def on_grading_end(self, packet): logger.info('%s: Grading has ended on: %s', self.name, packet['submission-id']) @@ -347,7 +481,11 @@ def on_grading_end(self, packet): submission = Submission.objects.get(id=packet['submission-id']) except Submission.DoesNotExist: logger.warning('Unknown submission: %s', packet['submission-id']) - json_log.error(self._make_json_log(packet, action='grading-end', info='unknown submission')) + json_log.error( + self._make_json_log( + packet, action='grading-end', info='unknown submission' + ) + ) return time = 0 @@ -395,12 +533,22 @@ def on_grading_end(self, packet): submission.result = status_codes[status] submission.save() - json_log.info(self._make_json_log( - packet, action='grading-end', time=time, memory=memory, - points=sub_points, total=problem.points, result=submission.result, - case_points=points, case_total=total, user=submission.user_id, - problem=problem.code, finish=True, - )) + json_log.info( + self._make_json_log( + packet, + action='grading-end', + time=time, + memory=memory, + points=sub_points, + total=problem.points, + result=submission.result, + case_points=points, + case_total=total, + user=submission.user_id, + problem=problem.code, + finish=True, + ) + ) if problem.is_public and not problem.is_organization_private: submission.user._updating_stats_only = True @@ -412,77 +560,158 @@ def on_grading_end(self, packet): finished_submission(submission) - event.post('sub_%s' % submission.id_secret, { - 'type': 'grading-end', - 'time': time, - 'memory': memory, - 'points': float(points), - 'total': float(problem.points), - 'result': submission.result, - }) + event.post( + 'sub_%s' % submission.id_secret, + { + 'type': 'grading-end', + 'time': time, + 'memory': memory, + 'points': float(points), + 'total': float(problem.points), + 'result': submission.result, + }, + ) if hasattr(submission, 'contest'): participation = submission.contest.participation event.post('contest_%d' % participation.contest_id, {'type': 'update'}) self._post_update_submission(submission.id, 'grading-end', done=True) def on_compile_error(self, packet): - logger.info('%s: Submission failed to compile: %s', self.name, packet['submission-id']) + logger.info( + '%s: Submission failed to compile: %s', self.name, packet['submission-id'] + ) self._free_self(packet) - if Submission.objects.filter(id=packet['submission-id']).update(status='CE', result='CE', error=packet['log']): - event.post('sub_%s' % Submission.get_id_secret(packet['submission-id']), { - 'type': 'compile-error', - 'log': packet['log'], - }) - self._post_update_submission(packet['submission-id'], 'compile-error', done=True) - json_log.info(self._make_json_log(packet, action='compile-error', log=packet['log'], - finish=True, result='CE')) + if Submission.objects.filter(id=packet['submission-id']).update( + status='CE', result='CE', error=packet['log'] + ): + event.post( + 'sub_%s' % Submission.get_id_secret(packet['submission-id']), + { + 'type': 'compile-error', + 'log': packet['log'], + }, + ) + self._post_update_submission( + packet['submission-id'], 'compile-error', done=True + ) + json_log.info( + self._make_json_log( + packet, + action='compile-error', + log=packet['log'], + finish=True, + result='CE', + ) + ) else: logger.warning('Unknown submission: %s', packet['submission-id']) - json_log.error(self._make_json_log(packet, action='compile-error', info='unknown submission', - log=packet['log'], finish=True, result='CE')) + json_log.error( + self._make_json_log( + packet, + action='compile-error', + info='unknown submission', + log=packet['log'], + finish=True, + result='CE', + ) + ) def on_compile_message(self, packet): - logger.info('%s: Submission generated compiler messages: %s', self.name, packet['submission-id']) + logger.info( + '%s: Submission generated compiler messages: %s', + self.name, + packet['submission-id'], + ) - if Submission.objects.filter(id=packet['submission-id']).update(error=packet['log']): - event.post('sub_%s' % Submission.get_id_secret(packet['submission-id']), {'type': 'compile-message'}) - json_log.info(self._make_json_log(packet, action='compile-message', log=packet['log'])) + if Submission.objects.filter(id=packet['submission-id']).update( + error=packet['log'] + ): + event.post( + 'sub_%s' % Submission.get_id_secret(packet['submission-id']), + {'type': 'compile-message'}, + ) + json_log.info( + self._make_json_log(packet, action='compile-message', log=packet['log']) + ) else: logger.warning('Unknown submission: %s', packet['submission-id']) - json_log.error(self._make_json_log(packet, action='compile-message', info='unknown submission', - log=packet['log'])) + json_log.error( + self._make_json_log( + packet, + action='compile-message', + info='unknown submission', + log=packet['log'], + ) + ) def on_internal_error(self, packet): try: raise ValueError('\n\n' + packet['message']) except ValueError: - logger.exception('Judge %s failed while handling submission %s', self.name, packet['submission-id']) + logger.exception( + 'Judge %s failed while handling submission %s', + self.name, + packet['submission-id'], + ) self._free_self(packet) id = packet['submission-id'] - if Submission.objects.filter(id=id).update(status='IE', result='IE', error=packet['message']): - event.post('sub_%s' % Submission.get_id_secret(id), {'type': 'internal-error'}) + if Submission.objects.filter(id=id).update( + status='IE', result='IE', error=packet['message'] + ): + event.post( + 'sub_%s' % Submission.get_id_secret(id), {'type': 'internal-error'} + ) self._post_update_submission(id, 'internal-error', done=True) - json_log.info(self._make_json_log(packet, action='internal-error', message=packet['message'], - finish=True, result='IE')) + json_log.info( + self._make_json_log( + packet, + action='internal-error', + message=packet['message'], + finish=True, + result='IE', + ) + ) else: logger.warning('Unknown submission: %s', id) - json_log.error(self._make_json_log(packet, action='internal-error', info='unknown submission', - message=packet['message'], finish=True, result='IE')) + json_log.error( + self._make_json_log( + packet, + action='internal-error', + info='unknown submission', + message=packet['message'], + finish=True, + result='IE', + ) + ) def on_submission_terminated(self, packet): logger.info('%s: Submission aborted: %s', self.name, packet['submission-id']) self._free_self(packet) - if Submission.objects.filter(id=packet['submission-id']).update(status='AB', result='AB', points=0): - event.post('sub_%s' % Submission.get_id_secret(packet['submission-id']), {'type': 'aborted'}) + if Submission.objects.filter(id=packet['submission-id']).update( + status='AB', result='AB', points=0 + ): + event.post( + 'sub_%s' % Submission.get_id_secret(packet['submission-id']), + {'type': 'aborted'}, + ) self._post_update_submission(packet['submission-id'], 'aborted', done=True) - json_log.info(self._make_json_log(packet, action='aborted', finish=True, result='AB')) + json_log.info( + self._make_json_log(packet, action='aborted', finish=True, result='AB') + ) else: logger.warning('Unknown submission: %s', packet['submission-id']) - json_log.error(self._make_json_log(packet, action='aborted', info='unknown submission', - finish=True, result='AB')) + json_log.error( + self._make_json_log( + packet, + action='aborted', + info='unknown submission', + finish=True, + result='AB', + ) + ) def on_batch_begin(self, packet): logger.info('%s: Batch began on: %s', self.name, packet['submission-id']) @@ -492,23 +721,42 @@ def on_batch_begin(self, packet): self._submission_is_batch(packet['submission-id']) self.batch_id += 1 - json_log.info(self._make_json_log(packet, action='batch-begin', batch=self.batch_id)) + json_log.info( + self._make_json_log(packet, action='batch-begin', batch=self.batch_id) + ) def on_batch_end(self, packet): self.in_batch = False logger.info('%s: Batch ended on: %s', self.name, packet['submission-id']) - json_log.info(self._make_json_log(packet, action='batch-end', batch=self.batch_id)) + json_log.info( + self._make_json_log(packet, action='batch-end', batch=self.batch_id) + ) - def on_test_case(self, packet, max_feedback=SubmissionTestCase._meta.get_field('feedback').max_length): - logger.info('%s: %d test case(s) completed on: %s', self.name, len(packet['cases']), packet['submission-id']) + def on_test_case( + self, + packet, + max_feedback=SubmissionTestCase._meta.get_field('feedback').max_length, + ): + logger.info( + '%s: %d test case(s) completed on: %s', + self.name, + len(packet['cases']), + packet['submission-id'], + ) id = packet['submission-id'] updates = packet['cases'] max_position = max(map(itemgetter('position'), updates)) - if not Submission.objects.filter(id=id).update(current_testcase=max_position + 1): + if not Submission.objects.filter(id=id).update( + current_testcase=max_position + 1 + ): logger.warning('Unknown submission: %s', id) - json_log.error(self._make_json_log(packet, action='test-case', info='unknown submission')) + json_log.error( + self._make_json_log( + packet, action='test-case', info='unknown submission' + ) + ) return bulk_test_case_updates = [] @@ -541,15 +789,29 @@ def on_test_case(self, packet, max_feedback=SubmissionTestCase._meta.get_field(' test_case.output = result['output'] bulk_test_case_updates.append(test_case) - json_log.info(self._make_json_log( - packet, action='test-case', case=test_case.case, batch=test_case.batch, - time=test_case.time, memory=test_case.memory, feedback=test_case.feedback, - extended_feedback=test_case.extended_feedback, output=test_case.output, - points=test_case.points, total=test_case.total, status=test_case.status, - voluntary_context_switches=result.get('voluntary-context-switches', 0), - involuntary_context_switches=result.get('involuntary-context-switches', 0), - runtime_version=result.get('runtime-version', ''), - )) + json_log.info( + self._make_json_log( + packet, + action='test-case', + case=test_case.case, + batch=test_case.batch, + time=test_case.time, + memory=test_case.memory, + feedback=test_case.feedback, + extended_feedback=test_case.extended_feedback, + output=test_case.output, + points=test_case.points, + total=test_case.total, + status=test_case.status, + voluntary_context_switches=result.get( + 'voluntary-context-switches', 0 + ), + involuntary_context_switches=result.get( + 'involuntary-context-switches', 0 + ), + runtime_version=result.get('runtime-version', ''), + ) + ) do_post = True @@ -566,17 +828,22 @@ def on_test_case(self, packet, max_feedback=SubmissionTestCase._meta.get_field(' self.update_counter[id] = (1, time.monotonic()) if do_post: - event.post('sub_%s' % Submission.get_id_secret(id), { - 'type': 'test-case', - 'id': max_position, - }) + event.post( + 'sub_%s' % Submission.get_id_secret(id), + { + 'type': 'test-case', + 'id': max_position, + }, + ) self._post_update_submission(id, state='test-case') SubmissionTestCase.objects.bulk_create(bulk_test_case_updates) def on_malformed(self, packet): logger.error('%s: Malformed packet: %s', self.name, packet) - json_log.exception(self._make_json_log(sub=self._working, info='malformed json packet')) + json_log.exception( + self._make_json_log(sub=self._working, info='malformed json packet') + ) def on_ping_response(self, packet): end = time.time() @@ -617,20 +884,34 @@ def _post_update_submission(self, id, state, done=False): if self._submission_cache_id == id: data = self._submission_cache else: - self._submission_cache = data = Submission.objects.filter(id=id).values( - 'problem__is_public', 'contest_object_id', - 'user_id', 'problem_id', 'status', 'language__key', - ).get() + self._submission_cache = data = ( + Submission.objects.filter(id=id) + .values( + 'problem__is_public', + 'contest_object_id', + 'user_id', + 'problem_id', + 'status', + 'language__key', + ) + .get() + ) self._submission_cache_id = id if data['problem__is_public']: - event.post('submissions', { - 'type': 'done-submission' if done else 'update-submission', - 'state': state, 'id': id, - 'contest': data['contest_object_id'], - 'user': data['user_id'], 'problem': data['problem_id'], - 'status': data['status'], 'language': data['language__key'], - }) + event.post( + 'submissions', + { + 'type': 'done-submission' if done else 'update-submission', + 'state': state, + 'id': id, + 'contest': data['contest_object_id'], + 'user': data['user_id'], + 'problem': data['problem_id'], + 'status': data['status'], + 'language': data['language__key'], + }, + ) def on_cleanup(self): db.connection.close() diff --git a/judge/bridge/judge_list.py b/judge/bridge/judge_list.py index b65d0ddb82..5d8cd9a2f7 100644 --- a/judge/bridge/judge_list.py +++ b/judge/bridge/judge_list.py @@ -20,7 +20,9 @@ class JudgeList(object): def __init__(self): self.queue = dllist() - self.priority = [self.queue.append(PriorityMarker(i)) for i in range(self.priorities)] + self.priority = [ + self.queue.append(PriorityMarker(i)) for i in range(self.priorities) + ] self.judges = set() self.node_map = {} self.submission_map = {} @@ -33,8 +35,15 @@ def _handle_free_judge(self, judge): while node: if isinstance(node.value, PriorityMarker): priority = node.value.priority + 1 - elif priority >= REJUDGE_PRIORITY and self.count_not_disabled() > 1 and sum( - not judge.working and not judge.is_disabled for judge in self.judges) <= 1: + elif ( + priority >= REJUDGE_PRIORITY + and self.count_not_disabled() > 1 + and sum( + not judge.working and not judge.is_disabled + for judge in self.judges + ) + <= 1 + ): return else: id, problem, language, source, judge_id = node.value @@ -43,10 +52,18 @@ def _handle_free_judge(self, judge): try: judge.submit(id, problem, language, source) except Exception: - logger.exception('Failed to dispatch %d (%s, %s) to %s', id, problem, language, judge.name) + logger.exception( + 'Failed to dispatch %d (%s, %s) to %s', + id, + problem, + language, + judge.name, + ) self.judges.remove(judge) return - logger.info('Dispatched queued submission %d: %s', id, judge.name) + logger.info( + 'Dispatched queued submission %d: %s', id, judge.name + ) self.queue.remove(node) del self.node_map[id] break @@ -131,14 +148,30 @@ def judge(self, id, problem, language, source, judge_id, priority): # idempotent. return - candidates = [judge for judge in self.judges if judge.can_judge(problem, language, judge_id)] - available = [judge for judge in candidates if not judge.working and not judge.is_disabled] + candidates = [ + judge + for judge in self.judges + if judge.can_judge(problem, language, judge_id) + ] + available = [ + judge + for judge in candidates + if not judge.working and not judge.is_disabled + ] if judge_id: - logger.info('Specified judge %s is%savailable', judge_id, ' ' if available else ' not ') + logger.info( + 'Specified judge %s is%savailable', + judge_id, + ' ' if available else ' not ', + ) else: logger.info('Free judges: %d', len(available)) - if len(candidates) > 1 and len(available) == 1 and priority >= REJUDGE_PRIORITY: + if ( + len(candidates) > 1 + and len(available) == 1 + and priority >= REJUDGE_PRIORITY + ): available = [] if available: @@ -149,7 +182,13 @@ def judge(self, id, problem, language, source, judge_id, priority): try: judge.submit(id, problem, language, source) except Exception: - logger.exception('Failed to dispatch %d (%s, %s) to %s', id, problem, language, judge.name) + logger.exception( + 'Failed to dispatch %d (%s, %s) to %s', + id, + problem, + language, + judge.name, + ) self.judges.discard(judge) return self.judge(id, problem, language, source, judge_id, priority) else: diff --git a/judge/bridge/server.py b/judge/bridge/server.py index cc83f84d13..4e67310773 100644 --- a/judge/bridge/server.py +++ b/judge/bridge/server.py @@ -12,7 +12,9 @@ def __init__(self, addresses, handler): self._shutdown = threading.Event() def serve_forever(self): - threads = [threading.Thread(target=server.serve_forever) for server in self.servers] + threads = [ + threading.Thread(target=server.serve_forever) for server in self.servers + ] for thread in threads: thread.daemon = True thread.start() diff --git a/judge/comments.py b/judge/comments.py index 21481394ff..83fc89ace2 100644 --- a/judge/comments.py +++ b/judge/comments.py @@ -7,7 +7,12 @@ from django.db.models.expressions import F, Value from django.db.models.functions import Coalesce from django.forms import ModelForm -from django.http import HttpResponseBadRequest, HttpResponseForbidden, HttpResponseNotFound, HttpResponseRedirect +from django.http import ( + HttpResponseBadRequest, + HttpResponseForbidden, + HttpResponseNotFound, + HttpResponseRedirect, +) from django.urls import reverse_lazy from django.utils import timezone from django.utils.decorators import method_decorator @@ -32,8 +37,11 @@ class Meta: } if HeavyPreviewPageDownWidget is not None: - widgets['body'] = HeavyPreviewPageDownWidget(preview=reverse_lazy('comment_preview'), - preview_timeout=1000, hide_preview_button=True) + widgets['body'] = HeavyPreviewPageDownWidget( + preview=reverse_lazy('comment_preview'), + preview_timeout=1000, + hide_preview_button=True, + ) def __init__(self, request, *args, **kwargs): self.request = request @@ -46,7 +54,11 @@ def clean(self): if profile.mute: raise ValidationError(_('Your part is silent, little toad.')) elif not self.request.user.is_staff and not profile.has_any_solves: - raise ValidationError(_('You must solve at least one problem before your voice can be heard.')) + raise ValidationError( + _( + 'You must solve at least one problem before your voice can be heard.' + ) + ) return super(CommentForm, self).clean() @@ -59,8 +71,9 @@ def get_comment_page(self): return self.comment_page def is_comment_locked(self): - return (CommentLock.objects.filter(page=self.get_comment_page()).exists() and - not self.request.user.has_perm('judge.override_comment_lock')) + return CommentLock.objects.filter( + page=self.get_comment_page() + ).exists() and not self.request.user.has_perm('judge.override_comment_lock') @method_decorator(login_required) def post(self, request, *args, **kwargs): @@ -82,8 +95,11 @@ def post(self, request, *args, **kwargs): parent_comment = Comment.objects.get(hidden=False, id=parent, page=page) except Comment.DoesNotExist: return HttpResponseNotFound() - if not (self.request.user.has_perm('judge.change_comment') or - parent_comment.time > timezone.now() - settings.DMOJ_COMMENT_REPLY_TIMEFRAME): + if not ( + self.request.user.has_perm('judge.change_comment') + or parent_comment.time + > timezone.now() - settings.DMOJ_COMMENT_REPLY_TIMEFRAME + ): return HttpResponseForbidden() form = CommentForm(request, request.POST) @@ -91,7 +107,9 @@ def post(self, request, *args, **kwargs): comment = form.save(commit=False) comment.author = request.profile comment.page = page - with LockModel(write=(Comment, Revision, Version), read=(ContentType,)), revisions.create_revision(): + with LockModel( + write=(Comment, Revision, Version), read=(ContentType,) + ), revisions.create_revision(): revisions.set_user(request.user) revisions.set_comment(_('Posted comment')) comment.save() @@ -102,10 +120,14 @@ def post(self, request, *args, **kwargs): def get(self, request, *args, **kwargs): self.object = self.get_object() - return self.render_to_response(self.get_context_data( - object=self.object, - comment_form=CommentForm(request, initial={'page': self.get_comment_page(), 'parent': None}), - )) + return self.render_to_response( + self.get_context_data( + object=self.object, + comment_form=CommentForm( + request, initial={'page': self.get_comment_page(), 'parent': None} + ), + ) + ) def get_context_data(self, **kwargs): context = super(CommentedDetailView, self).get_context_data(**kwargs) @@ -117,9 +139,13 @@ def get_context_data(self, **kwargs): if self.request.user.is_authenticated: profile = self.request.profile queryset = queryset.annotate( - my_vote=FilteredRelation('votes', condition=Q(votes__voter_id=profile.id)), + my_vote=FilteredRelation( + 'votes', condition=Q(votes__voter_id=profile.id) + ), ).annotate(vote_score=Coalesce(F('my_vote__score'), Value(0))) - context['is_new_user'] = not self.request.user.is_staff and not profile.has_any_solves + context['is_new_user'] = ( + not self.request.user.is_staff and not profile.has_any_solves + ) context['comment_list'] = queryset context['vote_hide_threshold'] = settings.DMOJ_COMMENT_VOTE_HIDE_THRESHOLD context['reply_cutoff'] = timezone.now() - settings.DMOJ_COMMENT_REPLY_TIMEFRAME diff --git a/judge/contest_format/atcoder.py b/judge/contest_format/atcoder.py index 9585eee1bf..8bd7d1de9d 100644 --- a/judge/contest_format/atcoder.py +++ b/judge/contest_format/atcoder.py @@ -29,7 +29,9 @@ def validate(cls, config): return if not isinstance(config, dict): - raise ValidationError('AtCoder-styled contest expects no config or dict as config') + raise ValidationError( + 'AtCoder-styled contest expects no config or dict as config' + ) for key, value in config.items(): if key not in cls.config_defaults: @@ -37,7 +39,9 @@ def validate(cls, config): if not isinstance(value, type(cls.config_defaults[key])): raise ValidationError('invalid type for config key "%s"' % key) if not cls.config_validators[key](value): - raise ValidationError('invalid value "%s" for config key "%s"' % (value, key)) + raise ValidationError( + 'invalid value "%s" for config key "%s"' % (value, key) + ) def __init__(self, contest, config): self.config = self.config_defaults.copy() @@ -51,7 +55,8 @@ def update_participation(self, participation): format_data = {} with connection.cursor() as cursor: - cursor.execute(""" + cursor.execute( + """ SELECT MAX(cs.points) as `score`, ( SELECT MIN(csub.date) FROM judge_contestsubmission ccs LEFT OUTER JOIN @@ -62,7 +67,9 @@ def update_participation(self, participation): judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN judge_submission sub ON (sub.id = cs.submission_id) GROUP BY cp.id - """, (participation.id, participation.id)) + """, + (participation.id, participation.id), + ) for score, time, prob in cursor.fetchall(): time = from_database_time(time) @@ -71,9 +78,13 @@ def update_participation(self, participation): # Compute penalty if self.config['penalty']: # An IE can have a submission result of `None` - subs = participation.submissions.exclude(submission__result__isnull=True) \ - .exclude(submission__result__in=['IE', 'CE']) \ - .filter(problem_id=prob) + subs = ( + participation.submissions.exclude( + submission__result__isnull=True + ) + .exclude(submission__result__in=['IE', 'CE']) + .filter(problem_id=prob) + ) if score: prev = subs.filter(submission__date__lte=time).count() - 1 penalty += prev * self.config['penalty'] * 60 @@ -98,14 +109,35 @@ def update_participation(self, participation): def display_user_problem(self, participation, contest_problem): format_data = (participation.format_data or {}).get(str(contest_problem.id)) if format_data: - penalty = format_html(' ({penalty})', - penalty=floatformat(format_data['penalty'])) if format_data['penalty'] else '' + penalty = ( + format_html( + ' ({penalty})', + penalty=floatformat(format_data['penalty']), + ) + if format_data['penalty'] + else '' + ) return format_html( '{points}{penalty}
{time}
', - state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') + - self.best_solution_state(format_data['points'], contest_problem.points)), - url=reverse('contest_user_submissions', - args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]), + state=( + ( + 'pretest-' + if self.contest.run_pretests_only + and contest_problem.is_pretested + else '' + ) + + self.best_solution_state( + format_data['points'], contest_problem.points + ) + ), + url=reverse( + 'contest_user_submissions', + args=[ + self.contest.key, + participation.user.user.username, + contest_problem.problem.code, + ], + ), points=floatformat(format_data['points']), penalty=penalty, time=nice_repr(timedelta(seconds=format_data['time']), 'noday'), diff --git a/judge/contest_format/default.py b/judge/contest_format/default.py index 1cf3e9861c..1a14af0994 100644 --- a/judge/contest_format/default.py +++ b/judge/contest_format/default.py @@ -20,7 +20,9 @@ class DefaultContestFormat(BaseContestFormat): @classmethod def validate(cls, config): if config is not None and (not isinstance(config, dict) or config): - raise ValidationError('default contest expects no config or empty dict as config') + raise ValidationError( + 'default contest expects no config or empty dict as config' + ) def __init__(self, contest, config): super(DefaultContestFormat, self).__init__(contest, config) @@ -31,12 +33,16 @@ def update_participation(self, participation): format_data = {} for result in participation.submissions.values('problem_id').annotate( - time=Max('submission__date'), points=Max('points'), + time=Max('submission__date'), + points=Max('points'), ): dt = (result['time'] - participation.start).total_seconds() if result['points']: cumtime += dt - format_data[str(result['problem_id'])] = {'time': dt, 'points': result['points']} + format_data[str(result['problem_id'])] = { + 'time': dt, + 'points': result['points'], + } points += result['points'] participation.cumtime = max(cumtime, 0) @@ -50,10 +56,25 @@ def display_user_problem(self, participation, contest_problem): if format_data: return format_html( '{points}
{time}
', - state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') + - self.best_solution_state(format_data['points'], contest_problem.points)), - url=reverse('contest_user_submissions', - args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]), + state=( + ( + 'pretest-' + if self.contest.run_pretests_only + and contest_problem.is_pretested + else '' + ) + + self.best_solution_state( + format_data['points'], contest_problem.points + ) + ), + url=reverse( + 'contest_user_submissions', + args=[ + self.contest.key, + participation.user.user.username, + contest_problem.problem.code, + ], + ), points=floatformat(format_data['points']), time=nice_repr(timedelta(seconds=format_data['time']), 'noday'), ) @@ -63,18 +84,25 @@ def display_user_problem(self, participation, contest_problem): def display_participation_result(self, participation): return format_html( '{points}
{cumtime}
', - url=reverse('contest_all_user_submissions', - args=[self.contest.key, participation.user.user.username]), + url=reverse( + 'contest_all_user_submissions', + args=[self.contest.key, participation.user.user.username], + ), points=floatformat(participation.score, -self.contest.points_precision), cumtime=nice_repr(timedelta(seconds=participation.cumtime), 'noday'), ) def get_problem_breakdown(self, participation, contest_problems): - return [(participation.format_data or {}).get(str(contest_problem.id)) for contest_problem in contest_problems] + return [ + (participation.format_data or {}).get(str(contest_problem.id)) + for contest_problem in contest_problems + ] def get_label_for_problem(self, index): return str(index + 1) def get_short_form_display(self): yield _('The maximum score submission for each problem will be used.') - yield _('Ties will be broken by the sum of the last submission time on problems with a non-zero score.') + yield _( + 'Ties will be broken by the sum of the last submission time on problems with a non-zero score.' + ) diff --git a/judge/contest_format/ecoo.py b/judge/contest_format/ecoo.py index 93c245c9c1..b7139b3ec9 100644 --- a/judge/contest_format/ecoo.py +++ b/judge/contest_format/ecoo.py @@ -17,7 +17,11 @@ class ECOOContestFormat(DefaultContestFormat): name = gettext_lazy('ECOO') config_defaults = {'cumtime': False, 'first_ac_bonus': 10, 'time_bonus': 5} - config_validators = {'cumtime': lambda x: True, 'first_ac_bonus': lambda x: x >= 0, 'time_bonus': lambda x: x >= 0} + config_validators = { + 'cumtime': lambda x: True, + 'first_ac_bonus': lambda x: x >= 0, + 'time_bonus': lambda x: x >= 0, + } """ cumtime: Specify True if cumulative time is to be used in breaking ties. Defaults to False. first_ac_bonus: The number of points to award if a solution gets AC on its first non-IE/CE run. Defaults to 10. @@ -31,7 +35,9 @@ def validate(cls, config): return if not isinstance(config, dict): - raise ValidationError('ECOO-styled contest expects no config or dict as config') + raise ValidationError( + 'ECOO-styled contest expects no config or dict as config' + ) for key, value in config.items(): if key not in cls.config_defaults: @@ -39,7 +45,9 @@ def validate(cls, config): if not isinstance(value, type(cls.config_defaults[key])): raise ValidationError('invalid type for config key "%s"' % key) if not cls.config_validators[key](value): - raise ValidationError('invalid value "%s" for config key "%s"' % (value, key)) + raise ValidationError( + 'invalid value "%s" for config key "%s"' % (value, key) + ) def __init__(self, contest, config): self.config = self.config_defaults.copy() @@ -51,18 +59,19 @@ def update_participation(self, participation): score = 0 format_data = {} - submissions = participation.submissions.exclude(submission__result__in=('IE', 'CE')) + submissions = participation.submissions.exclude( + submission__result__in=('IE', 'CE') + ) submission_counts = { - data['problem_id']: data['count'] for data in submissions.values('problem_id').annotate(count=Count('id')) + data['problem_id']: data['count'] + for data in submissions.values('problem_id').annotate(count=Count('id')) } queryset = ( - submissions - .values('problem_id') + submissions.values('problem_id') .filter( submission__date=Subquery( - submissions - .filter(problem_id=OuterRef('problem_id')) + submissions.filter(problem_id=OuterRef('problem_id')) .order_by('-submission__date') .values('submission__date')[:1], ), @@ -83,9 +92,17 @@ def update_participation(self, participation): bonus += self.config['first_ac_bonus'] # Time bonus if self.config['time_bonus']: - bonus += (participation.end_time - date).total_seconds() // 60 // self.config['time_bonus'] - - format_data[str(problem_id)] = {'time': dt, 'points': points, 'bonus': bonus} + bonus += ( + (participation.end_time - date).total_seconds() + // 60 + // self.config['time_bonus'] + ) + + format_data[str(problem_id)] = { + 'time': dt, + 'points': points, + 'bonus': bonus, + } for data in format_data.values(): if self.config['cumtime']: @@ -101,15 +118,35 @@ def update_participation(self, participation): def display_user_problem(self, participation, contest_problem): format_data = (participation.format_data or {}).get(str(contest_problem.id)) if format_data: - bonus = format_html(' +{bonus}', - bonus=floatformat(format_data['bonus'])) if format_data['bonus'] else '' + bonus = ( + format_html( + ' +{bonus}', bonus=floatformat(format_data['bonus']) + ) + if format_data['bonus'] + else '' + ) return format_html( '{points}{bonus}
{time}
', - state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') + - self.best_solution_state(format_data['points'], contest_problem.points)), - url=reverse('contest_user_submissions', - args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]), + state=( + ( + 'pretest-' + if self.contest.run_pretests_only + and contest_problem.is_pretested + else '' + ) + + self.best_solution_state( + format_data['points'], contest_problem.points + ) + ), + url=reverse( + 'contest_user_submissions', + args=[ + self.contest.key, + participation.user.user.username, + contest_problem.problem.code, + ], + ), points=floatformat(format_data['points']), bonus=bonus, time=nice_repr(timedelta(seconds=format_data['time']), 'noday'), @@ -120,14 +157,20 @@ def display_user_problem(self, participation, contest_problem): def display_participation_result(self, participation): return format_html( '{points}
{cumtime}
', - url=reverse('contest_all_user_submissions', - args=[self.contest.key, participation.user.user.username]), + url=reverse( + 'contest_all_user_submissions', + args=[self.contest.key, participation.user.user.username], + ), points=floatformat(participation.score, -self.contest.points_precision), - cumtime=nice_repr(timedelta(seconds=participation.cumtime), 'noday') if self.config['cumtime'] else '', + cumtime=nice_repr(timedelta(seconds=participation.cumtime), 'noday') + if self.config['cumtime'] + else '', ) def get_short_form_display(self): - yield _('The score on your **last** non-CE submission for each problem will be used.') + yield _( + 'The score on your **last** non-CE submission for each problem will be used.' + ) first_ac_bonus = self.config['first_ac_bonus'] if first_ac_bonus: @@ -144,6 +187,8 @@ def get_short_form_display(self): ) % time_bonus if self.config['cumtime']: - yield _('Ties will be broken by the sum of the last submission time on **all** problems.') + yield _( + 'Ties will be broken by the sum of the last submission time on **all** problems.' + ) else: yield _('Ties by score will **not** be broken.') diff --git a/judge/contest_format/icpc.py b/judge/contest_format/icpc.py index 13dfe2ed7c..c470c98e86 100644 --- a/judge/contest_format/icpc.py +++ b/judge/contest_format/icpc.py @@ -29,7 +29,9 @@ def validate(cls, config): return if not isinstance(config, dict): - raise ValidationError('ICPC-styled contest expects no config or dict as config') + raise ValidationError( + 'ICPC-styled contest expects no config or dict as config' + ) for key, value in config.items(): if key not in cls.config_defaults: @@ -37,7 +39,9 @@ def validate(cls, config): if not isinstance(value, type(cls.config_defaults[key])): raise ValidationError('invalid type for config key "%s"' % key) if not cls.config_validators[key](value): - raise ValidationError('invalid value "%s" for config key "%s"' % (value, key)) + raise ValidationError( + 'invalid value "%s" for config key "%s"' % (value, key) + ) def __init__(self, contest, config): self.config = self.config_defaults.copy() @@ -52,7 +56,8 @@ def update_participation(self, participation): format_data = {} with connection.cursor() as cursor: - cursor.execute(""" + cursor.execute( + """ SELECT MAX(cs.points) as `points`, ( SELECT MIN(csub.date) FROM judge_contestsubmission ccs LEFT OUTER JOIN @@ -63,7 +68,9 @@ def update_participation(self, participation): judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN judge_submission sub ON (sub.id = cs.submission_id) GROUP BY cp.id - """, (participation.id, participation.id)) + """, + (participation.id, participation.id), + ) for points, time, prob in cursor.fetchall(): time = from_database_time(time) @@ -72,9 +79,13 @@ def update_participation(self, participation): # Compute penalty if self.config['penalty']: # An IE can have a submission result of `None` - subs = participation.submissions.exclude(submission__result__isnull=True) \ - .exclude(submission__result__in=['IE', 'CE']) \ - .filter(problem_id=prob) + subs = ( + participation.submissions.exclude( + submission__result__isnull=True + ) + .exclude(submission__result__in=['IE', 'CE']) + .filter(problem_id=prob) + ) if points: prev = subs.filter(submission__date__lte=time).count() - 1 penalty += prev * self.config['penalty'] * 60 @@ -100,14 +111,35 @@ def update_participation(self, participation): def display_user_problem(self, participation, contest_problem): format_data = (participation.format_data or {}).get(str(contest_problem.id)) if format_data: - penalty = format_html(' ({penalty})', - penalty=floatformat(format_data['penalty'])) if format_data['penalty'] else '' + penalty = ( + format_html( + ' ({penalty})', + penalty=floatformat(format_data['penalty']), + ) + if format_data['penalty'] + else '' + ) return format_html( '{points}{penalty}
{time}
', - state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') + - self.best_solution_state(format_data['points'], contest_problem.points)), - url=reverse('contest_user_submissions', - args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]), + state=( + ( + 'pretest-' + if self.contest.run_pretests_only + and contest_problem.is_pretested + else '' + ) + + self.best_solution_state( + format_data['points'], contest_problem.points + ) + ), + url=reverse( + 'contest_user_submissions', + args=[ + self.contest.key, + participation.user.user.username, + contest_problem.problem.code, + ], + ), points=floatformat(format_data['points']), penalty=penalty, time=nice_repr(timedelta(seconds=format_data['time']), 'noday'), @@ -134,5 +166,7 @@ def get_short_form_display(self): penalty, ) % penalty - yield _('Ties will be broken by the sum of the last score altering submission time on problems with a non-zero ' - 'score, followed by the time of the last score altering submission.') + yield _( + 'Ties will be broken by the sum of the last score altering submission time on problems with a non-zero ' + 'score, followed by the time of the last score altering submission.' + ) diff --git a/judge/contest_format/ioi.py b/judge/contest_format/ioi.py index 9cb75fa6fe..ba74fc8147 100644 --- a/judge/contest_format/ioi.py +++ b/judge/contest_format/ioi.py @@ -20,7 +20,8 @@ def update_participation(self, participation): format_data = {} with connection.cursor() as cursor: - cursor.execute(""" + cursor.execute( + """ SELECT q.prob, MIN(q.date) as `date`, q.batch_points @@ -64,7 +65,9 @@ def update_participation(self, participation): ON p.prob = q.prob AND (p.batch = q.batch OR p.batch is NULL AND q.batch is NULL) WHERE p.max_batch_points = q.batch_points GROUP BY q.prob, q.batch - """, (participation.id, participation.id)) + """, + (participation.id, participation.id), + ) for problem_id, time, subtask_points in cursor.fetchall(): problem_id = str(problem_id) @@ -77,7 +80,9 @@ def update_participation(self, participation): if format_data.get(problem_id) is None: format_data[problem_id] = {'points': 0, 'time': 0} format_data[problem_id]['points'] += subtask_points - format_data[problem_id]['time'] = max(dt, format_data[problem_id]['time']) + format_data[problem_id]['time'] = max( + dt, format_data[problem_id]['time'] + ) for problem_data in format_data.values(): penalty = problem_data['time'] @@ -96,7 +101,9 @@ def get_short_form_display(self): yield _('The maximum score for each problem batch will be used.') if self.config['cumtime']: - yield _('Ties will be broken by the sum of the last score altering submission time on problems with a ' - 'non-zero score.') + yield _( + 'Ties will be broken by the sum of the last score altering submission time on problems with a ' + 'non-zero score.' + ) else: yield _('Ties by score will **not** be broken.') diff --git a/judge/contest_format/legacy_ioi.py b/judge/contest_format/legacy_ioi.py index 0b80b8c2d8..d955ccf7e9 100644 --- a/judge/contest_format/legacy_ioi.py +++ b/judge/contest_format/legacy_ioi.py @@ -27,7 +27,9 @@ def validate(cls, config): return if not isinstance(config, dict): - raise ValidationError('IOI-styled contest expects no config or dict as config') + raise ValidationError( + 'IOI-styled contest expects no config or dict as config' + ) for key, value in config.items(): if key not in cls.config_defaults: @@ -45,12 +47,18 @@ def update_participation(self, participation): score = 0 format_data = {} - queryset = (participation.submissions.values('problem_id') - .filter(points=Subquery( - participation.submissions.filter(problem_id=OuterRef('problem_id')) - .order_by('-points').values('points')[:1])) - .annotate(time=Min('submission__date')) - .values_list('problem_id', 'time', 'points')) + queryset = ( + participation.submissions.values('problem_id') + .filter( + points=Subquery( + participation.submissions.filter(problem_id=OuterRef('problem_id')) + .order_by('-points') + .values('points')[:1] + ) + ) + .annotate(time=Min('submission__date')) + .values_list('problem_id', 'time', 'points') + ) for problem_id, time, points in queryset: if self.config['cumtime']: @@ -74,12 +82,29 @@ def display_user_problem(self, participation, contest_problem): if format_data: return format_html( '{points}
{time}
', - state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') + - self.best_solution_state(format_data['points'], contest_problem.points)), - url=reverse('contest_user_submissions', - args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]), + state=( + ( + 'pretest-' + if self.contest.run_pretests_only + and contest_problem.is_pretested + else '' + ) + + self.best_solution_state( + format_data['points'], contest_problem.points + ) + ), + url=reverse( + 'contest_user_submissions', + args=[ + self.contest.key, + participation.user.user.username, + contest_problem.problem.code, + ], + ), points=floatformat(format_data['points']), - time=nice_repr(timedelta(seconds=format_data['time']), 'noday') if self.config['cumtime'] else '', + time=nice_repr(timedelta(seconds=format_data['time']), 'noday') + if self.config['cumtime'] + else '', ) else: return mark_safe('') @@ -87,17 +112,23 @@ def display_user_problem(self, participation, contest_problem): def display_participation_result(self, participation): return format_html( '{points}
{cumtime}
', - url=reverse('contest_all_user_submissions', - args=[self.contest.key, participation.user.user.username]), + url=reverse( + 'contest_all_user_submissions', + args=[self.contest.key, participation.user.user.username], + ), points=floatformat(participation.score, -self.contest.points_precision), - cumtime=nice_repr(timedelta(seconds=participation.cumtime), 'noday') if self.config['cumtime'] else '', + cumtime=nice_repr(timedelta(seconds=participation.cumtime), 'noday') + if self.config['cumtime'] + else '', ) def get_short_form_display(self): yield _('The maximum score submission for each problem will be used.') if self.config['cumtime']: - yield _('Ties will be broken by the sum of the last score altering submission time on problems with a ' - 'non-zero score.') + yield _( + 'Ties will be broken by the sum of the last score altering submission time on problems with a ' + 'non-zero score.' + ) else: yield _('Ties by score will **not** be broken.') diff --git a/judge/dblock.py b/judge/dblock.py index d4d518424c..c71ead228e 100644 --- a/judge/dblock.py +++ b/judge/dblock.py @@ -5,10 +5,12 @@ class LockModel(object): def __init__(self, write, read=()): - self.tables = ', '.join(chain( - ('`%s` WRITE' % model._meta.db_table for model in write), - ('`%s` READ' % model._meta.db_table for model in read), - )) + self.tables = ', '.join( + chain( + ('`%s` WRITE' % model._meta.db_table for model in write), + ('`%s` READ' % model._meta.db_table for model in read), + ) + ) self.cursor = connection.cursor() def __enter__(self): diff --git a/judge/event_poster.py b/judge/event_poster.py index 29100bd993..d2e7c67dc4 100644 --- a/judge/event_poster.py +++ b/judge/event_poster.py @@ -10,9 +10,12 @@ def post(channel, message): def last(): return 0 + elif hasattr(settings, 'EVENT_DAEMON_AMQP'): from .event_poster_amqp import last, post + real = True else: from .event_poster_ws import last, post + real = True diff --git a/judge/event_poster_amqp.py b/judge/event_poster_amqp.py index 74f6331e11..4d492112ec 100644 --- a/judge/event_poster_amqp.py +++ b/judge/event_poster_amqp.py @@ -15,14 +15,19 @@ def __init__(self): self._exchange = settings.EVENT_DAEMON_AMQP_EXCHANGE def _connect(self): - self._conn = pika.BlockingConnection(pika.URLParameters(settings.EVENT_DAEMON_AMQP)) + self._conn = pika.BlockingConnection( + pika.URLParameters(settings.EVENT_DAEMON_AMQP) + ) self._chan = self._conn.channel() def post(self, channel, message, tries=0): try: id = int(time() * 1000000) - self._chan.basic_publish(self._exchange, '', - json.dumps({'id': id, 'channel': channel, 'message': message})) + self._chan.basic_publish( + self._exchange, + '', + json.dumps({'id': id, 'channel': channel, 'message': message}), + ) return id except AMQPError: if tries > 10: diff --git a/judge/event_poster_ws.py b/judge/event_poster_ws.py index fba4052631..f7035c8209 100644 --- a/judge/event_poster_ws.py +++ b/judge/event_poster_ws.py @@ -20,14 +20,18 @@ def __init__(self): def _connect(self): self._conn = create_connection(settings.EVENT_DAEMON_POST) if settings.EVENT_DAEMON_KEY is not None: - self._conn.send(json.dumps({'command': 'auth', 'key': settings.EVENT_DAEMON_KEY})) + self._conn.send( + json.dumps({'command': 'auth', 'key': settings.EVENT_DAEMON_KEY}) + ) resp = json.loads(self._conn.recv()) if resp['status'] == 'error': raise EventPostingError(resp['code']) def post(self, channel, message, tries=0): try: - self._conn.send(json.dumps({'command': 'post', 'channel': channel, 'message': message})) + self._conn.send( + json.dumps({'command': 'post', 'channel': channel, 'message': message}) + ) resp = json.loads(self._conn.recv()) if resp['status'] == 'error': raise EventPostingError(resp['code']) diff --git a/judge/feed.py b/judge/feed.py index 4e7cc3c641..fd865b6d6f 100644 --- a/judge/feed.py +++ b/judge/feed.py @@ -12,7 +12,9 @@ class ProblemFeed(Feed): title = 'Recently Added %s Problems' % settings.SITE_NAME link = '/' - description = 'The latest problems added on the %s website' % settings.SITE_LONG_NAME + description = ( + 'The latest problems added on the %s website' % settings.SITE_LONG_NAME + ) def items(self): return Problem.get_public_problems().order_by('-date', '-id')[:25] @@ -75,7 +77,9 @@ class BlogFeed(Feed): description = 'The latest blog posts from the %s' % settings.SITE_LONG_NAME def items(self): - return BlogPost.objects.filter(visible=True, publish_on__lte=timezone.now()).order_by('-sticky', '-publish_on') + return BlogPost.objects.filter( + visible=True, publish_on__lte=timezone.now() + ).order_by('-sticky', '-publish_on') def item_title(self, post): return post.title diff --git a/judge/forms.py b/judge/forms.py index 87003e56f2..7fd078a5bf 100644 --- a/judge/forms.py +++ b/judge/forms.py @@ -10,17 +10,36 @@ from django.core.exceptions import ValidationError from django.core.validators import RegexValidator from django.db.models import Q -from django.forms import BooleanField, CharField, ChoiceField, Form, ModelForm, MultipleChoiceField +from django.forms import ( + BooleanField, + CharField, + ChoiceField, + Form, + ModelForm, + MultipleChoiceField, +) from django.urls import reverse_lazy from django.utils.text import format_lazy from django.utils.translation import gettext_lazy as _, ngettext_lazy from django_ace import AceWidget -from judge.models import Contest, Language, Organization, Problem, ProblemPointsVote, Profile, Submission, \ - WebAuthnCredential +from judge.models import ( + Contest, + Language, + Organization, + Problem, + ProblemPointsVote, + Profile, + Submission, + WebAuthnCredential, +) from judge.utils.mail import validate_email_domain from judge.utils.subscription import newsletter_id -from judge.widgets import HeavyPreviewPageDownWidget, Select2MultipleWidget, Select2Widget +from judge.widgets import ( + HeavyPreviewPageDownWidget, + Select2MultipleWidget, + Select2Widget, +) TOTP_CODE_LENGTH = 6 @@ -28,15 +47,22 @@ TOTP_CODE_LENGTH: { 'regex_validator': RegexValidator( f'^[0-9]{{{TOTP_CODE_LENGTH}}}$', - format_lazy(ngettext_lazy('Two-factor authentication tokens must be {count} decimal digit.', - 'Two-factor authentication tokens must be {count} decimal digits.', - TOTP_CODE_LENGTH), count=TOTP_CODE_LENGTH), + format_lazy( + ngettext_lazy( + 'Two-factor authentication tokens must be {count} decimal digit.', + 'Two-factor authentication tokens must be {count} decimal digits.', + TOTP_CODE_LENGTH, + ), + count=TOTP_CODE_LENGTH, + ), ), 'verify': lambda code, profile: not profile.check_totp_code(code), 'err': _('Invalid two-factor authentication token.'), }, 16: { - 'regex_validator': RegexValidator('^[A-Z0-9]{16}$', _('Scratch codes must be 16 Base32 characters.')), + 'regex_validator': RegexValidator( + '^[A-Z0-9]{16}$', _('Scratch codes must be 16 Base32 characters.') + ), 'verify': lambda code, profile: code not in json.loads(profile.scratch_codes), 'err': _('Invalid scratch code.'), }, @@ -45,12 +71,24 @@ class ProfileForm(ModelForm): if newsletter_id is not None: - newsletter = forms.BooleanField(label=_('Subscribe to contest updates'), initial=False, required=False) - test_site = forms.BooleanField(label=_('Enable experimental features'), initial=False, required=False) + newsletter = forms.BooleanField( + label=_('Subscribe to contest updates'), initial=False, required=False + ) + test_site = forms.BooleanField( + label=_('Enable experimental features'), initial=False, required=False + ) class Meta: model = Profile - fields = ['about', 'organizations', 'timezone', 'language', 'ace_theme', 'site_theme', 'user_script'] + fields = [ + 'about', + 'organizations', + 'timezone', + 'language', + 'ace_theme', + 'site_theme', + 'user_script', + ] widgets = { 'timezone': Select2Widget(attrs={'style': 'width:200px'}), 'language': Select2Widget(attrs={'style': 'width:200px'}), @@ -71,7 +109,11 @@ class Meta: def clean_about(self): if 'about' in self.changed_data and not self.instance.has_any_solves: - raise ValidationError(_('You must solve at least one problem before you can update your profile.')) + raise ValidationError( + _( + 'You must solve at least one problem before you can update your profile.' + ) + ) return self.cleaned_data['about'] def clean(self): @@ -79,9 +121,13 @@ def clean(self): max_orgs = settings.DMOJ_USER_MAX_ORGANIZATION_COUNT if sum(org.is_open for org in organizations) > max_orgs: - raise ValidationError(ngettext_lazy('You may not be part of more than {count} public organization.', - 'You may not be part of more than {count} public organizations.', - max_orgs).format(count=max_orgs)) + raise ValidationError( + ngettext_lazy( + 'You may not be part of more than {count} public organization.', + 'You may not be part of more than {count} public organizations.', + max_orgs, + ).format(count=max_orgs) + ) return self.cleaned_data @@ -94,7 +140,9 @@ def __init__(self, *args, **kwargs): ) if not self.fields['organizations'].queryset: self.fields.pop('organizations') - self.fields['user_script'].widget = AceWidget(mode='javascript', theme=user.profile.resolved_ace_theme) + self.fields['user_script'].widget = AceWidget( + mode='javascript', theme=user.profile.resolved_ace_theme + ) class EmailChangeForm(Form): @@ -120,11 +168,16 @@ def clean_password(self): class DownloadDataForm(Form): comment_download = BooleanField(required=False, label=_('Download comments?')) submission_download = BooleanField(required=False, label=_('Download submissions?')) - submission_problem_glob = CharField(initial='*', label=_('Filter by problem code glob:'), max_length=100) + submission_problem_glob = CharField( + initial='*', label=_('Filter by problem code glob:'), max_length=100 + ) submission_results = MultipleChoiceField( required=False, widget=Select2MultipleWidget( - attrs={'style': 'width: 260px', 'data-placeholder': _('Leave empty to include all submissions')}, + attrs={ + 'style': 'width: 260px', + 'data-placeholder': _('Leave empty to include all submissions'), + }, ), choices=sorted(map(itemgetter(0, 0), Submission.RESULT)), label=_('Filter by result:'), @@ -148,14 +201,18 @@ def clean_submission_result(self): class ProblemSubmitForm(ModelForm): - source = CharField(max_length=65536, widget=AceWidget(theme='twilight', no_ace_media=True)) + source = CharField( + max_length=65536, widget=AceWidget(theme='twilight', no_ace_media=True) + ) judge = ChoiceField(choices=(), widget=forms.HiddenInput(), required=False) def __init__(self, *args, judge_choices=(), **kwargs): super(ProblemSubmitForm, self).__init__(*args, **kwargs) self.fields['language'].empty_label = None self.fields['language'].label_from_instance = attrgetter('display_name') - self.fields['language'].queryset = Language.objects.filter(judges__online=True).distinct() + self.fields['language'].queryset = Language.objects.filter( + judges__online=True + ).distinct() if judge_choices: self.fields['judge'].widget = Select2Widget( @@ -174,7 +231,9 @@ class Meta: fields = ['about', 'logo_override_image', 'admins'] widgets = {'admins': Select2MultipleWidget(attrs={'style': 'width: 200px'})} if HeavyPreviewPageDownWidget is not None: - widgets['about'] = HeavyPreviewPageDownWidget(preview=reverse_lazy('organization_preview')) + widgets['about'] = HeavyPreviewPageDownWidget( + preview=reverse_lazy('organization_preview') + ) class CustomAuthenticationForm(AuthenticationForm): @@ -188,8 +247,9 @@ def __init__(self, *args, **kwargs): self.has_github_auth = self._has_social_auth('GITHUB_SECURE') def _has_social_auth(self, key): - return (getattr(settings, 'SOCIAL_AUTH_%s_KEY' % key, None) and - getattr(settings, 'SOCIAL_AUTH_%s_SECRET' % key, None)) + return getattr(settings, 'SOCIAL_AUTH_%s_KEY' % key, None) and getattr( + settings, 'SOCIAL_AUTH_%s_SECRET' % key, None + ) class NoAutoCompleteCharField(forms.CharField): @@ -202,7 +262,9 @@ def widget_attrs(self, widget): class TOTPForm(Form): TOLERANCE = settings.DMOJ_TOTP_TOLERANCE_HALF_MINUTES - totp_or_scratch_code = NoAutoCompleteCharField(required=False, widget=forms.TextInput(attrs={'autofocus': True})) + totp_or_scratch_code = NoAutoCompleteCharField( + required=False, widget=forms.TextInput(attrs={'autofocus': True}) + ) def __init__(self, *args, **kwargs): self.profile = kwargs.pop('profile') @@ -228,7 +290,9 @@ def clean(self): totp_validate = two_factor_validators_by_length[TOTP_CODE_LENGTH] code = self.cleaned_data.get('totp_or_scratch_code') totp_validate['regex_validator'](code) - if not pyotp.TOTP(self.totp_key).verify(code, valid_window=settings.DMOJ_TOTP_TOLERANCE_HALF_MINUTES): + if not pyotp.TOTP(self.totp_key).verify( + code, valid_window=settings.DMOJ_TOTP_TOLERANCE_HALF_MINUTES + ): raise ValidationError(totp_validate['err']) @@ -242,7 +306,9 @@ def __init__(self, *args, **kwargs): def clean(self): totp_or_scratch_code = self.cleaned_data.get('totp_or_scratch_code') - if self.profile.is_webauthn_enabled and self.cleaned_data.get('webauthn_response'): + if self.profile.is_webauthn_enabled and self.cleaned_data.get( + 'webauthn_response' + ): if len(self.cleaned_data['webauthn_response']) > 65536: raise ValidationError(_('Invalid WebAuthn response.')) @@ -251,7 +317,9 @@ def clean(self): response = json.loads(self.cleaned_data['webauthn_response']) try: - credential = self.profile.webauthn_credentials.get(cred_id=response.get('id', '')) + credential = self.profile.webauthn_credentials.get( + cred_id=response.get('id', '') + ) except WebAuthnCredential.DoesNotExist: raise ValidationError(_('Invalid WebAuthn credential ID.')) @@ -274,24 +342,37 @@ def clean(self): credential.counter = sign_count credential.save(update_fields=['counter']) elif totp_or_scratch_code: - if self.profile.is_totp_enabled and self.profile.check_totp_code(totp_or_scratch_code): + if self.profile.is_totp_enabled and self.profile.check_totp_code( + totp_or_scratch_code + ): return - elif self.profile.scratch_codes and totp_or_scratch_code in json.loads(self.profile.scratch_codes): + elif self.profile.scratch_codes and totp_or_scratch_code in json.loads( + self.profile.scratch_codes + ): scratch_codes = json.loads(self.profile.scratch_codes) scratch_codes.remove(totp_or_scratch_code) self.profile.scratch_codes = json.dumps(scratch_codes) self.profile.save(update_fields=['scratch_codes']) return elif self.profile.is_totp_enabled: - raise ValidationError(_('Invalid two-factor authentication token or scratch code.')) + raise ValidationError( + _('Invalid two-factor authentication token or scratch code.') + ) else: raise ValidationError(_('Invalid scratch code.')) else: - raise ValidationError(_('Must specify either totp_token or webauthn_response.')) + raise ValidationError( + _('Must specify either totp_token or webauthn_response.') + ) class ProblemCloneForm(Form): - code = CharField(max_length=20, validators=[RegexValidator('^[a-z0-9]+$', _('Problem code must be ^[a-z0-9]+$'))]) + code = CharField( + max_length=20, + validators=[ + RegexValidator('^[a-z0-9]+$', _('Problem code must be ^[a-z0-9]+$')) + ], + ) def clean_code(self): code = self.cleaned_data['code'] @@ -301,7 +382,10 @@ def clean_code(self): class ContestCloneForm(Form): - key = CharField(max_length=20, validators=[RegexValidator('^[a-z0-9]+$', _('Contest id must be ^[a-z0-9]+$'))]) + key = CharField( + max_length=20, + validators=[RegexValidator('^[a-z0-9]+$', _('Contest id must be ^[a-z0-9]+$'))], + ) def clean_key(self): key = self.cleaned_data['key'] diff --git a/judge/fulltext.py b/judge/fulltext.py index 5b9f7d3d09..0ee32ea47d 100644 --- a/judge/fulltext.py +++ b/judge/fulltext.py @@ -25,20 +25,26 @@ def search(self, query, mode=DEFAULT): # Get the table name and column names from the model # in `table_name`.`column_name` style columns = [meta.get_field(name).column for name in self._search_fields] - full_names = ['%s.%s' % - (connection.ops.quote_name(meta.db_table), - connection.ops.quote_name(column)) - for column in columns] + full_names = [ + '%s.%s' + % ( + connection.ops.quote_name(meta.db_table), + connection.ops.quote_name(column), + ) + for column in columns + ] # Create the MATCH...AGAINST expressions fulltext_columns = ', '.join(full_names) - match_expr = ('MATCH(%s) AGAINST (%%s%s)' % (fulltext_columns, mode)) + match_expr = 'MATCH(%s) AGAINST (%%s%s)' % (fulltext_columns, mode) # Add the extra SELECT and WHERE options - return self.extra(select={'relevance': match_expr}, - select_params=[query], - where=[match_expr], - params=[query]) + return self.extra( + select={'relevance': match_expr}, + select_params=[query], + where=[match_expr], + params=[query], + ) class SearchManager(models.Manager): diff --git a/judge/highlight_code.py b/judge/highlight_code.py index 06a947ed97..056a91ff11 100644 --- a/judge/highlight_code.py +++ b/judge/highlight_code.py @@ -14,9 +14,12 @@ def _make_pre_code(code): import pygments.formatters import pygments.util except ImportError: + def highlight_code(code, language, cssclass=None): return _make_pre_code(code) + else: + def highlight_code(code, language, cssclass='codehilite'): try: lexer = pygments.lexers.get_lexer_by_name(language) @@ -24,5 +27,9 @@ def highlight_code(code, language, cssclass='codehilite'): return _make_pre_code(code) return mark_safe( - pygments.highlight(code, lexer, pygments.formatters.HtmlFormatter(cssclass=cssclass, wrapcode=True)), + pygments.highlight( + code, + lexer, + pygments.formatters.HtmlFormatter(cssclass=cssclass, wrapcode=True), + ), ) diff --git a/judge/jinja2/__init__.py b/judge/jinja2/__init__.py index fa556db911..6389bc2a3e 100644 --- a/judge/jinja2/__init__.py +++ b/judge/jinja2/__init__.py @@ -8,8 +8,22 @@ from judge.highlight_code import highlight_code from judge.user_translations import gettext -from . import (camo, datetime, filesize, format, gravatar, language, markdown, rating, reference, render, social, - spaceless, submission, timedelta) +from . import ( + camo, + datetime, + filesize, + format, + gravatar, + language, + markdown, + rating, + reference, + render, + social, + spaceless, + submission, + timedelta, +) from . import registry registry.function('str', str) diff --git a/judge/jinja2/datetime.py b/judge/jinja2/datetime.py index 60b3f9753c..039430949b 100644 --- a/judge/jinja2/datetime.py +++ b/judge/jinja2/datetime.py @@ -27,6 +27,8 @@ def wrapper(datetime, *args, **kwargs): @registry.function def relative_time(time, **kwargs): abs_time = date(time, kwargs.get('format', _('N j, Y, g:i a'))) - return mark_safe(f'' - f'{escape(kwargs.get("abs", _("on {time}")).replace("{time}", abs_time))}') + return mark_safe( + f'' + f'{escape(kwargs.get("abs", _("on {time}")).replace("{time}", abs_time))}' + ) diff --git a/judge/jinja2/filesize.py b/judge/jinja2/filesize.py index 7b27fdebb7..e6352618e5 100644 --- a/judge/jinja2/filesize.py +++ b/judge/jinja2/filesize.py @@ -28,7 +28,11 @@ def _format_size(bytes, callback): @registry.filter def kbdetailformat(bytes): - return avoid_wrapping(_format_size(bytes * 1024, lambda x, y: ['%d %sB', '%.2f %sB'][bool(x)] % (y, x))) + return avoid_wrapping( + _format_size( + bytes * 1024, lambda x, y: ['%d %sB', '%.2f %sB'][bool(x)] % (y, x) + ) + ) @registry.filter diff --git a/judge/jinja2/gravatar.py b/judge/jinja2/gravatar.py index 259bf3ef59..638e79a3a3 100644 --- a/judge/jinja2/gravatar.py +++ b/judge/jinja2/gravatar.py @@ -17,7 +17,11 @@ def gravatar(email, size=80, default=None): elif isinstance(email, AbstractUser): email = email.email - gravatar_url = 'https://www.gravatar.com/avatar/' + hashlib.md5(utf8bytes(email.strip().lower())).hexdigest() + '?' + gravatar_url = ( + 'https://www.gravatar.com/avatar/' + + hashlib.md5(utf8bytes(email.strip().lower())).hexdigest() + + '?' + ) args = {'d': 'identicon', 's': str(size)} if default: args['f'] = 'y' diff --git a/judge/jinja2/markdown/__init__.py b/judge/jinja2/markdown/__init__.py index 6bb8d689b3..c01f8f9e47 100644 --- a/judge/jinja2/markdown/__init__.py +++ b/judge/jinja2/markdown/__init__.py @@ -71,7 +71,12 @@ def link(self, link, title, text): if not title: return '%s' % (link, self._link_rel(link), text) title = mistune.escape(title, quote=True) - return '%s' % (link, title, self._link_rel(link), text) + return '%s' % ( + link, + title, + self._link_rel(link), + text, + ) def block_code(self, code, lang=None): if not lang: @@ -80,23 +85,29 @@ def block_code(self, code, lang=None): def block_html(self, html): if self.texoid and html.startswith('')] - latex = html[html.index('>') + 1:html.rindex('<')] + attr = html[6 : html.index('>')] + latex = html[html.index('>') + 1 : html.rindex('<')] latex = unescape(latex) result = self.texoid.get_result(latex) if not result: return '
%s
' % mistune.escape(latex, smart_amp=False) elif 'error' not in result: - img = ('''') % { - 'svg': result['svg'], 'png': result['png'], - 'width': result['meta']['width'], 'height': result['meta']['height'], + img = ( + '''' + ) % { + 'svg': result['svg'], + 'png': result['png'], + 'width': result['meta']['width'], + 'height': result['meta']['height'], 'tail': ' /' if self.options.get('use_xhtml') else '', } - style = ['max-width: 100%', - 'height: %s' % result['meta']['height'], - 'max-height: %s' % result['meta']['height'], - 'width: %s' % result['meta']['width']] + style = [ + 'max-width: 100%', + 'height: %s' % result['meta']['height'], + 'max-height: %s' % result['meta']['height'], + 'width: %s' % result['meta']['width'], + ] if 'inline' in attr: tag = 'span' else: @@ -104,7 +115,9 @@ def block_html(self, html): style += ['text-align: center'] return '<%s style="%s">%s' % (tag, ';'.join(style), img, tag) else: - return '
%s
' % mistune.escape(result['error'], smart_amp=False) + return '
%s
' % mistune.escape( + result['error'], smart_amp=False + ) return super(AwesomeRenderer, self).block_html(html) def header(self, text, level, *args, **kwargs): @@ -120,7 +133,9 @@ def get_cleaner(name, params): styles = params.pop('styles', None) if styles: - params['css_sanitizer'] = CSSSanitizer(allowed_css_properties=all_styles if styles is True else styles) + params['css_sanitizer'] = CSSSanitizer( + allowed_css_properties=all_styles if styles is True else styles + ) if params.pop('mathml', False): params['tags'] = params.get('tags', []) + mathml_tags @@ -134,9 +149,13 @@ def get_cleaner(name, params): def fragments_to_tree(fragment): tree = html.Element('div') try: - parsed = html.fragments_fromstring(fragment, parser=html.HTMLParser(recover=True)) + parsed = html.fragments_fromstring( + fragment, parser=html.HTMLParser(recover=True) + ) except (XMLSyntaxError, ParserError) as e: - if fragment and (not isinstance(e, ParserError) or e.args[0] != 'Document is empty'): + if fragment and ( + not isinstance(e, ParserError) or e.args[0] != 'Document is empty' + ): logger.exception('Failed to parse HTML string') return tree @@ -161,7 +180,7 @@ def strip_paragraphs_tags(tree): def fragment_tree_to_str(tree): - return html.tostring(tree, encoding='unicode')[len('
'):-len('
')] + return html.tostring(tree, encoding='unicode')[len('
') : -len('
')] @registry.filter @@ -179,10 +198,19 @@ def markdown(value, style, math_engine=None, lazy_load=False, strip_paragraphs=F if lazy_load: post_processors.append(lazy_load_processor) - renderer = AwesomeRenderer(escape=escape, nofollow=nofollow, texoid=texoid, - math=math and math_engine is not None, math_engine=math_engine) - markdown = mistune.Markdown(renderer=renderer, inline=AwesomeInlineLexer, - parse_block_html=1, parse_inline_html=1) + renderer = AwesomeRenderer( + escape=escape, + nofollow=nofollow, + texoid=texoid, + math=math and math_engine is not None, + math_engine=math_engine, + ) + markdown = mistune.Markdown( + renderer=renderer, + inline=AwesomeInlineLexer, + parse_block_html=1, + parse_inline_html=1, + ) result = markdown(value) if post_processors or strip_paragraphs: diff --git a/judge/jinja2/markdown/bleach_whitelist.py b/judge/jinja2/markdown/bleach_whitelist.py index 3aed9d9730..560f03f2d7 100644 --- a/judge/jinja2/markdown/bleach_whitelist.py +++ b/judge/jinja2/markdown/bleach_whitelist.py @@ -5,347 +5,1358 @@ # This includes pseudo-classes, pseudo-elements, @-rules, units, and # selectors in addition to properties, but it doesn't matter for our # purposes -- we don't need to filter styles.. - ':active', '::after (:after)', 'align-content', 'align-items', 'align-self', - 'all', '', 'animation', 'animation-delay', 'animation-direction', - 'animation-duration', 'animation-fill-mode', 'animation-iteration-count', - 'animation-name', 'animation-play-state', 'animation-timing-function', - '@annotation', 'annotation()', 'attr()', '::backdrop', 'backface-visibility', - 'background', 'background-attachment', 'background-blend-mode', - 'background-clip', 'background-color', 'background-image', 'background-origin', - 'background-position', 'background-repeat', 'background-size', '', - '::before (:before)', '', 'blur()', 'border', 'border-bottom', - 'border-bottom-color', 'border-bottom-left-radius', - 'border-bottom-right-radius', 'border-bottom-style', 'border-bottom-width', - 'border-collapse', 'border-color', 'border-image', 'border-image-outset', - 'border-image-repeat', 'border-image-slice', 'border-image-source', - 'border-image-width', 'border-left', 'border-left-color', 'border-left-style', - 'border-left-width', 'border-radius', 'border-right', 'border-right-color', - 'border-right-style', 'border-right-width', 'border-spacing', 'border-style', - 'border-top', 'border-top-color', 'border-top-left-radius', - 'border-top-right-radius', 'border-top-style', 'border-top-width', - 'border-width', 'bottom', 'box-decoration-break', 'box-shadow', 'box-sizing', - 'break-after', 'break-before', 'break-inside', 'brightness()', 'calc()', - 'caption-side', 'ch', '@character-variant', 'character-variant()', '@charset', - ':checked', 'circle()', 'clear', 'clip', 'clip-path', 'cm', 'color', '', - 'columns', 'column-count', 'column-fill', 'column-gap', 'column-rule', - 'column-rule-color', 'column-rule-style', 'column-rule-width', 'column-span', - 'column-width', 'content', 'contrast()', '', 'counter-increment', - 'counter-reset', '@counter-style', 'cubic-bezier()', 'cursor', - '', ':default', 'deg', ':dir()', 'direction', ':disabled', - 'display', '@document', 'dpcm', 'dpi', 'dppx', 'drop-shadow()', 'element()', - 'ellipse()', 'em', ':empty', 'empty-cells', ':enabled', 'ex', 'filter', - ':first', ':first-child', '::first-letter', '::first-line', - ':first-of-type', 'flex', 'flex-basis', 'flex-direction', - 'flex-flow', 'flex-grow', 'flex-shrink', 'flex-wrap', 'float', ':focus', - 'font', '@font-face', 'font-family', 'font-feature-settings', - '@font-feature-values', 'font-kerning', 'font-language-override', 'font-size', - 'font-size-adjust', 'font-stretch', 'font-style', 'font-synthesis', - 'font-variant', 'font-variant-alternates', 'font-variant-caps', - 'font-variant-east-asian', 'font-variant-ligatures', 'font-variant-numeric', - 'font-variant-position', 'font-weight', '', ':fullscreen', 'grad', - '', 'grayscale()', 'grid', 'grid-area', 'grid-auto-columns', - 'grid-auto-flow', 'grid-auto-position', 'grid-auto-rows', 'grid-column', - 'grid-column-start', 'grid-column-end', 'grid-row', 'grid-row-start', - 'grid-row-end', 'grid-template', 'grid-template-areas', 'grid-template-rows', - 'grid-template-columns', 'height', ':hover', 'hsl()', 'hsla()', 'hue-rotate()', - 'hyphens', 'hz', '', 'image()', 'image-rendering', 'image-resolution', - 'image-orientation', 'ime-mode', '@import', 'in', ':indeterminate', 'inherit', - 'initial', ':in-range', 'inset()', '', ':invalid', 'invert()', - 'isolation', 'justify-content', '@keyframes', 'khz', ':lang()', ':last-child', - ':last-of-type', 'left', ':left', '', 'letter-spacing', - 'linear-gradient()', 'line-break', 'line-height', ':link', 'list-style', - 'list-style-image', 'list-style-position', 'list-style-type', 'margin', - 'margin-bottom', 'margin-left', 'margin-right', 'margin-top', 'marks', 'mask', - 'mask-type', 'matrix()', 'matrix3d()', 'max-height', 'max-width', '@media', - 'min-height', 'minmax()', 'min-width', 'mix-blend-mode', 'mm', 'ms', - '@namespace', ':not()', ':nth-child()', ':nth-last-child()', - ':nth-last-of-type()', ':nth-of-type()', '', 'object-fit', - 'object-position', ':only-child', ':only-of-type', 'opacity', 'opacity()', - ':optional', 'order', '@ornaments', 'ornaments()', 'orphans', 'outline', - 'outline-color', 'outline-offset', 'outline-style', 'outline-width', - ':out-of-range', 'overflow', 'overflow-wrap', 'overflow-x', 'overflow-y', - 'padding', 'padding-bottom', 'padding-left', 'padding-right', 'padding-top', - '@page', 'page-break-after', 'page-break-before', 'page-break-inside', 'pc', - '', 'perspective', 'perspective()', 'perspective-origin', - 'pointer-events', 'polygon()', 'position', '', 'pt', 'px', 'quotes', - 'rad', 'radial-gradient()', '', ':read-only', ':read-write', 'rect()', - 'rem', 'repeat()', '::repeat-index', '::repeat-item', - 'repeating-linear-gradient()', 'repeating-radial-gradient()', ':required', - 'resize', '', 'rgb()', 'rgba()', 'right', ':right', ':root', - 'rotate()', 'rotatex()', 'rotatey()', 'rotatez()', 'rotate3d()', 'ruby-align', - 'ruby-merge', 'ruby-position', 's', 'saturate()', 'scale()', 'scalex()', - 'scaley()', 'scalez()', 'scale3d()', ':scope', 'scroll-behavior', - '::selection', 'sepia()', '', 'shape-image-threshold', 'shape-margin', - 'shape-outside', 'skew()', 'skewx()', 'skewy()', 'steps()', '', - '@styleset', 'styleset()', '@stylistic', 'stylistic()', '@supports', '@swash', - 'swash()', 'symbol()', 'table-layout', 'tab-size', ':target', 'text-align', - 'text-align-last', 'text-combine-upright', 'text-decoration', - 'text-decoration-color', 'text-decoration-line', 'text-decoration-style', - 'text-indent', 'text-orientation', 'text-overflow', 'text-rendering', - 'text-shadow', 'text-transform', 'text-underline-position', '