diff --git a/.ci.settings.py b/.ci.settings.py index 637c918399..6b914e98ff 100644 --- a/.ci.settings.py +++ b/.ci.settings.py @@ -1,23 +1,19 @@ -COMPRESS_OUTPUT_DIR = 'cache' -STATICFILES_FINDERS += ('compressor.finders.CompressorFinder',) -STATIC_ROOT = os.path.join(BASE_DIR, 'static') +COMPRESS_OUTPUT_DIR = "cache" +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': { - 'ENGINE': 'django.db.backends.mysql', - 'NAME': 'dmoj', - 'USER': 'root', - 'PASSWORD': 'root', - 'HOST': 'localhost', - 'PORT': '3306', - 'OPTIONS': { - 'charset': 'utf8mb4', + "default": { + "ENGINE": "django.db.backends.mysql", + "NAME": "dmoj", + "USER": "root", + "PASSWORD": "root", + "HOST": "localhost", + "PORT": "3306", + "OPTIONS": { + "charset": "utf8mb4", }, }, } diff --git a/django_ace/widgets.py b/django_ace/widgets.py index 2f521bf7f7..dee433a23d 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 @@ -23,10 +32,10 @@ def __init__(self, mode=None, theme=None, wordwrap=False, width='100%', height=' @property def media(self): - js = [urljoin(settings.ACE_URL, 'ace.js')] if self.ace_media else [] - js.append('django_ace/widget.js') + js = [urljoin(settings.ACE_URL, "ace.js")] if self.ace_media else [] + js.append("django_ace/widget.js") css = { - 'screen': ['django_ace/widget.css'], + "screen": ["django_ace/widget.css"], } return forms.Media(js=js, css=css) @@ -34,26 +43,30 @@ def render(self, name, value, attrs=None, renderer=None): attrs = attrs or {} ace_attrs = { - 'class': 'django-ace-widget loading', - 'style': 'width:%s; height:%s' % (self.width, self.height), - 'id': 'ace_%s' % name, + "class": "django-ace-widget loading", + "style": "width:%s; height:%s" % (self.width, self.height), + "id": "ace_%s" % name, } if self.mode: - ace_attrs['data-mode'] = self.mode + ace_attrs["data-mode"] = self.mode if self.theme: - ace_attrs['data-theme'] = self.theme - ace_attrs['data-default-light-theme'] = settings.ACE_DEFAULT_LIGHT_THEME - ace_attrs['data-default-dark-theme'] = settings.ACE_DEFAULT_DARK_THEME + ace_attrs["data-theme"] = self.theme + ace_attrs["data-default-light-theme"] = settings.ACE_DEFAULT_LIGHT_THEME + ace_attrs["data-default-dark-theme"] = settings.ACE_DEFAULT_DARK_THEME if self.wordwrap: - ace_attrs['data-wordwrap'] = 'true' + 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) + 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..0ea573e90e 100644 --- a/dmoj/celery.py +++ b/dmoj/celery.py @@ -4,24 +4,30 @@ from celery import Celery from celery.signals import task_failure -app = Celery('dmoj') +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'): +app.config_from_object(settings, namespace="CELERY") + +if hasattr(settings, "CELERY_BROKER_URL_SECRET"): app.conf.broker_url = settings.CELERY_BROKER_URL_SECRET -if hasattr(settings, 'CELERY_RESULT_BACKEND_SECRET'): +if hasattr(settings, "CELERY_RESULT_BACKEND_SECRET"): app.conf.result_backend = settings.CELERY_RESULT_BACKEND_SECRET # Load task modules from all registered Django app configs. app.autodiscover_tasks() # Logger to enable reporting of errors. -logger = logging.getLogger('judge.celery') +logger = logging.getLogger("judge.celery") @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..3ea9dc566d 100644 --- a/dmoj/settings.py +++ b/dmoj/settings.py @@ -22,7 +22,7 @@ # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = '5*9f5q57mqmlz2#f$x1h76&jxy#yortjl1v+l*6hd18$d*yx#0' +SECRET_KEY = "5*9f5q57mqmlz2#f$x1h76&jxy#yortjl1v+l*6hd18$d*yx#0" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -30,9 +30,9 @@ ALLOWED_HOSTS = [] SITE_ID = 1 -SITE_NAME = 'WLMOJ' -SITE_LONG_NAME = 'WLMOJ: Modern Online Judge' -SITE_ADMIN_EMAIL = '' +SITE_NAME = "WLMOJ" +SITE_LONG_NAME = "WLMOJ: Modern Online Judge" +SITE_ADMIN_EMAIL = "" DMOJ_REQUIRE_STAFF_2FA = True # Display warnings that admins will not perform 2FA recovery. @@ -46,11 +46,11 @@ # 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' -SELECT2_CSS_URL = '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/css/select2.min.css' +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" +SELECT2_CSS_URL = "//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/css/select2.min.css" DMOJ_CAMO_URL = None DMOJ_CAMO_KEY = None @@ -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) @@ -77,7 +92,7 @@ DMOJ_SUBMISSIONS_REJUDGE_LIMIT = 10 # Whether to allow users to view source code: 'all' | 'all-solved' | 'only-own' -DMOJ_SUBMISSION_SOURCE_VISIBILITY = 'all-solved' +DMOJ_SUBMISSION_SOURCE_VISIBILITY = "all-solved" DMOJ_BLOG_NEW_PROBLEM_COUNT = 7 DMOJ_TOTP_TOLERANCE_HALF_MINUTES = 1 DMOJ_SCRATCH_CODES_COUNT = 5 @@ -85,7 +100,7 @@ # Whether to allow users to download their data DMOJ_USER_DATA_DOWNLOAD = False -DMOJ_USER_DATA_CACHE = '' +DMOJ_USER_DATA_CACHE = "" DMOJ_USER_DATA_DOWNLOAD_RATELIMIT = datetime.timedelta(days=1) DMOJ_COMMENT_VOTE_HIDE_THRESHOLD = -5 @@ -99,11 +114,11 @@ DMOJ_STATS_LANGUAGE_THRESHOLD = 10 DMOJ_STATS_SUBMISSION_RESULT_COLORS = { - 'TLE': '#a3bcbd', - 'AC': '#00a92a', - 'WA': '#ed4420', - 'CE': '#42586d', - 'ERR': '#ffa71c', + "TLE": "#a3bcbd", + "AC": "#00a92a", + "WA": "#ed4420", + "CE": "#42586d", + "ERR": "#ffa71c", } DMOJ_API_PAGE_SIZE = 1000 @@ -119,15 +134,15 @@ # At the bare minimum, dark and light theme CSS file locations must be declared DMOJ_THEME_CSS = { - 'light': 'style.css', - 'dark': 'dark/style.css', + "light": "style.css", + "dark": "dark/style.css", } # At the bare minimum, dark and light ace themes must be declared DMOJ_THEME_DEFAULT_ACE_THEME = { - 'light': 'github', - 'dark': 'twilight', + "light": "github", + "dark": "twilight", } -DMOJ_SELECT2_THEME = 'dmoj' +DMOJ_SELECT2_THEME = "dmoj" MARKDOWN_STYLES = {} MARKDOWN_DEFAULT_STYLE = {} @@ -135,14 +150,14 @@ MATHOID_URL = False MATHOID_GZIP = False MATHOID_MML_CACHE = None -MATHOID_CSS_CACHE = 'default' -MATHOID_DEFAULT_TYPE = 'auto' +MATHOID_CSS_CACHE = "default" +MATHOID_DEFAULT_TYPE = "auto" MATHOID_MML_CACHE_TTL = 86400 -MATHOID_CACHE_ROOT = '' +MATHOID_CACHE_ROOT = "" MATHOID_CACHE_URL = False TEXOID_GZIP = False -TEXOID_META_CACHE = 'default' +TEXOID_META_CACHE = "default" TEXOID_META_CACHE_TTL = 86400 DMOJ_NEWSLETTER_ID_ON_REGISTER = None @@ -150,16 +165,18 @@ BAD_MAIL_PROVIDER_REGEX = () NOFOLLOW_EXCLUDED = set() -TIMEZONE_MAP = 'https://static.dmoj.ca/assets/earth.jpg' +TIMEZONE_MAP = "https://static.dmoj.ca/assets/earth.jpg" TERMS_OF_SERVICE_URL = None -DEFAULT_USER_LANGUAGE = 'PY3' +DEFAULT_USER_LANGUAGE = "PY3" 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' -DMOJ_CANONICAL = '' +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" +) +DMOJ_CANONICAL = "" # Application definition @@ -171,133 +188,133 @@ pass else: del wpadmin - INSTALLED_APPS += ('wpadmin',) + INSTALLED_APPS += ("wpadmin",) WPADMIN = { - 'admin': { - 'title': 'DMOJ Admin', - 'menu': { - 'top': 'wpadmin.menu.menus.BasicTopMenu', - 'left': 'wpadmin.menu.custom.CustomModelLeftMenuWithDashboard', + "admin": { + "title": "DMOJ Admin", + "menu": { + "top": "wpadmin.menu.menus.BasicTopMenu", + "left": "wpadmin.menu.custom.CustomModelLeftMenuWithDashboard", }, - 'custom_menu': [ + "custom_menu": [ { - 'model': 'judge.Problem', - 'icon': 'fa-question-circle', - 'children': [ - 'judge.ProblemGroup', - 'judge.ProblemType', - 'judge.License', - 'judge.ProblemPointsVote', + "model": "judge.Problem", + "icon": "fa-question-circle", + "children": [ + "judge.ProblemGroup", + "judge.ProblemType", + "judge.License", + "judge.ProblemPointsVote", ], }, - ('judge.Submission', 'fa-check-square-o'), + ("judge.Submission", "fa-check-square-o"), { - 'model': 'judge.Language', - 'icon': 'fa-file-code-o', - 'children': [ - 'judge.Judge', + "model": "judge.Language", + "icon": "fa-file-code-o", + "children": [ + "judge.Judge", ], }, { - 'model': 'judge.Contest', - 'icon': 'fa-bar-chart', - 'children': [ - 'judge.ContestParticipation', - 'judge.ContestTag', + "model": "judge.Contest", + "icon": "fa-bar-chart", + "children": [ + "judge.ContestParticipation", + "judge.ContestTag", ], }, - ('judge.Ticket', 'fa-bell'), + ("judge.Ticket", "fa-bell"), { - 'model': 'auth.User', - 'icon': 'fa-user', - 'children': [ - 'judge.Profile', - 'auth.Group', - 'registration.RegistrationProfile', + "model": "auth.User", + "icon": "fa-user", + "children": [ + "judge.Profile", + "auth.Group", + "registration.RegistrationProfile", ], }, { - 'model': 'judge.Organization', - 'icon': 'fa-users', - 'children': [ - 'judge.OrganizationRequest', - 'judge.Class', + "model": "judge.Organization", + "icon": "fa-users", + "children": [ + "judge.OrganizationRequest", + "judge.Class", ], }, { - 'model': 'judge.NavigationBar', - 'icon': 'fa-bars', - 'children': [ - 'sites.Site', - 'redirects.Redirect', + "model": "judge.NavigationBar", + "icon": "fa-bars", + "children": [ + "sites.Site", + "redirects.Redirect", ], }, - ('judge.BlogPost', 'fa-rss-square'), + ("judge.BlogPost", "fa-rss-square"), { - 'model': 'judge.Comment', - 'icon': 'fa-comment-o', - 'children': [ - 'judge.CommentLock', + "model": "judge.Comment", + "icon": "fa-comment-o", + "children": [ + "judge.CommentLock", ], }, - ('flatpages.FlatPage', 'fa-file-text-o'), - ('judge.MiscConfig', 'fa-question-circle'), + ("flatpages.FlatPage", "fa-file-text-o"), + ("judge.MiscConfig", "fa-question-circle"), ], - 'dashboard': { - 'breadcrumbs': True, + "dashboard": { + "breadcrumbs": True, }, }, } INSTALLED_APPS += ( - 'django.contrib.admin', - 'judge', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.flatpages', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.redirects', - 'django.contrib.staticfiles', - 'django.contrib.sites', - 'django.contrib.sitemaps', - 'registration', - 'mptt', - 'reversion', - 'django_social_share', - 'social_django', - 'compressor', - 'django_ace', - 'pagedown', - 'sortedm2m', - 'statici18n', - 'impersonate', - 'django_jinja', - 'martor', - 'adminsortable2', + "django.contrib.admin", + "judge", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.flatpages", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.redirects", + "django.contrib.staticfiles", + "django.contrib.sites", + "django.contrib.sitemaps", + "registration", + "mptt", + "reversion", + "django_social_share", + "social_django", + "compressor", + "django_ace", + "pagedown", + "sortedm2m", + "statici18n", + "impersonate", + "django_jinja", + "martor", + "adminsortable2", ) MIDDLEWARE = ( - 'judge.middleware.ShortCircuitMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.locale.LocaleMiddleware', - 'judge.middleware.APIMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'judge.middleware.MiscConfigMiddleware', - 'judge.middleware.DMOJLoginMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'judge.user_log.LogUserAccessMiddleware', - 'judge.timezone.TimezoneMiddleware', - 'impersonate.middleware.ImpersonateMiddleware', - 'judge.middleware.DMOJImpersonationMiddleware', - 'judge.middleware.ContestMiddleware', - 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware', - 'judge.social_auth.SocialAuthExceptionMiddleware', - 'django.contrib.redirects.middleware.RedirectFallbackMiddleware', + "judge.middleware.ShortCircuitMiddleware", + "django.middleware.common.CommonMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.locale.LocaleMiddleware", + "judge.middleware.APIMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "judge.middleware.MiscConfigMiddleware", + "judge.middleware.DMOJLoginMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "judge.user_log.LogUserAccessMiddleware", + "judge.timezone.TimezoneMiddleware", + "impersonate.middleware.ImpersonateMiddleware", + "judge.middleware.DMOJImpersonationMiddleware", + "judge.middleware.ContestMiddleware", + "django.contrib.flatpages.middleware.FlatpageFallbackMiddleware", + "judge.social_auth.SocialAuthExceptionMiddleware", + "django.contrib.redirects.middleware.RedirectFallbackMiddleware", ) IMPERSONATE_REQUIRE_SUPERUSER = True @@ -307,290 +324,365 @@ AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'judge.utils.pwned.PwnedPasswordsValidator', + "NAME": "judge.utils.pwned.PwnedPasswordsValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] -SILENCED_SYSTEM_CHECKS = ['urls.W002', 'fields.W342'] +SILENCED_SYSTEM_CHECKS = ["urls.W002", "fields.W342"] -ROOT_URLCONF = 'dmoj.urls' -LOGIN_REDIRECT_URL = '/user' -WSGI_APPLICATION = 'dmoj.wsgi.application' -DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' +ROOT_URLCONF = "dmoj.urls" +LOGIN_REDIRECT_URL = "/user" +WSGI_APPLICATION = "dmoj.wsgi.application" +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" TEMPLATES = [ { - 'BACKEND': 'django_jinja.backend.Jinja2', - 'DIRS': [ - os.path.join(BASE_DIR, 'templates'), + "BACKEND": "django_jinja.backend.Jinja2", + "DIRS": [ + os.path.join(BASE_DIR, "templates"), ], - 'APP_DIRS': False, - 'OPTIONS': { - 'match_extension': ('.html', '.txt'), - 'match_regex': '^(?!admin/)', - 'context_processors': [ - 'django.template.context_processors.media', - 'django.template.context_processors.tz', - 'django.template.context_processors.i18n', - 'django.template.context_processors.request', - 'django.contrib.messages.context_processors.messages', - 'judge.template_context.comet_location', - 'judge.template_context.get_resource', - 'judge.template_context.general_info', - 'judge.template_context.site', - 'judge.template_context.site_name', - 'judge.template_context.site_theme', - 'judge.template_context.misc_config', - 'judge.template_context.math_setting', - 'social_django.context_processors.backends', - 'social_django.context_processors.login_redirect', + "APP_DIRS": False, + "OPTIONS": { + "match_extension": (".html", ".txt"), + "match_regex": "^(?!admin/)", + "context_processors": [ + "django.template.context_processors.media", + "django.template.context_processors.tz", + "django.template.context_processors.i18n", + "django.template.context_processors.request", + "django.contrib.messages.context_processors.messages", + "judge.template_context.comet_location", + "judge.template_context.get_resource", + "judge.template_context.general_info", + "judge.template_context.site", + "judge.template_context.site_name", + "judge.template_context.site_theme", + "judge.template_context.misc_config", + "judge.template_context.math_setting", + "social_django.context_processors.backends", + "social_django.context_processors.login_redirect", ], - 'autoescape': select_autoescape(['html', 'xml']), - 'trim_blocks': True, - 'lstrip_blocks': True, - 'translation_engine': 'judge.utils.safe_translations', - 'extensions': DEFAULT_EXTENSIONS + [ - 'compressor.contrib.jinja2ext.CompressorExtension', - 'judge.jinja2.DMOJExtension', - 'judge.jinja2.spaceless.SpacelessExtension', + "autoescape": select_autoescape(["html", "xml"]), + "trim_blocks": True, + "lstrip_blocks": True, + "translation_engine": "judge.utils.safe_translations", + "extensions": DEFAULT_EXTENSIONS + + [ + "compressor.contrib.jinja2ext.CompressorExtension", + "judge.jinja2.DMOJExtension", + "judge.jinja2.spaceless.SpacelessExtension", ], }, }, { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'APP_DIRS': True, - 'DIRS': [ - os.path.join(BASE_DIR, 'templates'), + "BACKEND": "django.template.backends.django.DjangoTemplates", + "APP_DIRS": True, + "DIRS": [ + os.path.join(BASE_DIR, "templates"), ], - 'OPTIONS': { - 'context_processors': [ - 'django.contrib.auth.context_processors.auth', - 'django.template.context_processors.media', - 'django.template.context_processors.tz', - 'django.template.context_processors.i18n', - 'django.template.context_processors.request', - 'django.contrib.messages.context_processors.messages', + "OPTIONS": { + "context_processors": [ + "django.contrib.auth.context_processors.auth", + "django.template.context_processors.media", + "django.template.context_processors.tz", + "django.template.context_processors.i18n", + "django.template.context_processors.request", + "django.contrib.messages.context_processors.messages", ], }, }, ] LOCALE_PATHS = [ - os.path.join(BASE_DIR, 'locale'), + os.path.join(BASE_DIR, "locale"), ] LANGUAGES = [ - ('ca', _('Catalan')), - ('de', _('German')), - ('el', _('Greek')), - ('en', _('English')), - ('es', _('Spanish')), - ('fr', _('French')), - ('hr', _('Croatian')), - ('hu', _('Hungarian')), - ('ja', _('Japanese')), - ('ko', _('Korean')), - ('pt', _('Brazilian Portuguese')), - ('ro', _('Romanian')), - ('ru', _('Russian')), - ('sr-latn', _('Serbian (Latin)')), - ('tr', _('Turkish')), - ('vi', _('Vietnamese')), - ('zh-hans', _('Simplified Chinese')), - ('zh-hant', _('Traditional Chinese')), + ("ca", _("Catalan")), + ("de", _("German")), + ("el", _("Greek")), + ("en", _("English")), + ("es", _("Spanish")), + ("fr", _("French")), + ("hr", _("Croatian")), + ("hu", _("Hungarian")), + ("ja", _("Japanese")), + ("ko", _("Korean")), + ("pt", _("Brazilian Portuguese")), + ("ro", _("Romanian")), + ("ru", _("Russian")), + ("sr-latn", _("Serbian (Latin)")), + ("tr", _("Turkish")), + ("vi", _("Vietnamese")), + ("zh-hans", _("Simplified Chinese")), + ("zh-hant", _("Traditional Chinese")), ] 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', - 'a', - 'style', 'noscript', 'center', + "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", ] BLEACH_USER_SAFE_ATTRS = { - '*': ['id', 'class', 'style'], - 'img': ['src', 'alt', 'title', 'width', 'height', 'data-src', 'align'], - 'a': ['href', 'alt', 'title'], - 'abbr': ['title'], - 'dfn': ['title'], - 'time': ['datetime'], - 'data': ['value'], - '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'], - 'source': ['src', 'srcset', 'type'], - 'li': ['value'], + "*": ["id", "class", "style"], + "img": ["src", "alt", "title", "width", "height", "data-src", "align"], + "a": ["href", "alt", "title"], + "abbr": ["title"], + "dfn": ["title"], + "time": ["datetime"], + "data": ["value"], + "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", + ], + "source": ["src", "srcset", "type"], + "li": ["value"], } MARKDOWN_STAFF_EDITABLE_STYLE = { - 'safe_mode': False, - 'use_camo': True, - 'texoid': True, - 'math': True, - 'bleach': { - 'tags': BLEACH_USER_SAFE_TAGS, - 'attributes': BLEACH_USER_SAFE_ATTRS, - 'styles': True, - 'mathml': True, + "safe_mode": False, + "use_camo": True, + "texoid": True, + "math": True, + "bleach": { + "tags": BLEACH_USER_SAFE_TAGS, + "attributes": BLEACH_USER_SAFE_ATTRS, + "styles": True, + "mathml": True, }, } MARKDOWN_ADMIN_EDITABLE_STYLE = { - 'safe_mode': False, - 'use_camo': True, - 'texoid': True, - 'math': True, + "safe_mode": False, + "use_camo": True, + "texoid": True, + "math": True, } MARKDOWN_DEFAULT_STYLE = { - 'safe_mode': True, - 'nofollow': True, - 'use_camo': True, - 'math': True, + "safe_mode": True, + "nofollow": True, + "use_camo": True, + "math": True, } MARKDOWN_USER_LARGE_STYLE = { - 'safe_mode': True, - 'nofollow': True, - 'use_camo': True, - 'math': True, + "safe_mode": True, + "nofollow": True, + "use_camo": True, + "math": True, } MARKDOWN_STYLES = { - 'default': MARKDOWN_DEFAULT_STYLE, - 'comment': MARKDOWN_DEFAULT_STYLE, - 'self-description': MARKDOWN_USER_LARGE_STYLE, - 'problem': MARKDOWN_STAFF_EDITABLE_STYLE, - 'problem-full': MARKDOWN_ADMIN_EDITABLE_STYLE, - 'contest': MARKDOWN_ADMIN_EDITABLE_STYLE, - 'flatpage': MARKDOWN_ADMIN_EDITABLE_STYLE, - 'language': MARKDOWN_STAFF_EDITABLE_STYLE, - 'license': MARKDOWN_STAFF_EDITABLE_STYLE, - 'judge': MARKDOWN_STAFF_EDITABLE_STYLE, - 'blog': MARKDOWN_STAFF_EDITABLE_STYLE, - 'solution': MARKDOWN_STAFF_EDITABLE_STYLE, - 'contest_tag': MARKDOWN_STAFF_EDITABLE_STYLE, - 'organization-about': MARKDOWN_USER_LARGE_STYLE, - 'ticket': MARKDOWN_USER_LARGE_STYLE, + "default": MARKDOWN_DEFAULT_STYLE, + "comment": MARKDOWN_DEFAULT_STYLE, + "self-description": MARKDOWN_USER_LARGE_STYLE, + "problem": MARKDOWN_STAFF_EDITABLE_STYLE, + "problem-full": MARKDOWN_ADMIN_EDITABLE_STYLE, + "contest": MARKDOWN_ADMIN_EDITABLE_STYLE, + "flatpage": MARKDOWN_ADMIN_EDITABLE_STYLE, + "language": MARKDOWN_STAFF_EDITABLE_STYLE, + "license": MARKDOWN_STAFF_EDITABLE_STYLE, + "judge": MARKDOWN_STAFF_EDITABLE_STYLE, + "blog": MARKDOWN_STAFF_EDITABLE_STYLE, + "solution": MARKDOWN_STAFF_EDITABLE_STYLE, + "contest_tag": MARKDOWN_STAFF_EDITABLE_STYLE, + "organization-about": MARKDOWN_USER_LARGE_STYLE, + "ticket": MARKDOWN_USER_LARGE_STYLE, } MARTOR_ENABLE_CONFIGS = { - 'imgur': 'true', - 'mention': 'true', - 'jquery': 'false', - 'living': 'false', - 'spellcheck': 'true', - 'hljs': 'false', + "imgur": "true", + "mention": "true", + "jquery": "false", + "living": "false", + "spellcheck": "true", + "hljs": "false", } -MARTOR_MARKDOWNIFY_URL = '/widgets/preview/default' -MARTOR_SEARCH_USERS_URL = '/widgets/martor/search-user' -MARTOR_UPLOAD_URL = '/widgets/martor/upload-image' -MARTOR_MARKDOWN_BASE_MENTION_URL = '/user/' +MARTOR_MARKDOWNIFY_URL = "/widgets/preview/default" +MARTOR_SEARCH_USERS_URL = "/widgets/martor/search-user" +MARTOR_UPLOAD_URL = "/widgets/martor/upload-image" +MARTOR_MARKDOWN_BASE_MENTION_URL = "/user/" # Directory under MEDIA_ROOT to use to store image uploaded through martor. -MARTOR_UPLOAD_MEDIA_DIR = 'martor' -MARTOR_UPLOAD_SAFE_EXTS = {'.jpg', '.png', '.gif'} +MARTOR_UPLOAD_MEDIA_DIR = "martor" +MARTOR_UPLOAD_SAFE_EXTS = {".jpg", ".png", ".gif"} # Database # https://docs.djangoproject.com/en/3.2/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.path.join(BASE_DIR, "db.sqlite3"), }, } ENABLE_FTS = False # Bridged configuration -BRIDGED_JUDGE_ADDRESS = [('localhost', 9999)] +BRIDGED_JUDGE_ADDRESS = [("localhost", 9999)] BRIDGED_JUDGE_PROXIES = None -BRIDGED_DJANGO_ADDRESS = [('localhost', 9998)] +BRIDGED_DJANGO_ADDRESS = [("localhost", 9998)] BRIDGED_DJANGO_CONNECT = None # Event Server configuration EVENT_DAEMON_USE = False -EVENT_DAEMON_POST = 'ws://localhost:9997/' -EVENT_DAEMON_GET = 'ws://localhost:9996/' -EVENT_DAEMON_POLL = '/channels/' +EVENT_DAEMON_POST = "ws://localhost:9997/" +EVENT_DAEMON_GET = "ws://localhost:9996/" +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_AMQP_EXCHANGE = "dmoj-events" +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/ # Whatever you do, this better be one of the entries in `LANGUAGES`. -LANGUAGE_CODE = 'en' -TIME_ZONE = 'UTC' -DEFAULT_USER_TIME_ZONE = 'America/Toronto' +LANGUAGE_CODE = "en" +TIME_ZONE = "UTC" +DEFAULT_USER_TIME_ZONE = "America/Toronto" USE_I18N = True USE_L10N = True USE_TZ = True # Cookies -SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db' +SESSION_ENGINE = "django.contrib.sessions.backends.cached_db" # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.2/howto/static-files/ -DMOJ_RESOURCES = os.path.join(BASE_DIR, 'resources') +DMOJ_RESOURCES = os.path.join(BASE_DIR, "resources") STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + "django.contrib.staticfiles.finders.FileSystemFinder", + "django.contrib.staticfiles.finders.AppDirectoriesFinder", ) STATICFILES_DIRS = [ - os.path.join(BASE_DIR, 'resources'), + os.path.join(BASE_DIR, "resources"), ] -STATIC_URL = '/static/' +STATIC_URL = "/static/" # Define a cache CACHES = {} # Authentication AUTHENTICATION_BACKENDS = ( - 'social_core.backends.google.GoogleOAuth2', - 'social_core.backends.facebook.FacebookOAuth2', - 'judge.social_auth.GitHubSecureEmailOAuth2', - 'django.contrib.auth.backends.ModelBackend', + "social_core.backends.google.GoogleOAuth2", + "social_core.backends.facebook.FacebookOAuth2", + "judge.social_auth.GitHubSecureEmailOAuth2", + "django.contrib.auth.backends.ModelBackend", ) SOCIAL_AUTH_PIPELINE = ( - 'social_core.pipeline.social_auth.social_details', - 'social_core.pipeline.social_auth.social_uid', - 'social_core.pipeline.social_auth.auth_allowed', - 'judge.social_auth.verify_email', - 'social_core.pipeline.social_auth.social_user', - 'social_core.pipeline.user.get_username', - 'social_core.pipeline.social_auth.associate_by_email', - 'judge.social_auth.choose_username', - 'social_core.pipeline.user.create_user', - 'judge.social_auth.make_profile', - 'social_core.pipeline.social_auth.associate_user', - 'social_core.pipeline.social_auth.load_extra_data', - 'social_core.pipeline.user.user_details', + "social_core.pipeline.social_auth.social_details", + "social_core.pipeline.social_auth.social_uid", + "social_core.pipeline.social_auth.auth_allowed", + "judge.social_auth.verify_email", + "social_core.pipeline.social_auth.social_user", + "social_core.pipeline.user.get_username", + "social_core.pipeline.social_auth.associate_by_email", + "judge.social_auth.choose_username", + "social_core.pipeline.user.create_user", + "judge.social_auth.make_profile", + "social_core.pipeline.social_auth.associate_user", + "social_core.pipeline.social_auth.load_extra_data", + "social_core.pipeline.user.user_details", ) -SOCIAL_AUTH_GITHUB_SECURE_SCOPE = ['user:email'] -SOCIAL_AUTH_FACEBOOK_SCOPE = ['email'] +SOCIAL_AUTH_GITHUB_SECURE_SCOPE = ["user:email"] +SOCIAL_AUTH_FACEBOOK_SCOPE = ["email"] SOCIAL_AUTH_SLUGIFY_USERNAMES = True -SOCIAL_AUTH_SLUGIFY_FUNCTION = 'judge.social_auth.slugify_username' +SOCIAL_AUTH_SLUGIFY_FUNCTION = "judge.social_auth.slugify_username" MOSS_API_KEY = None @@ -599,7 +691,7 @@ WEBAUTHN_RP_ID = None try: - with open(os.path.join(os.path.dirname(__file__), 'local_settings.py')) as f: + with open(os.path.join(os.path.dirname(__file__), "local_settings.py")) as f: exec(f.read(), globals()) except IOError: pass @@ -618,5 +710,5 @@ assert DMOJ_PDF_PROBLEM_INTERNAL is None or DMOJ_PDF_PROBLEM_CACHE is not None # Compute these values after local_settings.py is loaded -ACE_DEFAULT_LIGHT_THEME = DMOJ_THEME_DEFAULT_ACE_THEME['light'] -ACE_DEFAULT_DARK_THEME = DMOJ_THEME_DEFAULT_ACE_THEME['dark'] +ACE_DEFAULT_LIGHT_THEME = DMOJ_THEME_DEFAULT_ACE_THEME["light"] +ACE_DEFAULT_DARK_THEME = DMOJ_THEME_DEFAULT_ACE_THEME["dark"] diff --git a/dmoj/throttle_mail.py b/dmoj/throttle_mail.py index 7346a2193e..047e261851 100644 --- a/dmoj/throttle_mail.py +++ b/dmoj/throttle_mail.py @@ -8,8 +8,8 @@ def new_email(): - cache.add('error_email_throttle', 0, settings.DMOJ_EMAIL_THROTTLING[1]) - return cache.incr('error_email_throttle') + cache.add("error_email_throttle", 0, settings.DMOJ_EMAIL_THROTTLING[1]) + return cache.incr("error_email_throttle") class ThrottledEmailHandler(AdminEmailHandler): diff --git a/dmoj/urls.py b/dmoj/urls.py index 08b7812ed7..50c01dd393 100644 --- a/dmoj/urls.py +++ b/dmoj/urls.py @@ -10,386 +10,958 @@ 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('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('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('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('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('api/token/generate/', user.generate_api_token, name='generate_api_token'), - path('api/token/remove/', user.remove_api_token, name='remove_api_token'), + 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("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("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("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("api/token/generate/", user.generate_api_token, name="generate_api_token"), + path("api/token/remove/", user.remove_api_token, name="remove_api_token"), ] def exception(request): if not request.user.is_superuser: raise Http404() - raise RuntimeError('@Xyene asked me to cause this') + raise RuntimeError("@Xyene asked me to cause this") 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('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('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('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('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('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('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('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('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( + "", + 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("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("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("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("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("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("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("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), + ] + ), + ), ] -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), - ))) - -handler404 = 'judge.views.error.error404' -handler403 = 'judge.views.error.error403' -handler500 = 'judge.views.error.error500' - -if 'newsletter' in settings.INSTALLED_APPS: - urlpatterns.append(path('newsletter/', include('newsletter.urls'))) -if 'impersonate' in settings.INSTALLED_APPS: - urlpatterns.append(path('impersonate/', include('impersonate.urls'))) + urlpatterns.append( + path( + favicon, + RedirectView.as_view( + url=static_lazy("icons/" + favicon), + ), + ) + ) + +handler404 = "judge.views.error.error404" +handler403 = "judge.views.error.error403" +handler500 = "judge.views.error.error500" + +if "newsletter" in settings.INSTALLED_APPS: + urlpatterns.append(path("newsletter/", include("newsletter.urls"))) +if "impersonate" in settings.INSTALLED_APPS: + urlpatterns.append(path("impersonate/", include("impersonate.urls"))) diff --git a/dmoj/wsgi.py b/dmoj/wsgi.py index 6bec753460..3cde2a490a 100644 --- a/dmoj/wsgi.py +++ b/dmoj/wsgi.py @@ -1,5 +1,6 @@ import os -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dmoj.settings') + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dmoj.settings") try: import MySQLdb # noqa: F401, imported for side effect @@ -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..f208e2f879 100644 --- a/dmoj/wsgi_async.py +++ b/dmoj/wsgi_async.py @@ -2,11 +2,14 @@ import gevent.monkey # noqa: I100, gevent must be imported here -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dmoj.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dmoj.settings") gevent.monkey.patch_all() # 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..8d7ed79a87 100644 --- a/dmoj_bridge_async.py +++ b/dmoj_bridge_async.py @@ -2,16 +2,19 @@ import gevent.monkey # noqa: I100, gevent must be imported here -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dmoj.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dmoj.settings") gevent.monkey.patch_all() # noinspection PyUnresolvedReferences 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__': +if __name__ == "__main__": judge_daemon() diff --git a/dmoj_celery.py b/dmoj_celery.py index 3f9701f17e..bec542ca90 100644 --- a/dmoj_celery.py +++ b/dmoj_celery.py @@ -6,7 +6,7 @@ import dmoj_install_pymysql # noqa: F401, imported for side effect # set the default Django settings module for the 'celery' program. -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dmoj.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dmoj.settings") # noinspection PyUnresolvedReferences from dmoj.celery import app # noqa: E402, F401, imported for side effect diff --git a/dmoj_install_pymysql.py b/dmoj_install_pymysql.py index 0687155088..e1795f5658 100644 --- a/dmoj_install_pymysql.py +++ b/dmoj_install_pymysql.py @@ -1,4 +1,4 @@ import pymysql pymysql.install_as_MySQLdb() -pymysql.version_info = (1, 4, 0, 'final', 0) +pymysql.version_info = (1, 4, 0, "final", 0) diff --git a/judge/__init__.py b/judge/__init__.py index 5c386cd75e..8aefa9df3f 100644 --- a/judge/__init__.py +++ b/judge/__init__.py @@ -1 +1 @@ -default_app_config = 'judge.apps.JudgeAppConfig' +default_app_config = "judge.apps.JudgeAppConfig" 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..f07af07924 100644 --- a/judge/admin/comments.py +++ b/judge/admin/comments.py @@ -13,54 +13,68 @@ class CommentForm(ModelForm): 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')}), + "author": AdminHeavySelect2Widget(data_view="profile_select2"), + "parent": AdminHeavySelect2Widget(data_view="comment_select2"), + "body": AdminMartorWidget( + attrs={"data-markdownfy-url": reverse_lazy("comment_preview")} + ), } class CommentAdmin(VersionAdmin): fieldsets = ( - (None, {'fields': ('author', 'page', 'parent', 'time', 'score', 'hidden')}), - (_('Content'), {'fields': ('body',)}), + (None, {"fields": ("author", "page", "parent", "time", "score", "hidden")}), + (_("Content"), {"fields": ("body",)}), ) - list_display = ['author', 'linked_page', 'time', 'score', 'hidden'] - search_fields = ['author__user__username', 'page', 'body'] - actions = ['hide_comment', 'unhide_comment'] - list_filter = ['hidden'] - readonly_fields = ['time', 'score'] + list_display = ["author", "linked_page", "time", "score", "hidden"] + search_fields = ["author__user__username", "page", "body"] + actions = ["hide_comment", "unhide_comment"] + list_filter = ["hidden"] + readonly_fields = ["time", "score"] actions_on_top = True actions_on_bottom = True form = CommentForm - date_hierarchy = 'time' + date_hierarchy = "time" def get_queryset(self, request): - return Comment.objects.order_by('-time') + return Comment.objects.order_by("-time") - @admin.display(description=_('Hide comments')) + @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')) + @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') + @admin.display(description=_("associated page"), ordering="page") def linked_page(self, obj): link = obj.link if link is not None: return format_html('{1}', link, obj.page) else: - return format_html('{0}', obj.page) + return format_html("{0}", obj.page) def save_model(self, request, obj, form, change): - obj.revisions = F('revisions') + 1 + obj.revisions = F("revisions") + 1 super().save_model(request, obj, form, change) if obj.hidden: obj.get_descendants().update(hidden=obj.hidden) diff --git a/judge/admin/contest.py b/judge/admin/contest.py index 59c25d103a..cafa7ee494 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): @@ -28,191 +41,299 @@ def is_hidden(self): class ContestTagForm(ModelForm): contests = ModelMultipleChoiceField( - label=_('Included contests'), + 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): - fields = ('name', 'color', 'description', 'contests') - list_display = ('name', 'color') + fields = ("name", "color", "description", "contests") + list_display = ("name", "color") actions_on_top = True actions_on_bottom = True form = ContestTagForm formfield_overrides = { - TextField: {'widget': AdminMartorWidget}, + TextField: {"widget": AdminMartorWidget}, } def save_model(self, request, obj, form, change): super(ContestTagAdmin, self).save_model(request, obj, form, change) - obj.contests.set(form.cleaned_data['contests']) + obj.contests.set(form.cleaned_data["contests"]) def get_form(self, request, obj=None, **kwargs): form = super(ContestTagAdmin, self).get_form(request, obj, **kwargs) if obj is not None: - form.base_fields['contests'].initial = obj.contests.all() + form.base_fields["contests"].initial = obj.contests.all() return form class ContestProblemInlineForm(ModelForm): class Meta: - widgets = {'problem': AdminHeavySelect2Widget(data_view='problem_select2')} + widgets = {"problem": AdminHeavySelect2Widget(data_view="problem_select2")} 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') - readonly_fields = ('rejudge_column',) + verbose_name = _("Problem") + verbose_name_plural = _("Problems") + fields = ( + "problem", + "points", + "partial", + "is_pretested", + "max_submissions", + "output_prefix_override", + "order", + "rejudge_column", + ) + readonly_fields = ("rejudge_column",) form = ContestProblemInlineForm - @admin.display(description='') + @admin.display(description="") 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 "" + return format_html( + '{1}', + reverse("admin:judge_contest_rejudge", args=(obj.contest.id, obj.id)), + _("Rejudge"), + ) class ContestForm(ModelForm): def __init__(self, *args, **kwargs): super(ContestForm, self).__init__(*args, **kwargs) - if 'rate_exclude' in self.fields: + 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 - self.fields['view_contest_scoreboard'].widget.can_add_related = False + self.fields["rate_exclude"].queryset = Profile.objects.none() + self.fields["banned_users"].widget.can_add_related = False + self.fields["view_contest_scoreboard"].widget.can_add_related = False 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 = { - 'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2'), - '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'), - 'classes': AdminHeavySelect2MultipleWidget(data_view='class_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')}), + "authors": AdminHeavySelect2MultipleWidget(data_view="profile_select2"), + "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" + ), + "classes": AdminHeavySelect2MultipleWidget(data_view="class_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")} + ), } 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')}), - (_('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')}), - (_('Justice'), {'fields': ('banned_users',)}), + ( + 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", + ) + }, + ), + (_("Justice"), {"fields": ("banned_users",)}), ) - list_display = ('key', 'name', 'is_visible', 'is_rated', 'locked_after', 'start_time', 'end_time', 'time_limit', - 'user_count') - search_fields = ('key', 'name') + 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 actions_on_bottom = True form = ContestForm - change_list_template = 'admin/judge/contest/change_list.html' - filter_horizontal = ['rate_exclude'] - date_hierarchy = 'start_time' + change_list_template = "admin/judge/contest/change_list.html" + filter_horizontal = ["rate_exclude"] + date_hierarchy = "start_time" def get_actions(self, request): actions = super(ContestAdmin, self).get_actions(request) - if request.user.has_perm('judge.change_contest_visibility') or \ - request.user.has_perm('judge.create_private_contest'): - for action in ('make_visible', 'make_hidden'): + 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) - if request.user.has_perm('judge.lock_contest'): - for action in ('set_locked', 'set_unlocked'): + if request.user.has_perm("judge.lock_contest"): + for action in ("set_locked", "set_unlocked"): actions[action] = self.get_action(action) return actions def get_queryset(self, request): queryset = Contest.objects.all() - if request.user.has_perm('judge.edit_all_contest'): + 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 = [] - if not request.user.has_perm('judge.contest_rating'): - readonly += ['is_rated', 'rate_all', 'rate_exclude'] - if not request.user.has_perm('judge.lock_contest'): - readonly += ['locked_after'] - if not request.user.has_perm('judge.contest_access_code'): - readonly += ['access_code'] - if not request.user.has_perm('judge.create_private_contest'): - readonly += ['private_contestants', 'organizations'] - if not request.user.has_perm('judge.change_contest_visibility'): - readonly += ['is_visible'] - if not request.user.has_perm('judge.contest_problem_label'): - readonly += ['problem_label_script'] + if not request.user.has_perm("judge.contest_rating"): + readonly += ["is_rated", "rate_all", "rate_exclude"] + if not request.user.has_perm("judge.lock_contest"): + readonly += ["locked_after"] + if not request.user.has_perm("judge.contest_access_code"): + readonly += ["access_code"] + if not request.user.has_perm("judge.create_private_contest"): + readonly += ["private_contestants", "organizations"] + if not request.user.has_perm("judge.change_contest_visibility"): + readonly += ["is_visible"] + if not request.user.has_perm("judge.contest_problem_label"): + readonly += ["problem_label_script"] return readonly def save_model(self, request, obj, form, change): # `private_contestants` and `organizations` will not appear in `cleaned_data` if user cannot edit it if form.changed_data: - 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']) - if 'join_organizations' in form.cleaned_data: - obj.limit_join_organizations = bool(form.cleaned_data['join_organizations']) + 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"] + ) + if "join_organizations" in form.cleaned_data: + 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'): + if not request.user.has_perm("judge.create_private_contest"): raise PermissionDenied super().save_model(request, obj, form, change) # We need this flag because `save_related` deals with the inlines, but does not know if we have already rescored self._rescored = False - if form.changed_data and any(f in form.changed_data for f in ('format_config', 'format_name')): + 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 - if form.changed_data and 'locked_after' in form.changed_data: - self.set_locked_after(obj, form.cleaned_data['locked_after']) + if form.changed_data and "locked_after" in form.changed_data: + self.set_locked_after(obj, form.cleaned_data["locked_after"]) def save_related(self, request, form, formsets, change): super().save_related(request, form, formsets, change) # Only rescored if we did not already do so in `save_model` if not self._rescored and any(formset.has_changed() for formset in formsets): - self._rescore(form.cleaned_data['key']) + self._rescore(form.cleaned_data["key"]) def has_change_permission(self, request, obj=None): - if not request.user.has_perm('judge.edit_own_contest'): + if not request.user.has_perm("judge.edit_own_contest"): return False if obj is None: return True @@ -220,150 +341,224 @@ 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')) + @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)) + if not request.user.has_perm("judge.change_contest_visibility"): + queryset = queryset.filter( + Q(is_private=True) | Q(is_organization_private=True) + ) count = queryset.update(is_visible=True) - self.message_user(request, 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')) + @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)) + if not request.user.has_perm("judge.change_contest_visibility"): + queryset = queryset.filter( + Q(is_private=True) | Q(is_organization_private=True) + ) count = queryset.update(is_visible=True) - self.message_user(request, 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')) + @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')) + @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("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", + ), ] + 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'): + if not request.user.has_perm("judge.contest_rating"): raise PermissionDenied() with transaction.atomic(): with connection.cursor() as cursor: - cursor.execute('TRUNCATE TABLE `%s`' % Rating._meta.db_table) + 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')) + return HttpResponseRedirect(reverse("admin:judge_contest_changelist")) def rate_view(self, request, id): - if not request.user.has_perm('judge.contest_rating'): + if not request.user.has_perm("judge.contest_rating"): raise PermissionDenied() contest = get_object_or_404(Contest, id=id) if not contest.is_rated or not contest.ended: 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) - if 'problem_label_script' in form.base_fields: + if "problem_label_script" in form.base_fields: # form.base_fields['problem_label_script'] does not exist when the user has only view permission # on the model. - form.base_fields['problem_label_script'].widget = AceWidget( - mode='lua', theme=request.profile.resolved_ace_theme, + form.base_fields["problem_label_script"].widget = AceWidget( + 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), + perms = ("edit_own_contest", "edit_all_contest") + form.base_fields["curators"].queryset = Profile.objects.filter( + Q(user__is_superuser=True) + | Q(user__groups__permissions__codename__in=perms) + | Q(user__user_permissions__codename__in=perms), ).distinct() - form.base_fields['classes'].queryset = Class.get_visible_classes(request.user) + form.base_fields["classes"].queryset = Class.get_visible_classes(request.user) return form class ContestParticipationForm(ModelForm): class Meta: widgets = { - 'contest': AdminSelect2Widget(), - 'user': AdminHeavySelect2Widget(data_view='profile_select2'), + "contest": AdminSelect2Widget(), + "user": AdminHeavySelect2Widget(data_view="profile_select2"), } class ContestParticipationAdmin(admin.ModelAdmin): - fields = ('contest', 'user', 'real_start', 'virtual', 'is_disqualified') - list_display = ('contest', 'username', 'show_virtual', 'real_start', 'score', 'cumtime', 'tiebreaker') - actions = ['recalculate_results'] + fields = ("contest", "user", "real_start", "virtual", "is_disqualified") + 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') + search_fields = ("contest__key", "contest__name", "user__user__username") form = ContestParticipationForm - date_hierarchy = 'real_start' + 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): super().save_model(request, obj, form, change) - if form.changed_data and 'is_disqualified' in form.changed_data: + if form.changed_data and "is_disqualified" in form.changed_data: obj.set_disqualified(obj.is_disqualified) - @admin.display(description=_('Recalculate results')) + @admin.display(description=_("Recalculate results")) def recalculate_results(self, request, queryset): count = 0 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') + @admin.display(description=_("username"), ordering="user__user__username") def username(self, obj): return obj.user.username - @admin.display(description=_('virtual'), ordering='virtual') + @admin.display(description=_("virtual"), ordering="virtual") def show_virtual(self, obj): - return obj.virtual or '-' + return obj.virtual or "-" diff --git a/judge/admin/interface.py b/judge/admin/interface.py index f49c3a654c..778ac383da 100644 --- a/judge/admin/interface.py +++ b/judge/admin/interface.py @@ -11,21 +11,25 @@ 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): - list_display = DraggableMPTTAdmin.list_display + ('key', 'linked_path') - fields = ('key', 'label', 'path', 'order', 'regex', 'parent') + list_display = DraggableMPTTAdmin.list_display + ("key", "linked_path") + fields = ("key", "label", "path", "order", "regex", "parent") list_editable = () # Bug in SortableModelAdmin: 500 without list_editable being set mptt_level_indent = 20 - sortable = 'order' + sortable = "order" def __init__(self, *args, **kwargs): super(NavigationBarAdmin, self).__init__(*args, **kwargs) self.__save_model_calls = 0 - @admin.display(description=_('link path')) + @admin.display(description=_("link path")) def linked_path(self, obj): return format_html('{0}', obj.path) @@ -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): @@ -55,44 +65,53 @@ class FlatPageAdmin(VersionAdmin, OldFlatPageAdmin): class BlogPostForm(ModelForm): def __init__(self, *args, **kwargs): super(BlogPostForm, self).__init__(*args, **kwargs) - if 'authors' in self.fields: + if "authors" in self.fields: # self.fields['authors'] does not exist when the user has only view permission on the model. - self.fields['authors'].widget.can_add_related = False + self.fields["authors"].widget.can_add_related = False 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')}), - (_('Content'), {'fields': ('content', 'og_image')}), - (_('Summary'), {'classes': ('collapse',), 'fields': ('summary',)}), + ( + None, + {"fields": ("title", "slug", "authors", "visible", "sticky", "publish_on")}, + ), + (_("Content"), {"fields": ("content", "og_image")}), + (_("Summary"), {"classes": ("collapse",), "fields": ("summary",)}), ) - prepopulated_fields = {'slug': ('title',)} - list_display = ('id', 'title', 'visible', 'sticky', 'publish_on') - list_display_links = ('id', 'title') - ordering = ('-publish_on',) + prepopulated_fields = {"slug": ("title",)} + list_display = ("id", "title", "visible", "sticky", "publish_on") + list_display_links = ("id", "title") + ordering = ("-publish_on",) form = BlogPostForm - date_hierarchy = 'publish_on' + date_hierarchy = "publish_on" def has_change_permission(self, request, obj=None): if obj is None: - return request.user.has_perm('judge.change_blogpost') + return request.user.has_perm("judge.change_blogpost") return obj.is_editable_by(request.user) def get_readonly_fields(self, request, obj=None): - if not request.user.has_perm('judge.change_post_visibility'): - return ['visible'] + if not request.user.has_perm("judge.change_post_visibility"): + return ["visible"] return [] def get_queryset(self, request): queryset = BlogPost.objects.all() - if not request.user.has_perm('judge.edit_all_post'): + if not request.user.has_perm("judge.edit_all_post"): queryset = queryset.filter(authors=request.profile) return queryset @@ -100,33 +119,43 @@ def get_queryset(self, request): class SolutionForm(ModelForm): def __init__(self, *args, **kwargs): super(SolutionForm, self).__init__(*args, **kwargs) - self.fields['authors'].widget.can_add_related = False + self.fields["authors"].widget.can_add_related = False 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): - fields = ('key', 'link', 'name', 'display', 'icon', 'text') - list_display = ('name', 'key') + fields = ("key", "link", "name", "display", "icon", "text") + list_display = ("name", "key") form = LicenseForm class UserListFilter(admin.SimpleListFilter): - title = _('user') - parameter_name = 'user' + title = _("user") + parameter_name = "user" def lookups(self, request, model_admin): - return User.objects.filter(is_staff=True).values_list('id', 'username') + return User.objects.filter(is_staff=True).values_list("id", "username") def queryset(self, request, queryset): if self.value(): @@ -135,10 +164,17 @@ def queryset(self, request, queryset): class LogEntryAdmin(admin.ModelAdmin): - 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') + 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") list_display_links = None actions = None @@ -151,18 +187,24 @@ def has_change_permission(self, request, obj=None): def has_delete_permission(self, request, obj=None): return False - @admin.display(description=_('object'), ordering='object_repr') + @admin.display(description=_("object"), ordering="object_repr") def object_link(self, obj): if obj.is_deletion(): link = obj.object_repr 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 def queryset(self, request): - return super().queryset(request).prefetch_related('content_type') + return super().queryset(request).prefetch_related("content_type") diff --git a/judge/admin/organization.py b/judge/admin/organization.py index ff5fab5f0b..af947928a5 100644 --- a/judge/admin/organization.py +++ b/judge/admin/organization.py @@ -13,100 +13,130 @@ class ClassForm(ModelForm): class Meta: widgets = { - 'admins': AdminHeavySelect2MultipleWidget(data_view='profile_select2'), + "admins": AdminHeavySelect2MultipleWidget(data_view="profile_select2"), } class ClassAdmin(VersionAdmin): - fields = ('name', 'slug', 'organization', 'is_active', 'access_code', 'admins', 'description', 'members') - list_display = ('name', 'organization', 'is_active') - prepopulated_fields = {'slug': ('name',)} + fields = ( + "name", + "slug", + "organization", + "is_active", + "access_code", + "admins", + "description", + "members", + ) + list_display = ("name", "organization", "is_active") + prepopulated_fields = {"slug": ("name",)} form = ClassForm def get_queryset(self, request): queryset = super().get_queryset(request) - if not request.user.has_perm('judge.edit_all_organization'): + 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'): + if not request.user.has_perm("judge.change_class"): return False - if request.user.has_perm('judge.edit_all_organization') or obj is None: + 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 = [] if obj: - fields.append('organization') + fields.append("organization") if not obj.organization.admins.filter(id=request.profile.id).exists(): - fields.append('admins') + fields.append("admins") return fields 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) + if "organization" in form.base_fields: + form.base_fields["organization"].queryset = Organization.objects.filter( + admins__id=request.profile.id + ) return form class OrganizationForm(ModelForm): class Meta: widgets = { - 'admins': AdminHeavySelect2MultipleWidget(data_view='profile_select2'), - 'about': AdminMartorWidget(attrs={'data-markdownfy-url': reverse_lazy('organization_preview')}), + "admins": AdminHeavySelect2MultipleWidget(data_view="profile_select2"), + "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') - list_display = ('name', 'short_name', 'is_open', 'slots', 'show_public') - prepopulated_fields = {'slug': ('name',)} + readonly_fields = ("creation_date",) + 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 actions_on_bottom = True form = OrganizationForm - @admin.display(description='') + @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 - if not request.user.has_perm('judge.organization_admin'): - return fields + ('admins', 'is_open', 'slots', 'class_required') + if not request.user.has_perm("judge.organization_admin"): + return fields + ("admins", "is_open", "slots", "class_required") return fields def get_queryset(self, request): queryset = Organization.objects.all() - if request.user.has_perm('judge.edit_all_organization'): + if request.user.has_perm("judge.edit_all_organization"): return queryset else: return queryset.filter(admins=request.profile.id) def has_change_permission(self, request, obj=None): - if not request.user.has_perm('judge.change_organization'): + if not request.user.has_perm("judge.change_organization"): return False - if request.user.has_perm('judge.edit_all_organization') or obj is None: + if request.user.has_perm("judge.edit_all_organization") or obj is None: return True return obj.admins.filter(id=request.profile.id).exists() class OrganizationRequestAdmin(admin.ModelAdmin): - list_display = ('username', 'organization', 'state', 'time') - readonly_fields = ('user', 'organization', 'request_class') + list_display = ("username", "organization", "state", "time") + readonly_fields = ("user", "organization", "request_class") - @admin.display(description=_('username'), ordering='user__user__username') + @admin.display(description=_("username"), ordering="user__user__username") def username(self, obj): return obj.user.user.username diff --git a/judge/admin/problem.py b/judge/admin/problem.py index c78c0981f6..f5687f1d5c 100644 --- a/judge/admin/problem.py +++ b/judge/admin/problem.py @@ -10,46 +10,74 @@ 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) - self.fields['authors'].widget.can_add_related = False - 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["authors"].widget.can_add_related = False + 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)"), + } + ) 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%'}), - 'types': AdminSelect2MultipleWidget, - 'group': AdminSelect2Widget, - 'description': AdminMartorWidget(attrs={'data-markdownfy-url': reverse_lazy('problem_preview')}), + "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")} + ), } class ProblemCreatorListFilter(admin.SimpleListFilter): - title = parameter_name = 'creator' + 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): @@ -60,23 +88,27 @@ def queryset(self, request, queryset): class LanguageLimitInlineForm(ModelForm): class Meta: - widgets = {'language': AdminSelect2Widget} + widgets = {"language": AdminSelect2Widget} class LanguageLimitInline(admin.TabularInline): model = LanguageLimit - fields = ('language', 'time_limit', 'memory_limit') + fields = ("language", "time_limit", "memory_limit") form = LanguageLimitInlineForm 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): model = ProblemClarification - fields = ('description',) + fields = ("description",) form = ProblemClarificationForm extra = 0 @@ -84,188 +116,264 @@ class ProblemClarificationInline(admin.StackedInline): class ProblemSolutionForm(ModelForm): def __init__(self, *args, **kwargs): super(ProblemSolutionForm, self).__init__(*args, **kwargs) - self.fields['authors'].widget.can_add_related = False + self.fields["authors"].widget.can_add_related = False 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")} + ), } class ProblemSolutionInline(admin.StackedInline): model = Solution - fields = ('is_public', 'publish_on', 'authors', 'content') + fields = ("is_public", "publish_on", "authors", "content") form = ProblemSolutionForm extra = 0 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): model = ProblemTranslation - fields = ('language', 'name', 'description') + fields = ("language", "name", "description") form = ProblemTranslationForm extra = 0 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')}), - (_('Taxonomy'), {'fields': ('types', 'group')}), - (_('Points'), {'fields': (('points', 'partial'), 'short_circuit')}), - (_('Limits'), {'fields': ('time_limit', 'memory_limit')}), - (_('Language'), {'fields': ('allowed_languages',)}), - (_('Justice'), {'fields': ('banned_users',)}), - (_('History'), {'fields': ('change_message',)}), + ( + 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")}), + (_("Language"), {"fields": ("allowed_languages",)}), + (_("Justice"), {"fields": ("banned_users",)}), + (_("History"), {"fields": ("change_message",)}), ) - 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] + 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, + ] list_max_show_all = 1000 actions_on_top = True actions_on_bottom = True - list_filter = ('is_public', ProblemCreatorListFilter) + list_filter = ("is_public", ProblemCreatorListFilter) form = ProblemForm - date_hierarchy = 'date' + date_hierarchy = "date" def get_actions(self, request): actions = super(ProblemAdmin, self).get_actions(request) - if request.user.has_perm('judge.change_public_visibility'): - func, name, desc = self.get_action('make_public') + if request.user.has_perm("judge.change_public_visibility"): + func, name, desc = self.get_action("make_public") actions[name] = (func, name, desc) - func, name, desc = self.get_action('make_private') + func, name, desc = self.get_action("make_private") actions[name] = (func, name, desc) - func, name, desc = self.get_action('update_publish_date') + func, name, desc = self.get_action("update_publish_date") actions[name] = (func, name, desc) return actions def get_readonly_fields(self, request, obj=None): fields = self.readonly_fields - if not request.user.has_perm('judge.change_public_visibility'): - fields += ('is_public',) - if not request.user.has_perm('judge.change_manually_managed'): - fields += ('is_manually_managed',) - if not request.user.has_perm('judge.problem_full_markup'): - fields += ('is_full_markup',) + if not request.user.has_perm("judge.change_public_visibility"): + fields += ("is_public",) + if not request.user.has_perm("judge.change_manually_managed"): + fields += ("is_manually_managed",) + if not request.user.has_perm("judge.problem_full_markup"): + fields += ("is_full_markup",) if obj and obj.is_full_markup: - fields += ('description',) + fields += ("description",) return fields - @admin.display(description=_('authors')) + @admin.display(description=_("authors")) def show_authors(self, obj): - return ', '.join(map(attrgetter('user.username'), obj.authors.all())) + return ", ".join(map(attrgetter("user.username"), obj.authors.all())) - @admin.display(description='') + @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')) + @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) - - @admin.display(description=_('Mark problems as public')) + 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): + 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) - - @admin.display(description=_('Mark problems as private')) + 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): + 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: - return request.user.has_perm('judge.edit_own_problem') + return request.user.has_perm("judge.edit_own_problem") return obj.is_editable_by(request.user) 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) + if db_field.name == "allowed_languages": + kwargs["widget"] = CheckboxSelectMultipleWithSelectAll() + return super(ProblemAdmin, self).formfield_for_manytomany( + db_field, request, **kwargs + ) def get_form(self, *args, **kwargs): form = super(ProblemAdmin, self).get_form(*args, **kwargs) - form.base_fields['authors'].queryset = Profile.objects.all() + form.base_fields["authors"].queryset = Profile.objects.all() return form def save_model(self, request, obj, form, change): # `organizations` will not appear in `cleaned_data` if user cannot edit it - if form.changed_data and 'organizations' in form.changed_data: - obj.is_organization_private = bool(form.cleaned_data['organizations']) + 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) + if form.cleaned_data.get("change_message"): + return form.cleaned_data["change_message"] + return super(ProblemAdmin, self).construct_change_message( + request, form, *args, **kwargs + ) class ProblemPointsVoteAdmin(admin.ModelAdmin): - list_display = ('points', 'voter', 'linked_problem', 'vote_time') - search_fields = ('voter__user__username', 'problem__code', 'problem__name') - readonly_fields = ('voter', 'problem', 'vote_time') + list_display = ("points", "voter", "linked_problem", "vote_time") + search_fields = ("voter__user__username", "problem__code", "problem__name") + 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 def has_change_permission(self, request, obj=None): if obj is None: - return request.user.has_perm('judge.edit_own_problem') + return request.user.has_perm("judge.edit_own_problem") return obj.problem.is_editable_by(request.user) def lookup_allowed(self, key, value): - return super().lookup_allowed(key, value) or key in ('problem__code',) + return super().lookup_allowed(key, value) or key in ("problem__code",) - @admin.display(description=_('problem'), ordering='problem__name') + @admin.display(description=_("problem"), ordering="problem__name") def linked_problem(self, obj): - link = reverse('problem_detail', args=[obj.problem.code]) + link = reverse("problem_detail", args=[obj.problem.code]) return format_html('{1}', link, obj.problem.name) diff --git a/judge/admin/profile.py b/judge/admin/profile.py index 5e267bf93e..7072d4add7 100644 --- a/judge/admin/profile.py +++ b/judge/admin/profile.py @@ -15,29 +15,41 @@ class ProfileForm(ModelForm): def __init__(self, *args, **kwargs): super(ProfileForm, self).__init__(*args, **kwargs) - if 'current_contest' in self.base_fields: + 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 = { - 'timezone': AdminSelect2Widget, - 'language': AdminSelect2Widget, - 'ace_theme': AdminSelect2Widget, - 'current_contest': AdminSelect2Widget, - 'about': AdminMartorWidget(attrs={'data-markdownfy-url': reverse_lazy('profile_preview')}), + "timezone": AdminSelect2Widget, + "language": AdminSelect2Widget, + "ace_theme": AdminSelect2Widget, + "current_contest": AdminSelect2Widget, + "about": AdminMartorWidget( + attrs={"data-markdownfy-url": reverse_lazy("profile_preview")} + ), } class TimezoneFilter(admin.SimpleListFilter): - title = _('timezone') - parameter_name = 'timezone' + title = _("timezone") + 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: @@ -47,7 +59,7 @@ def queryset(self, request, queryset): class WebAuthnInline(admin.TabularInline): model = WebAuthnCredential - readonly_fields = ('cred_id', 'public_key', 'counter') + readonly_fields = ("cred_id", "public_key", "counter") extra = 0 def has_add_permission(self, request, obj=None): @@ -55,16 +67,41 @@ 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') - readonly_fields = ('user',) - 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) - actions = ('recalculate_points',) + 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", + ) + ordering = ("user__username",) + search_fields = ("user__username", "ip", "user__email") + list_filter = ("language", TimezoneFilter) + actions = ("recalculate_points",) actions_on_top = True actions_on_bottom = True form = ProfileForm @@ -78,64 +115,74 @@ def has_add_permission(self, request, obj=None): # If an admin wants to go directly to the delete endpoint to delete a profile, more # power to them. def render_change_form(self, request, context, **kwargs): - context['show_delete'] = False + context["show_delete"] = False return super().render_change_form(request, context, **kwargs) def get_queryset(self, request): - return super(ProfileAdmin, self).get_queryset(request).select_related('user') + return super(ProfileAdmin, self).get_queryset(request).select_related("user") def get_fields(self, request, obj=None): - if request.user.has_perm('judge.totp'): + if request.user.has_perm("judge.totp"): fields = list(self.fields) - fields.insert(fields.index('is_totp_enabled') + 1, 'totp_key') - fields.insert(fields.index('totp_key') + 1, 'scratch_codes') + fields.insert(fields.index("is_totp_enabled") + 1, "totp_key") + fields.insert(fields.index("totp_key") + 1, "scratch_codes") return tuple(fields) else: return self.fields def get_readonly_fields(self, request, obj=None): fields = self.readonly_fields - if not request.user.has_perm('judge.totp'): - fields += ('is_totp_enabled',) + if not request.user.has_perm("judge.totp"): + fields += ("is_totp_enabled",) return fields - @admin.display(description='') + @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') + @admin.display(description=_("user"), ordering="user__username") def admin_user_admin(self, obj): return obj.username - @admin.display(description=_('email'), ordering='user__email') + @admin.display(description=_("email"), ordering="user__email") def email(self, obj): return obj.user.email - @admin.display(description=_('timezone'), ordering='timezone') + @admin.display(description=_("timezone"), ordering="timezone") def timezone_full(self, obj): return obj.timezone - @admin.display(description=_('date joined'), ordering='user__date_joined') + @admin.display(description=_("date joined"), ordering="user__date_joined") def date_joined(self, obj): return obj.user.date_joined - @admin.display(description=_('Recalculate scores')) + @admin.display(description=_("Recalculate scores")) def recalculate_points(self, request, queryset): count = 0 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: + 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, + form.base_fields["user_script"].widget = AceWidget( + mode="javascript", + theme=request.profile.resolved_ace_theme, ) return form diff --git a/judge/admin/runtime.py b/judge/admin/runtime.py index 3c756527c8..0d0e972fdb 100644 --- a/judge/admin/runtime.py +++ b/judge/admin/runtime.py @@ -15,20 +15,31 @@ class LanguageForm(ModelForm): class Meta: - widgets = {'description': AdminMartorWidget} + widgets = {"description": AdminMartorWidget} class LanguageAdmin(VersionAdmin): - fields = ('key', 'name', 'short_name', 'common_name', 'ace', 'pygments', 'info', 'extension', 'description', - 'template') - list_display = ('key', 'name', 'common_name', 'info') + fields = ( + "key", + "name", + "short_name", + "common_name", + "ace", + "pygments", + "info", + "extension", + "description", + "template", + ) + list_display = ("key", "name", "common_name", "info") form = LanguageForm 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, + form.base_fields["template"].widget = AceWidget( + 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): class Meta: - widgets = {'auth_key': GenerateKeyTextInput, 'description': AdminMartorWidget} + widgets = {"auth_key": GenerateKeyTextInput, "description": AdminMartorWidget} 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')}), - (_('Capabilities'), {'fields': ('runtimes',)}), + (None, {"fields": ("name", "auth_key", "is_blocked", "is_disabled")}), + (_("Description"), {"fields": ("description",)}), + ( + _("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'] + ordering = ["-online", "name"] formfield_overrides = { - TextField: {'widget': AdminMartorWidget}, + 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) judge.disconnect(force=force) - return HttpResponseRedirect(reverse('admin:judge_judge_changelist')) + return HttpResponseRedirect(reverse("admin:judge_judge_changelist")) def disconnect_view(self, request, id): return self.disconnect_judge(id) @@ -93,11 +137,13 @@ 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: - return self.readonly_fields + ('name',) + return self.readonly_fields + ("name",) return self.readonly_fields def has_delete_permission(self, request, obj=None): diff --git a/judge/admin/submission.py b/judge/admin/submission.py index aa3889d304..3f5197ad78 100644 --- a/judge/admin/submission.py +++ b/judge/admin/submission.py @@ -14,248 +14,373 @@ 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 + parameter_name = title = "status" + __lookups = ( + ("None", _("None")), + ("NotDone", _("Not done")), + ("EX", _("Exceptional")), + ) + Submission.STATUS __handles = set(map(itemgetter(0), Submission.STATUS)) def lookups(self, request, model_admin): return self.__lookups def queryset(self, request, queryset): - if self.value() == 'None': + if self.value() == "None": return queryset.filter(status=None) - elif self.value() == 'NotDone': - return queryset.exclude(status__in=['D', 'IE', 'CE', 'AB']) - elif self.value() == 'EX': - return queryset.exclude(status__in=['D', 'CE', 'G', 'AB']) + elif self.value() == "NotDone": + return queryset.exclude(status__in=["D", "IE", "CE", "AB"]) + elif self.value() == "EX": + return queryset.exclude(status__in=["D", "CE", "G", "AB"]) elif self.value() in self.__handles: return queryset.filter(status=self.value()) class SubmissionResultFilter(admin.SimpleListFilter): - parameter_name = title = 'result' - __lookups = (('None', _('None')), ('BAD', _('Unaccepted'))) + Submission.RESULT + parameter_name = title = "result" + __lookups = (("None", _("None")), ("BAD", _("Unaccepted"))) + Submission.RESULT __handles = set(map(itemgetter(0), Submission.RESULT)) def lookups(self, request, model_admin): return self.__lookups def queryset(self, request, queryset): - if self.value() == 'None': + if self.value() == "None": return queryset.filter(result=None) - elif self.value() == 'BAD': - return queryset.exclude(result='AC') + elif self.value() == "BAD": + return queryset.exclude(result="AC") elif self.value() in self.__handles: return queryset.filter(result=self.value()) class SubmissionTestCaseInline(admin.TabularInline): - fields = ('case', 'batch', 'status', 'time', 'memory', 'points', 'total') - readonly_fields = ('case', 'batch', 'total') + fields = ("case", "batch", "status", "time", "memory", "points", "total") + readonly_fields = ("case", "batch", "total") model = SubmissionTestCase can_delete = False max_num = 0 class ContestSubmissionInline(admin.StackedInline): - fields = ('problem', 'participation', 'points') + fields = ("problem", "participation", "points") 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): - submission = kwargs.pop('obj', None) + submission = kwargs.pop("obj", None) 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') + if db_field.name == "participation": + 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 + 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') + + elif db_field.name == "problem": + 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, + return pgettext("contest problem", "%(problem)s in %(contest)s") % { + "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 class SubmissionSourceInline(admin.StackedInline): - fields = ('source',) + fields = ("source",) model = SubmissionSource can_delete = False extra = 0 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, + kwargs.setdefault("widgets", {})["source"] = AceWidget( + 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') - 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_filter = ('language', SubmissionStatusFilter, SubmissionResultFilter) - search_fields = ('problem__code', 'problem__name', 'user__user__username') + 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", + ) + 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_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 - if not request.user.has_perm('judge.lock_submission'): - fields += ('locked_after',) + if not request.user.has_perm("judge.lock_submission"): + fields += ("locked_after",) 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'): + 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): return False def has_change_permission(self, request, obj=None): - if not request.user.has_perm('judge.edit_own_problem'): + if not request.user.has_perm("judge.edit_own_problem"): return False - if request.user.has_perm('judge.edit_all_problem') or obj is None: + if request.user.has_perm("judge.edit_all_problem") or obj is None: return True 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')) + @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) + 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, + ) return - if not request.user.has_perm('judge.edit_all_problem'): + 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')) + @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) + 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, + ) 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) + 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') + @admin.display(description=_("problem code"), ordering="problem__code") def problem_code(self, obj): return obj.problem.code - @admin.display(description=_('problem name'), ordering='problem__name') + @admin.display(description=_("problem name"), ordering="problem__name") def problem_name(self, obj): return obj.problem.name - @admin.display(description=_('user'), ordering='user__user__username') + @admin.display(description=_("user"), ordering="user__user__username") def user_column(self, obj): return obj.user.user.username - @admin.display(description=_('time'), ordering='time') + @admin.display(description=_("time"), ordering="time") def execution_time(self, obj): - return round(obj.time, 2) if obj.time is not None else 'None' + return round(obj.time, 2) if obj.time is not None else "None" - @admin.display(description=_('memory'), ordering='memory') + @admin.display(description=_("memory"), ordering="memory") def pretty_memory(self, obj): memory = obj.memory if memory is None: - return gettext('None') + return gettext("None") if memory < 1000: - return gettext('%d KB') % memory + return gettext("%d KB") % memory else: - return gettext('%.2f MB') % (memory / 1024) + return gettext("%.2f MB") % (memory / 1024) - @admin.display(description=_('language'), ordering='language__name') + @admin.display(description=_("language"), ordering="language__name") def language_column(self, obj): return obj.language.name - @admin.display(description='') + @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 [ - path('/judge/', self.judge_view, name='judge_submission_rejudge'), + path("/judge/", self.judge_view, name="judge_submission_rejudge"), ] + 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', '/')) + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) diff --git a/judge/admin/taxon.py b/judge/admin/taxon.py index aa245d7dc7..cc81053915 100644 --- a/judge/admin/taxon.py +++ b/judge/admin/taxon.py @@ -8,45 +8,51 @@ class ProblemGroupForm(ModelForm): problems = ModelMultipleChoiceField( - label=_('Included problems'), + label=_("Included problems"), queryset=Problem.objects.all(), required=False, - help_text=_('These problems are included in this group of problems.'), - widget=AdminHeavySelect2MultipleWidget(data_view='problem_select2')) + help_text=_("These problems are included in this group of problems."), + widget=AdminHeavySelect2MultipleWidget(data_view="problem_select2"), + ) class ProblemGroupAdmin(admin.ModelAdmin): - fields = ('name', 'full_name', 'problems') + fields = ("name", "full_name", "problems") form = ProblemGroupForm def save_model(self, request, obj, form, change): super(ProblemGroupAdmin, self).save_model(request, obj, form, change) - obj.problem_set.set(form.cleaned_data['problems']) + obj.problem_set.set(form.cleaned_data["problems"]) 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) class ProblemTypeForm(ModelForm): problems = ModelMultipleChoiceField( - label=_('Included problems'), + label=_("Included problems"), queryset=Problem.objects.all(), required=False, - help_text=_('These problems are included in this type of problems.'), - widget=AdminHeavySelect2MultipleWidget(data_view='problem_select2')) + help_text=_("These problems are included in this type of problems."), + widget=AdminHeavySelect2MultipleWidget(data_view="problem_select2"), + ) class ProblemTypeAdmin(admin.ModelAdmin): - fields = ('name', 'full_name', 'problems') + fields = ("name", "full_name", "problems") form = ProblemTypeForm def save_model(self, request, obj, form, change): super(ProblemTypeAdmin, self).save_model(request, obj, form, change) - obj.problem_set.set(form.cleaned_data['problems']) + obj.problem_set.set(form.cleaned_data["problems"]) 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..e500dd9f0c 100644 --- a/judge/admin/ticket.py +++ b/judge/admin/ticket.py @@ -4,35 +4,55 @@ 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")} + ), } class TicketMessageInline(StackedInline): model = TicketMessage form = TicketMessageForm - fields = ('user', 'body') + fields = ("user", "body") 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') - readonly_fields = ('time',) - list_display = ('title', 'user', 'time', 'linked_item') + fields = ( + "title", + "time", + "user", + "assignees", + "content_type", + "object_id", + "notes", + ) + readonly_fields = ("time",) + list_display = ("title", "user", "time", "linked_item") inlines = [TicketMessageInline] form = TicketForm - date_hierarchy = 'time' + date_hierarchy = "time" diff --git a/judge/apps.py b/judge/apps.py index 5df2bdea8c..212e01a45f 100644 --- a/judge/apps.py +++ b/judge/apps.py @@ -4,8 +4,8 @@ class JudgeAppConfig(AppConfig): - name = 'judge' - verbose_name = gettext_lazy('Online Judge') + name = "judge" + verbose_name = gettext_lazy("Online Judge") def ready(self): # WARNING: AS THIS IS NOT A FUNCTIONAL PROGRAMMING LANGUAGE, diff --git a/judge/bridge/base_handler.py b/judge/bridge/base_handler.py index e3ed9c574a..d44ed5029c 100644 --- a/judge/bridge/base_handler.py +++ b/judge/bridge/base_handler.py @@ -8,9 +8,9 @@ from judge.utils.unicode import utf8text -logger = logging.getLogger('judge.bridge') +logger = logging.getLogger("judge.bridge") -size_pack = struct.Struct('!I') +size_pack = struct.Struct("!I") assert size_pack.size == 4 MAX_ALLOWED_PACKET_SIZE = 8 * 1024 * 1024 @@ -20,7 +20,7 @@ def proxy_list(human_readable): globs = [] addrs = [] for item in human_readable: - if '*' in item or '-' in item: + if "*" in item or "-" in item: globs.append(IPGlob(item)) else: addrs.append(item) @@ -43,7 +43,7 @@ def __call__(cls, *args, **kwargs): try: handler.handle() except BaseException: - logger.exception('Error in base packet handling') + logger.exception("Error in base packet handling") raise finally: handler.on_disconnect() @@ -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 = [] @@ -86,7 +90,7 @@ def read_sized_packet(self, size, initial=None): data = self.request.recv(remainder) remainder -= len(data) buffer.append(data) - self._on_packet(b''.join(buffer)) + self._on_packet(b"".join(buffer)) def parse_proxy_protocol(self, line): words = line.split() @@ -94,18 +98,18 @@ def parse_proxy_protocol(self, line): if len(words) < 2: raise Disconnect() - if words[1] == b'TCP4': + if words[1] == b"TCP4": if len(words) != 6: raise Disconnect() self.client_address = (utf8text(words[2]), utf8text(words[4])) self.server_address = (utf8text(words[3]), utf8text(words[5])) - elif words[1] == b'TCP6': + elif words[1] == b"TCP6": self.client_address = (utf8text(words[2]), utf8text(words[4]), 0, 0) self.server_address = (utf8text(words[3]), utf8text(words[5]), 0, 0) - elif words[1] != b'UNKNOWN': + elif words[1] != b"UNKNOWN": raise Disconnect() - def read_size(self, buffer=b''): + def read_size(self, buffer=b""): while len(buffer) < size_pack.size: recv = self.request.recv(size_pack.size - len(buffer)) if not recv: @@ -113,9 +117,9 @@ def read_size(self, buffer=b''): buffer += recv return size_pack.unpack(buffer)[0] - def read_proxy_header(self, buffer=b''): + def read_proxy_header(self, buffer=b""): # Max line length for PROXY protocol is 107, and we received 4 already. - while b'\r\n' not in buffer: + while b"\r\n" not in buffer: if len(buffer) > 107: raise Disconnect() data = self.request.recv(107) @@ -125,7 +129,7 @@ def read_proxy_header(self, buffer=b''): return buffer def _on_packet(self, data): - decompressed = zlib.decompress(data).decode('utf-8') + decompressed = zlib.decompress(data).decode("utf-8") self._got_packet = True self.on_packet(decompressed) @@ -148,8 +152,10 @@ def handle(self): try: 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') + 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") 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,27 +180,38 @@ 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) + 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': + if e.__class__.__name__ == "cancel_wait_ex": return raise finally: self.on_cleanup() def send(self, data): - compressed = zlib.compress(data.encode('utf-8')) + compressed = zlib.compress(data.encode("utf-8")) self.request.sendall(size_pack.pack(len(compressed)) + compressed) def close(self): diff --git a/judge/bridge/daemon.py b/judge/bridge/daemon.py index ce8a702dbd..b9988ce196 100644 --- a/judge/bridge/daemon.py +++ b/judge/bridge/daemon.py @@ -11,7 +11,7 @@ from judge.bridge.server import Server from judge.models import Judge, Submission -logger = logging.getLogger('judge.bridge') +logger = logging.getLogger("judge.bridge") def reset_judges(): @@ -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() @@ -33,7 +38,7 @@ def judge_daemon(): stop = threading.Event() def signal_handler(signum, _): - logger.info('Exiting due to %s', signal.Signals(signum).name) + logger.info("Exiting due to %s", signal.Signals(signum).name) stop.set() signal.signal(signal.SIGINT, signal_handler) diff --git a/judge/bridge/django_handler.py b/judge/bridge/django_handler.py index cdde06e0dd..b4eb2d4475 100644 --- a/judge/bridge/django_handler.py +++ b/judge/bridge/django_handler.py @@ -6,8 +6,8 @@ from judge.bridge.base_handler import Disconnect, ZlibPacketHandler -logger = logging.getLogger('judge.bridge') -size_pack = struct.Struct('!I') +logger = logging.getLogger("judge.bridge") +size_pack = struct.Struct("!I") class DjangoHandler(ZlibPacketHandler): @@ -15,53 +15,58 @@ def __init__(self, request, client_address, server, judges): super().__init__(request, client_address, server) self.handlers = { - 'submission-request': self.on_submission, - 'terminate-submission': self.on_termination, - 'disconnect-judge': self.on_disconnect_request, - 'disable-judge': self.on_disable_judge, + "submission-request": self.on_submission, + "terminate-submission": self.on_termination, + "disconnect-judge": self.on_disconnect_request, + "disable-judge": self.on_disable_judge, } self.judges = judges def send(self, data): - super().send(json.dumps(data, separators=(',', ':'))) + super().send(json.dumps(data, separators=(",", ":"))) 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'} + logger.exception("Error in packet handling (Django-facing)") + result = {"name": "bad-request"} self.send(result) raise Disconnect() def on_submission(self, data): - id = data['submission-id'] - problem = data['problem-id'] - language = data['language'] - source = data['source'] - judge_id = data['judge-id'] - priority = data['priority'] + id = data["submission-id"] + problem = data["problem-id"] + language = data["language"] + source = data["source"] + judge_id = data["judge-id"] + priority = data["priority"] if not self.judges.check_priority(priority): - return {'name': 'bad-request'} + return {"name": "bad-request"} self.judges.judge(id, problem, language, source, judge_id, priority) - return {'name': 'submission-received', 'submission-id': id} + 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'] - force = data['force'] + judge_id = data["judge-id"] + force = data["force"] self.judges.disconnect(judge_id, force=force) def on_disable_judge(self, data): - judge_id = data['judge-id'] - is_disabled = data['is-disabled'] + judge_id = data["judge-id"] + is_disabled = data["is-disabled"] self.judges.update_disable_judge(judge_id, is_disabled) def on_malformed(self, packet): - logger.error('Malformed packet: %s', packet) + logger.error("Malformed packet: %s", packet) def on_cleanup(self): db.connection.close() diff --git a/judge/bridge/echo_test_client.py b/judge/bridge/echo_test_client.py index 8fec692aaf..801d34e4d4 100644 --- a/judge/bridge/echo_test_client.py +++ b/judge/bridge/echo_test_client.py @@ -4,7 +4,7 @@ import time import zlib -size_pack = struct.Struct('!I') +size_pack = struct.Struct("!I") def open_connection(): @@ -13,69 +13,70 @@ def open_connection(): def zlibify(data): - data = zlib.compress(data.encode('utf-8')) + data = zlib.compress(data.encode("utf-8")) return size_pack.pack(len(data)) + data def dezlibify(data, skip_head=True): if skip_head: - data = data[size_pack.size:] - return zlib.decompress(data).decode('utf-8') + 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) + parser.add_argument("-l", "--host", default="localhost") + parser.add_argument("-p", "--port", default=9999, type=int) args = parser.parse_args() host, port = args.host, args.port - print('Opening idle connection:', end=' ') + print("Opening idle connection:", end=" ") s1 = open_connection() - print('Success') - print('Opening hello world connection:', end=' ') + print("Success") + print("Opening hello world connection:", end=" ") s2 = open_connection() - print('Success') - print('Sending Hello, World!', end=' ') - s2.sendall(zlibify('Hello, World!')) - print('Success') - print('Testing blank connection:', end=' ') + print("Success") + print("Sending Hello, World!", end=" ") + s2.sendall(zlibify("Hello, World!")) + print("Success") + print("Testing blank connection:", end=" ") s3 = open_connection() s3.close() - print('Success') + print("Success") result = dezlibify(s2.recv(1024)) - assert result == 'Hello, World!' + assert result == "Hello, World!" print(result) s2.close() - print('Large random data test:', end=' ') + print("Large random data test:", end=" ") s4 = open_connection() - data = os.urandom(1000000).decode('iso-8859-1') - print('Generated', end=' ') + data = os.urandom(1000000).decode("iso-8859-1") + print("Generated", end=" ") s4.sendall(zlibify(data)) - print('Sent', end=' ') - result = b'' + print("Sent", end=" ") + 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=' ') + print("Received", end=" ") assert dezlibify(result, False) == data - print('Success') + print("Success") s4.close() - print('Test malformed connection:', end=' ') + print("Test malformed connection:", end=" ") s5 = open_connection() - s5.sendall(data[:100000].encode('utf-8')) + s5.sendall(data[:100000].encode("utf-8")) s5.close() - print('Success') - print('Waiting for timeout to close idle connection:', end=' ') + print("Success") + print("Waiting for timeout to close idle connection:", end=" ") time.sleep(6) - print('Done') + print("Done") s1.close() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/judge/bridge/echo_test_server.py b/judge/bridge/echo_test_server.py index 59e21fa223..2d84e85240 100644 --- a/judge/bridge/echo_test_server.py +++ b/judge/bridge/echo_test_server.py @@ -3,19 +3,22 @@ class EchoPacketHandler(ZlibPacketHandler): def on_connect(self): - print('New client:', self.client_address) + print("New client:", self.client_address) self.timeout = 5 def on_timeout(self): - print('Inactive client:', self.client_address) + print("Inactive client:", self.client_address) 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): - print('Closed client:', self.client_address) + print("Closed client:", self.client_address) def main(): @@ -23,9 +26,9 @@ def main(): from judge.bridge.server import Server parser = argparse.ArgumentParser() - parser.add_argument('-l', '--host', action='append') - parser.add_argument('-p', '--port', type=int, action='append') - parser.add_argument('-P', '--proxy', action='append') + parser.add_argument("-l", "--host", action="append") + parser.add_argument("-p", "--port", type=int, action="append") + parser.add_argument("-P", "--proxy", action="append") args = parser.parse_args() class Handler(EchoPacketHandler): @@ -35,5 +38,5 @@ class Handler(EchoPacketHandler): server.serve_forever() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/judge/bridge/judge_handler.py b/judge/bridge/judge_handler.py index 073de27262..d7fd377049 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 - -logger = logging.getLogger('judge.bridge') -json_log = logging.getLogger('judge.json.bridge') +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(): @@ -35,19 +46,19 @@ def __init__(self, request, client_address, server, judges): self.judges = judges self.handlers = { - 'grading-begin': self.on_grading_begin, - 'grading-end': self.on_grading_end, - 'compile-error': self.on_compile_error, - 'compile-message': self.on_compile_message, - 'batch-begin': self.on_batch_begin, - 'batch-end': self.on_batch_end, - 'test-case-status': self.on_test_case, - 'internal-error': self.on_internal_error, - 'submission-terminated': self.on_submission_terminated, - 'submission-acknowledged': self.on_submission_acknowledged, - 'ping-response': self.on_ping_response, - 'supported-problems': self.on_supported_problems, - 'handshake': self.on_handshake, + "grading-begin": self.on_grading_begin, + "grading-end": self.on_grading_end, + "compile-error": self.on_compile_error, + "compile-message": self.on_compile_message, + "batch-begin": self.on_batch_begin, + "batch-end": self.on_batch_end, + "test-case-status": self.on_test_case, + "internal-error": self.on_internal_error, + "submission-terminated": self.on_submission_terminated, + "submission-acknowledged": self.on_submission_acknowledged, + "ping-response": self.on_ping_response, + "supported-problems": self.on_supported_problems, + "handshake": self.on_handshake, } self._working = False self._no_response_job = None @@ -75,22 +86,38 @@ def __init__(self, request, client_address, server, judges): def on_connect(self): self.timeout = 15 - logger.info('Judge connected from: %s', self.client_address) - json_log.info(self._make_json_log(action='connect')) + logger.info("Judge connected from: %s", self.client_address) + json_log.info(self._make_json_log(action="connect")) 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: @@ -99,12 +126,20 @@ def _authenticate(self, id, key): return False 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')) + logger.warning("Judge authentication failure: %s", self.client_address) + 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,40 +189,49 @@ 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): - super().send(json.dumps(data, separators=(',', ':'))) + super().send(json.dumps(data, separators=(",", ":"))) def on_handshake(self, packet): - if 'id' not in packet or 'key' not in packet: - logger.warning('Malformed handshake: %s', self.client_address) + if "id" not in packet or "key" not in packet: + logger.warning("Malformed handshake: %s", self.client_address) self.close() return - if not self._authenticate(packet['id'], packet['key']): + if not self._authenticate(packet["id"], packet["key"]): self.close() return self.timeout = 60 - self._problems = packet['problems'] + self._problems = packet["problems"] self.problems = dict(self._problems) - self.executors = packet['executors'] - self.name = packet['id'] + self.executors = packet["executors"] + self.name = packet["id"] - self.send({'name': 'handshake-success'}) - logger.info('Judge authenticated: %s (%s)', self.client_address, packet['id']) + self.send({"name": "handshake-success"}) + logger.info("Judge authenticated: %s (%s)", self.client_address, packet["id"]) self.judges.register(self) threading.Thread(target=self._ping_thread).start() 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', - )) + logger.error("Submission vanished: %s", submission) + 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 @@ -220,134 +313,179 @@ def disconnect(self, force=False): # Yank the power out. self.close() else: - self.send({'name': 'disconnect'}) + self.send({"name": "disconnect"}) 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): if self.name: - logger.warning('Judge seems dead: %s: %s', self.name, self._working) + logger.warning("Judge seems dead: %s: %s", self.name, self._working) def on_submission_processing(self, packet): _ensure_connection() - id = packet['submission-id'] - if Submission.objects.filter(id=id).update(status='P', judged_on=self.judge): - event.post('sub_%s' % Submission.get_id_secret(id), {'type': 'processing'}) - self._post_update_submission(id, 'processing') - json_log.info(self._make_json_log(packet, action='processing')) + id = packet["submission-id"] + if Submission.objects.filter(id=id).update(status="P", judged_on=self.judge): + event.post("sub_%s" % Submission.get_id_secret(id), {"type": "processing"}) + self._post_update_submission(id, "processing") + 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')) + logger.warning("Unknown submission: %s", id) + 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)) + 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) + ) self.close() - logger.info('Submission acknowledged: %d', self._working) + logger.info("Submission acknowledged: %d", self._working) if self._no_response_job: self._no_response_job.cancel() self._no_response_job = None self.on_submission_processing(packet) def abort(self): - self.send({'name': 'terminate-submission'}) + self.send({"name": "terminate-submission"}) def get_current_submission(self): return self._working or None def ping(self): - self.send({'name': 'ping', 'when': time.time()}) + self.send({"name": "ping", "when": time.time()}) def on_packet(self, data): try: try: data = json.loads(data) - if 'name' not in data: + if "name" not in data: raise ValueError except ValueError: self.on_malformed(data) else: - handler = self.handlers.get(data['name'], self.on_malformed) + handler = self.handlers.get(data["name"], self.on_malformed) handler(data) except Exception: - logger.exception('Error in packet handling (Judge-side): %s', self.name) + logger.exception("Error in packet handling (Judge-side): %s", self.name) self._packet_exception() # You can't crash here because you aren't so sure about the judges # 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): - logger.warning('Unknown submission: %s', id) + logger.warning("Unknown submission: %s", id) def on_supported_problems(self, packet): - logger.info('%s: Updated problem list', self.name) - self._problems = packet['problems'] + logger.info("%s: Updated problem list", self.name) + self._problems = packet["problems"] self.problems = dict(self._problems) 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']) + 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'}) - self._post_update_submission(packet['submission-id'], 'grading-begin') - json_log.info(self._make_json_log(packet, action='grading-begin')) + 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"}, + ) + 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')) + logger.warning("Unknown submission: %s", packet["submission-id"]) + 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']) + logger.info("%s: Grading has ended on: %s", self.name, packet["submission-id"]) self._free_self(packet) self.batch_id = None try: - submission = Submission.objects.get(id=packet['submission-id']) + 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')) + logger.warning("Unknown submission: %s", packet["submission-id"]) + json_log.error( + self._make_json_log( + packet, action="grading-end", info="unknown submission" + ) + ) return time = 0 @@ -355,7 +493,7 @@ def on_grading_end(self, packet): points = 0.0 total = 0 status = 0 - status_codes = ['SC', 'AC', 'WA', 'MLE', 'TLE', 'IR', 'RTE', 'OLE'] + status_codes = ["SC", "AC", "WA", "MLE", "TLE", "IR", "RTE", "OLE"] batches = {} # batch number: (points, total) for case in SubmissionTestCase.objects.filter(submission=submission): @@ -388,19 +526,29 @@ def on_grading_end(self, packet): if not problem.partial and sub_points != problem.points: sub_points = 0 - submission.status = 'D' + submission.status = "D" submission.time = time submission.memory = memory submission.points = sub_points 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,144 +560,258 @@ 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, - }) - if hasattr(submission, 'contest'): + 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) + 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')) + 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", + ) + ) 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'])) + 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"], + ) + ) def on_internal_error(self, packet): try: - raise ValueError('\n\n' + packet['message']) + 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'}) - 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')) + 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"} + ) + 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", + ) + ) 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')) + 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", + ) + ) def on_submission_terminated(self, packet): - logger.info('%s: Submission aborted: %s', self.name, packet['submission-id']) + 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'}) - self._post_update_submission(packet['submission-id'], 'aborted', done=True) - json_log.info(self._make_json_log(packet, action='aborted', finish=True, result='AB')) + 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") + ) 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')) + 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", + ) + ) def on_batch_begin(self, packet): - logger.info('%s: Batch began on: %s', self.name, packet['submission-id']) + logger.info("%s: Batch began on: %s", self.name, packet["submission-id"]) self.in_batch = True if self.batch_id is None: self.batch_id = 0 - self._submission_is_batch(packet['submission-id']) + 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)) - - 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']) + 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) + ) - id = packet['submission-id'] - updates = packet['cases'] - max_position = max(map(itemgetter('position'), updates)) + 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"], + ) - 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')) + 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 + ): + logger.warning("Unknown submission: %s", id) + json_log.error( + self._make_json_log( + packet, action="test-case", info="unknown submission" + ) + ) return bulk_test_case_updates = [] for result in updates: - test_case = SubmissionTestCase(submission_id=id, case=result['position']) - status = result['status'] + test_case = SubmissionTestCase(submission_id=id, case=result["position"]) + status = result["status"] if status & 4: - test_case.status = 'TLE' + test_case.status = "TLE" elif status & 8: - test_case.status = 'MLE' + test_case.status = "MLE" elif status & 64: - test_case.status = 'OLE' + test_case.status = "OLE" elif status & 2: - test_case.status = 'RTE' + test_case.status = "RTE" elif status & 16: - test_case.status = 'IR' + test_case.status = "IR" elif status & 1: - test_case.status = 'WA' + test_case.status = "WA" elif status & 32: - test_case.status = 'SC' + test_case.status = "SC" else: - test_case.status = 'AC' - test_case.time = result['time'] - test_case.memory = result['memory'] - test_case.points = result['points'] - test_case.total = result['total-points'] + test_case.status = "AC" + test_case.time = result["time"] + test_case.memory = result["memory"] + test_case.points = result["points"] + test_case.total = result["total-points"] test_case.batch = self.batch_id if self.in_batch else None - test_case.feedback = (result.get('feedback') or '')[:max_feedback] - test_case.extended_feedback = result.get('extended-feedback') or '' - test_case.output = result['output'] + test_case.feedback = (result.get("feedback") or "")[:max_feedback] + test_case.extended_feedback = result.get("extended-feedback") or "" + 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,29 +828,34 @@ 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, - }) - self._post_update_submission(id, state='test-case') + 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')) + logger.error("%s: Malformed packet: %s", self.name, packet) + json_log.exception( + self._make_json_log(sub=self._working, info="malformed json packet") + ) def on_ping_response(self, packet): end = time.time() - self._ping_average.append(end - packet['when']) - self._time_delta.append((end + packet['when']) / 2 - packet['time']) + self._ping_average.append(end - packet["when"]) + self._time_delta.append((end + packet["when"]) / 2 - packet["time"]) self.latency = sum(self._ping_average) / len(self._ping_average) self.time_delta = sum(self._time_delta) / len(self._time_delta) - self.load = packet['load'] + self.load = packet["load"] self._update_ping() def _free_self(self, packet): - self.judges.on_judge_free(self, packet['submission-id']) + self.judges.on_judge_free(self, packet["submission-id"]) def _ping_thread(self): try: @@ -597,19 +864,19 @@ def _ping_thread(self): if self._stop_ping.wait(10): break except Exception: - logger.exception('Ping error in %s', self.name) + logger.exception("Ping error in %s", self.name) self.close() raise def _make_json_log(self, packet=None, sub=None, **kwargs): data = { - 'judge': self.name, - 'address': self.judge_address, + "judge": self.name, + "address": self.judge_address, } if sub is None and packet is not None: - sub = packet.get('submission-id') + sub = packet.get("submission-id") if sub is not None: - data['submission'] = sub + data["submission"] = sub data.update(kwargs) return json.dumps(data) @@ -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'], - }) + 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"], + }, + ) def on_cleanup(self): db.connection.close() diff --git a/judge/bridge/judge_list.py b/judge/bridge/judge_list.py index b65d0ddb82..349308bbfe 100644 --- a/judge/bridge/judge_list.py +++ b/judge/bridge/judge_list.py @@ -10,9 +10,9 @@ except ImportError: from pyllist import dllist -logger = logging.getLogger('judge.bridge') +logger = logging.getLogger("judge.bridge") -PriorityMarker = namedtuple('PriorityMarker', 'priority') +PriorityMarker = namedtuple("PriorityMarker", "priority") class JudgeList(object): @@ -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 @@ -99,14 +116,14 @@ def __iter__(self): return iter(self.judges) def on_judge_free(self, judge, submission): - logger.info('Judge available after grading %d: %s', submission, judge.name) + logger.info("Judge available after grading %d: %s", submission, judge.name) with self.lock: del self.submission_map[submission] judge._working = False self._handle_free_judge(judge) def abort(self, submission): - logger.info('Abort request: %d', submission) + logger.info("Abort request: %d", submission) with self.lock: try: self.submission_map[submission].abort() @@ -131,25 +148,47 @@ 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)) + 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: # Schedule the submission on the judge reporting least load. judge = min(available, key=lambda judge: (judge.load, random())) - logger.info('Dispatched submission %d to: %s', id, judge.name) + logger.info("Dispatched submission %d to: %s", id, judge.name) self.submission_map[id] = 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.discard(judge) return self.judge(id, problem, language, source, judge_id, priority) else: @@ -157,4 +196,4 @@ def judge(self, id, problem, language, source, judge_id, priority): (id, problem, language, source, judge_id), self.priority[priority], ) - logger.info('Queued submission: %d', id) + logger.info("Queued submission: %d", id) 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/caching.py b/judge/caching.py index 7f0a687bd2..99bbf81465 100644 --- a/judge/caching.py +++ b/judge/caching.py @@ -2,9 +2,9 @@ def finished_submission(sub): - keys = ['user_complete:%d' % sub.user_id, 'user_attempted:%s' % sub.user_id] - if hasattr(sub, 'contest'): + keys = ["user_complete:%d" % sub.user_id, "user_attempted:%s" % sub.user_id] + if hasattr(sub, "contest"): participation = sub.contest.participation - keys += ['contest_complete:%d' % participation.id] - keys += ['contest_attempted:%d' % participation.id] + keys += ["contest_complete:%d" % participation.id] + keys += ["contest_attempted:%d" % participation.id] cache.delete_many(keys) diff --git a/judge/comments.py b/judge/comments.py index 21481394ff..87064de0ea 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 @@ -26,27 +31,34 @@ class CommentForm(ModelForm): class Meta: model = Comment - fields = ['body', 'parent'] + fields = ["body", "parent"] widgets = { - 'parent': forms.HiddenInput(), + "parent": forms.HiddenInput(), } 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 super(CommentForm, self).__init__(*args, **kwargs) - self.fields['body'].widget.attrs.update({'placeholder': _('Comment body')}) + self.fields["body"].widget.attrs.update({"placeholder": _("Comment body")}) def clean(self): if self.request is not None and self.request.user.is_authenticated: profile = self.request.profile if profile.mute: - raise ValidationError(_('Your part is silent, little toad.')) + 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): @@ -70,7 +83,7 @@ def post(self, request, *args, **kwargs): if self.is_comment_locked(): return HttpResponseForbidden() - parent = request.POST.get('parent') + parent = request.POST.get("parent") if parent: if len(parent) > 10: return HttpResponseBadRequest() @@ -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,9 +107,11 @@ 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')) + revisions.set_comment(_("Posted comment")) comment.save() return HttpResponseRedirect(request.path) @@ -102,26 +120,34 @@ 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) queryset = Comment.objects.filter(hidden=False, page=self.get_comment_page()) - context['has_comments'] = queryset.exists() - context['comment_lock'] = self.is_comment_locked() - queryset = queryset.select_related('author__user').defer('author__about') + context["has_comments"] = queryset.exists() + context["comment_lock"] = self.is_comment_locked() + queryset = queryset.select_related("author__user").defer("author__about") if self.request.user.is_authenticated: profile = self.request.profile queryset = queryset.annotate( - 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['comment_list'] = queryset - context['vote_hide_threshold'] = settings.DMOJ_COMMENT_VOTE_HIDE_THRESHOLD - context['reply_cutoff'] = timezone.now() - settings.DMOJ_COMMENT_REPLY_TIMEFRAME + 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["comment_list"] = queryset + context["vote_hide_threshold"] = settings.DMOJ_COMMENT_VOTE_HIDE_THRESHOLD + context["reply_cutoff"] = timezone.now() - settings.DMOJ_COMMENT_REPLY_TIMEFRAME return context diff --git a/judge/contest_format/atcoder.py b/judge/contest_format/atcoder.py index 9585eee1bf..36282413a8 100644 --- a/judge/contest_format/atcoder.py +++ b/judge/contest_format/atcoder.py @@ -14,11 +14,11 @@ from judge.utils.timedelta import nice_repr -@register_contest_format('atcoder') +@register_contest_format("atcoder") class AtCoderContestFormat(DefaultContestFormat): - name = gettext_lazy('AtCoder') - config_defaults = {'penalty': 5} - config_validators = {'penalty': lambda x: x >= 0} + name = gettext_lazy("AtCoder") + config_defaults = {"penalty": 5} + config_validators = {"penalty": lambda x: x >= 0} """ penalty: Number of penalty minutes each incorrect submission adds. Defaults to 5. """ @@ -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,21 +67,27 @@ 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) dt = (time - participation.start).total_seconds() # Compute penalty - if self.config['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 + penalty += prev * self.config["penalty"] * 60 else: # We should always display the penalty, even if the user has a score of 0 prev = subs.count() @@ -86,7 +97,7 @@ def update_participation(self, participation): if score: cumtime = max(cumtime, dt) - format_data[str(prob)] = {'time': dt, 'points': score, 'penalty': prev} + format_data[str(prob)] = {"time": dt, "points": score, "penalty": prev} points += score participation.cumtime = cumtime + penalty @@ -98,30 +109,51 @@ 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]), - points=floatformat(format_data['points']), + 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'), + time=nice_repr(timedelta(seconds=format_data["time"]), "noday"), ) else: - return mark_safe('') + return mark_safe("") def get_short_form_display(self): - yield _('The maximum score submission for each problem will be used.') + yield _("The maximum score submission for each problem will be used.") - penalty = self.config['penalty'] + penalty = self.config["penalty"] if penalty: yield ngettext( - 'Each submission before the first maximum score submission will incur a **penalty of %d minute**.', - 'Each submission before the first maximum score submission will incur a **penalty of %d minutes**.', + "Each submission before the first maximum score submission will incur a **penalty of %d minute**.", + "Each submission before the first maximum score submission will incur a **penalty of %d minutes**.", penalty, ) % penalty - yield _('Ties will be broken by the last score altering submission time.') + yield _("Ties will be broken by the last score altering submission time.") diff --git a/judge/contest_format/base.py b/judge/contest_format/base.py index 64257d448b..c0e9cd10e7 100644 --- a/judge/contest_format/base.py +++ b/judge/contest_format/base.py @@ -103,7 +103,7 @@ def get_short_form_display(self): @classmethod def best_solution_state(cls, points, total): if not points: - return 'failed-score' + return "failed-score" if points == total: - return 'full-score' - return 'partial-score' + return "full-score" + return "partial-score" diff --git a/judge/contest_format/default.py b/judge/contest_format/default.py index 1cf3e9861c..29fb56b0f9 100644 --- a/judge/contest_format/default.py +++ b/judge/contest_format/default.py @@ -13,14 +13,16 @@ from judge.utils.timedelta import nice_repr -@register_contest_format('default') +@register_contest_format("default") class DefaultContestFormat(BaseContestFormat): - name = gettext_lazy('Default') + name = gettext_lazy("Default") @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) @@ -30,14 +32,18 @@ def update_participation(self, participation): points = 0 format_data = {} - for result in participation.submissions.values('problem_id').annotate( - time=Max('submission__date'), points=Max('points'), + for result in participation.submissions.values("problem_id").annotate( + time=Max("submission__date"), + points=Max("points"), ): - dt = (result['time'] - participation.start).total_seconds() - if result['points']: + dt = (result["time"] - participation.start).total_seconds() + if result["points"]: cumtime += dt - format_data[str(result['problem_id'])] = {'time': dt, 'points': result['points']} - points += result['points'] + format_data[str(result["problem_id"])] = { + "time": dt, + "points": result["points"], + } + points += result["points"] participation.cumtime = max(cumtime, 0) participation.score = round(points, self.contest.points_precision) @@ -50,31 +56,53 @@ 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]), - points=floatformat(format_data['points']), - time=nice_repr(timedelta(seconds=format_data['time']), 'noday'), + 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"), ) else: - return mark_safe('') + return mark_safe("") 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'), + 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 _("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." + ) diff --git a/judge/contest_format/ecoo.py b/judge/contest_format/ecoo.py index 93c245c9c1..8b3e16de05 100644 --- a/judge/contest_format/ecoo.py +++ b/judge/contest_format/ecoo.py @@ -13,11 +13,15 @@ from judge.utils.timedelta import nice_repr -@register_contest_format('ecoo') +@register_contest_format("ecoo") 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} + 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, + } """ 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,24 +59,25 @@ 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')) - .order_by('-submission__date') - .values('submission__date')[:1], + submissions.filter(problem_id=OuterRef("problem_id")) + .order_by("-submission__date") + .values("submission__date")[:1], ), ) - .annotate(points=Max('points')) - .values_list('problem_id', 'problem__points', 'points', 'submission__date') + .annotate(points=Max("points")) + .values_list("problem_id", "problem__points", "points", "submission__date") ) for problem_id, problem_points, points, date in queryset: @@ -80,17 +89,25 @@ def update_participation(self, participation): if points > 0: # First AC bonus if sub_cnt == 1 and points == problem_points: - bonus += self.config['first_ac_bonus'] + 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} + 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, + } for data in format_data.values(): - if self.config['cumtime']: - cumtime += data['time'] - score += data['points'] + data['bonus'] + if self.config["cumtime"]: + cumtime += data["time"] + score += data["points"] + data["bonus"] participation.cumtime = cumtime participation.score = round(score, self.contest.points_precision) @@ -101,49 +118,77 @@ 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]), - points=floatformat(format_data['points']), + 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'), + time=nice_repr(timedelta(seconds=format_data["time"]), "noday"), ) else: - return mark_safe('') + return mark_safe("") 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'] + first_ac_bonus = self.config["first_ac_bonus"] if first_ac_bonus: yield _( - 'There is a **%d bonus** for fully solving on your first non-CE submission.', + "There is a **%d bonus** for fully solving on your first non-CE submission.", ) % first_ac_bonus - time_bonus = self.config['time_bonus'] + time_bonus = self.config["time_bonus"] if time_bonus: yield ngettext( - 'For every **%d minute** you submit before the end of your window, there will be a **1** point bonus.', - 'For every **%d minutes** you submit before the end of your window, there will be a **1** point bonus.', + "For every **%d minute** you submit before the end of your window, there will be a **1** point bonus.", + "For every **%d minutes** you submit before the end of your window, there will be a **1** point bonus.", time_bonus, ) % time_bonus - if self.config['cumtime']: - yield _('Ties will be broken by the sum of the last submission time on **all** problems.') + if self.config["cumtime"]: + 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.') + yield _("Ties by score will **not** be broken.") diff --git a/judge/contest_format/icpc.py b/judge/contest_format/icpc.py index 13dfe2ed7c..698658b53d 100644 --- a/judge/contest_format/icpc.py +++ b/judge/contest_format/icpc.py @@ -14,11 +14,11 @@ from judge.utils.timedelta import nice_repr -@register_contest_format('icpc') +@register_contest_format("icpc") class ICPCContestFormat(DefaultContestFormat): - name = gettext_lazy('ICPC') - config_defaults = {'penalty': 20} - config_validators = {'penalty': lambda x: x >= 0} + name = gettext_lazy("ICPC") + config_defaults = {"penalty": 20} + config_validators = {"penalty": lambda x: x >= 0} """ penalty: Number of penalty minutes each incorrect submission adds. Defaults to 20. """ @@ -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,21 +68,27 @@ 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) dt = (time - participation.start).total_seconds() # Compute penalty - if self.config['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 + penalty += prev * self.config["penalty"] * 60 else: # We should always display the penalty, even if the user has a score of 0 prev = subs.count() @@ -88,7 +99,7 @@ def update_participation(self, participation): cumtime += dt last = max(last, dt) - format_data[str(prob)] = {'time': dt, 'points': points, 'penalty': prev} + format_data[str(prob)] = {"time": dt, "points": points, "penalty": prev} score += points participation.cumtime = cumtime + penalty @@ -100,39 +111,62 @@ 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]), - points=floatformat(format_data['points']), + 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'), + time=nice_repr(timedelta(seconds=format_data["time"]), "noday"), ) else: - return mark_safe('') + return mark_safe("") def get_label_for_problem(self, index): index += 1 - ret = '' + ret = "" while index > 0: ret += chr((index - 1) % 26 + 65) index = (index - 1) // 26 return ret[::-1] def get_short_form_display(self): - yield _('The maximum score submission for each problem will be used.') + yield _("The maximum score submission for each problem will be used.") - penalty = self.config['penalty'] + penalty = self.config["penalty"] if penalty: yield ngettext( - 'Each submission before the first maximum score submission will incur a **penalty of %d minute**.', - 'Each submission before the first maximum score submission will incur a **penalty of %d minutes**.', + "Each submission before the first maximum score submission will incur a **penalty of %d minute**.", + "Each submission before the first maximum score submission will incur a **penalty of %d minutes**.", 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..55c632eb79 100644 --- a/judge/contest_format/ioi.py +++ b/judge/contest_format/ioi.py @@ -6,10 +6,10 @@ from judge.timezone import from_database_time -@register_contest_format('ioi16') +@register_contest_format("ioi16") class IOIContestFormat(LegacyIOIContestFormat): - name = gettext_lazy('IOI') - config_defaults = {'cumtime': False} + name = gettext_lazy("IOI") + config_defaults = {"cumtime": False} """ cumtime: Specify True if time penalties are to be computed. Defaults to False. """ @@ -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,25 +65,29 @@ 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) time = from_database_time(time) - if self.config['cumtime']: + if self.config["cumtime"]: dt = (time - participation.start).total_seconds() else: dt = 0 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] = {"points": 0, "time": 0} + format_data[problem_id]["points"] += subtask_points + format_data[problem_id]["time"] = max( + dt, format_data[problem_id]["time"] + ) for problem_data in format_data.values(): - penalty = problem_data['time'] - points = problem_data['points'] - if self.config['cumtime'] and points: + penalty = problem_data["time"] + points = problem_data["points"] + if self.config["cumtime"] and points: cumtime += penalty score += points @@ -93,10 +98,12 @@ def update_participation(self, participation): participation.save() def get_short_form_display(self): - yield _('The maximum score for each problem batch will be used.') + 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.') + 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." + ) else: - yield _('Ties by score will **not** be broken.') + 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..fb5c5a561c 100644 --- a/judge/contest_format/legacy_ioi.py +++ b/judge/contest_format/legacy_ioi.py @@ -13,10 +13,10 @@ from judge.utils.timedelta import nice_repr -@register_contest_format('ioi') +@register_contest_format("ioi") class LegacyIOIContestFormat(DefaultContestFormat): - name = gettext_lazy('IOI (pre-2016)') - config_defaults = {'cumtime': False} + name = gettext_lazy("IOI (pre-2016)") + config_defaults = {"cumtime": False} """ cumtime: Specify True if time penalties are to be computed. Defaults to False. """ @@ -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,22 +47,28 @@ 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']: + if self.config["cumtime"]: dt = (time - participation.start).total_seconds() if points: cumtime += dt else: dt = 0 - format_data[str(problem_id)] = {'points': points, 'time': dt} + format_data[str(problem_id)] = {"points": points, "time": dt} score += points participation.cumtime = max(cumtime, 0) @@ -74,30 +82,53 @@ 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]), - points=floatformat(format_data['points']), - time=nice_repr(timedelta(seconds=format_data['time']), 'noday') if self.config['cumtime'] else '', + 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 "", ) else: - return mark_safe('') + return mark_safe("") 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.') + 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.') + 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." + ) else: - yield _('Ties by score will **not** be broken.') + yield _("Ties by score will **not** be broken.") diff --git a/judge/dblock.py b/judge/dblock.py index d4d518424c..5b7feabc7d 100644 --- a/judge/dblock.py +++ b/judge/dblock.py @@ -5,19 +5,21 @@ 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): - self.cursor.execute('LOCK TABLES ' + self.tables) + self.cursor.execute("LOCK TABLES " + self.tables) def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is None: transaction.commit() else: transaction.rollback() - self.cursor.execute('UNLOCK TABLES') + self.cursor.execute("UNLOCK TABLES") self.cursor.close() diff --git a/judge/event_poster.py b/judge/event_poster.py index 29100bd993..e7c57fd6d3 100644 --- a/judge/event_poster.py +++ b/judge/event_poster.py @@ -1,6 +1,6 @@ from django.conf import settings -__all__ = ['last', 'post'] +__all__ = ["last", "post"] if not settings.EVENT_DAEMON_USE: real = False @@ -10,9 +10,12 @@ def post(channel, message): def last(): return 0 -elif hasattr(settings, 'EVENT_DAEMON_AMQP'): + +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..959d88241a 100644 --- a/judge/event_poster_amqp.py +++ b/judge/event_poster_amqp.py @@ -6,7 +6,7 @@ from django.conf import settings from pika.exceptions import AMQPError -__all__ = ['EventPoster', 'post', 'last'] +__all__ = ["EventPoster", "post", "last"] class EventPoster(object): @@ -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: @@ -35,7 +40,7 @@ def post(self, channel, message, tries=0): def _get_poster(): - if 'poster' not in _local.__dict__: + if "poster" not in _local.__dict__: _local.poster = EventPoster() return _local.poster diff --git a/judge/event_poster_ws.py b/judge/event_poster_ws.py index fba4052631..8daddab41d 100644 --- a/judge/event_poster_ws.py +++ b/judge/event_poster_ws.py @@ -5,7 +5,7 @@ from django.conf import settings from websocket import WebSocketException, create_connection -__all__ = ['EventPostingError', 'EventPoster', 'post', 'last'] +__all__ = ["EventPostingError", "EventPoster", "post", "last"] _local = threading.local() @@ -20,19 +20,23 @@ 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']) + 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']) + if resp["status"] == "error": + raise EventPostingError(resp["code"]) else: - return resp['id'] + return resp["id"] except WebSocketException: if tries > 10: raise @@ -43,10 +47,10 @@ def last(self, tries=0): try: self._conn.send('{"command": "last-msg"}') resp = json.loads(self._conn.recv()) - if resp['status'] == 'error': - raise EventPostingError(resp['code']) + if resp["status"] == "error": + raise EventPostingError(resp["code"]) else: - return resp['id'] + return resp["id"] except WebSocketException: if tries > 10: raise @@ -55,7 +59,7 @@ def last(self, tries=0): def _get_poster(): - if 'poster' not in _local.__dict__: + if "poster" not in _local.__dict__: _local.poster = EventPoster() return _local.poster diff --git a/judge/feed.py b/judge/feed.py index 4e7cc3c641..1622b5bb15 100644 --- a/judge/feed.py +++ b/judge/feed.py @@ -10,21 +10,23 @@ 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 + title = "Recently Added %s Problems" % settings.SITE_NAME + link = "/" + 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] + return Problem.get_public_problems().order_by("-date", "-id")[:25] def item_title(self, problem): return problem.name def item_description(self, problem): - key = 'problem_feed:%d' % problem.id + key = "problem_feed:%d" % problem.id desc = cache.get(key) if desc is None: - desc = str(markdown(problem.description, 'problem'))[:500] + '...' + desc = str(markdown(problem.description, "problem"))[:500] + "..." cache.set(key, desc, 86400) return desc @@ -40,21 +42,21 @@ class AtomProblemFeed(ProblemFeed): class CommentFeed(Feed): - title = 'Latest %s Comments' % settings.SITE_NAME - link = '/' - description = 'The latest comments on the %s website' % settings.SITE_LONG_NAME + title = "Latest %s Comments" % settings.SITE_NAME + link = "/" + description = "The latest comments on the %s website" % settings.SITE_LONG_NAME def items(self): return Comment.most_recent(AnonymousUser(), 25) def item_title(self, comment): - return '%s -> %s' % (comment.author.user.username, comment.page_title) + return "%s -> %s" % (comment.author.user.username, comment.page_title) def item_description(self, comment): - key = 'comment_feed:%d' % comment.id + key = "comment_feed:%d" % comment.id desc = cache.get(key) if desc is None: - desc = str(markdown(comment.body, 'comment')) + desc = str(markdown(comment.body, "comment")) cache.set(key, desc, 86400) return desc @@ -70,21 +72,23 @@ class AtomCommentFeed(CommentFeed): class BlogFeed(Feed): - title = 'Latest %s Blog Posts' % settings.SITE_NAME - link = '/' - description = 'The latest blog posts from the %s' % settings.SITE_LONG_NAME + title = "Latest %s Blog Posts" % settings.SITE_NAME + link = "/" + 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 def item_description(self, post): - key = 'blog_feed:%d' % post.id + key = "blog_feed:%d" % post.id summary = cache.get(key) if summary is None: - summary = str(markdown(post.summary or post.content, 'blog')) + summary = str(markdown(post.summary or post.content, "blog")) cache.set(key, summary, 86400) return summary diff --git a/judge/forms.py b/judge/forms.py index 87003e56f2..d960e3d021 100644 --- a/judge/forms.py +++ b/judge/forms.py @@ -10,91 +10,139 @@ 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 two_factor_validators_by_length = { 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), + "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, + ), ), - 'verify': lambda code, profile: not profile.check_totp_code(code), - 'err': _('Invalid two-factor authentication token.'), + "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.')), - 'verify': lambda code, profile: code not in json.loads(profile.scratch_codes), - 'err': _('Invalid scratch code.'), + "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."), }, } 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'}), - 'ace_theme': Select2Widget(attrs={'style': 'width:200px'}), - 'site_theme': Select2Widget(attrs={'style': 'width:200px'}), + "timezone": Select2Widget(attrs={"style": "width:200px"}), + "language": Select2Widget(attrs={"style": "width:200px"}), + "ace_theme": Select2Widget(attrs={"style": "width:200px"}), + "site_theme": Select2Widget(attrs={"style": "width:200px"}), } has_math_config = bool(settings.MATHOID_URL) if has_math_config: - fields.append('math_engine') - widgets['math_engine'] = Select2Widget(attrs={'style': 'width:200px'}) + fields.append("math_engine") + widgets["math_engine"] = Select2Widget(attrs={"style": "width:200px"}) if HeavyPreviewPageDownWidget is not None: - widgets['about'] = HeavyPreviewPageDownWidget( - preview=reverse_lazy('profile_preview'), - attrs={'style': 'max-width:700px;min-width:700px;width:700px'}, + widgets["about"] = HeavyPreviewPageDownWidget( + preview=reverse_lazy("profile_preview"), + attrs={"style": "max-width:700px;min-width:700px;width:700px"}, ) 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.')) - return self.cleaned_data['about'] + 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." + ) + ) + return self.cleaned_data["about"] def clean(self): - organizations = self.cleaned_data.get('organizations') or [] + organizations = self.cleaned_data.get("organizations") or [] 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 def __init__(self, *args, **kwargs): - user = kwargs.pop('user', None) + user = kwargs.pop("user", None) super(ProfileForm, self).__init__(*args, **kwargs) - if not user.has_perm('judge.edit_all_organization'): - self.fields['organizations'].queryset = Organization.objects.filter( + if not user.has_perm("judge.edit_all_organization"): + self.fields["organizations"].queryset = Organization.objects.filter( Q(is_open=True) | Q(id__in=user.profile.organizations.all()), ) - if not self.fields['organizations'].queryset: - self.fields.pop('organizations') - self.fields['user_script'].widget = AceWidget(mode='javascript', theme=user.profile.resolved_ace_theme) + if not self.fields["organizations"].queryset: + self.fields.pop("organizations") + self.fields["user_script"].widget = AceWidget( + mode="javascript", theme=user.profile.resolved_ace_theme + ) class EmailChangeForm(Form): @@ -106,161 +154,181 @@ def __init__(self, *args, user, **kwargs): self.user = user def clean_email(self): - if User.objects.filter(email=self.cleaned_data['email']).exists(): - raise ValidationError(_('This email address is already taken.')) - validate_email_domain(self.cleaned_data['email']) - return self.cleaned_data['email'] + if User.objects.filter(email=self.cleaned_data["email"]).exists(): + raise ValidationError(_("This email address is already taken.")) + validate_email_domain(self.cleaned_data["email"]) + return self.cleaned_data["email"] def clean_password(self): - if not self.user.check_password(self.cleaned_data['password']): - raise ValidationError(_('Invalid password')) - return self.cleaned_data['password'] + if not self.user.check_password(self.cleaned_data["password"]): + raise ValidationError(_("Invalid password")) + return self.cleaned_data["password"] 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) + 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_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:'), + label=_("Filter by result:"), ) def clean(self): - can_download = ('comment_download', 'submission_download') + can_download = ("comment_download", "submission_download") if not any(self.cleaned_data[v] for v in can_download): - raise ValidationError(_('Please select at least one thing to download.')) + raise ValidationError(_("Please select at least one thing to download.")) return self.cleaned_data def clean_submission_problem_glob(self): - if not self.cleaned_data['submission_download']: - return '*' - return self.cleaned_data['submission_problem_glob'] + if not self.cleaned_data["submission_download"]: + return "*" + return self.cleaned_data["submission_problem_glob"] def clean_submission_result(self): - if not self.cleaned_data['submission_download']: + if not self.cleaned_data["submission_download"]: return () - return self.cleaned_data['submission_result'] + return self.cleaned_data["submission_result"] 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"].empty_label = None + self.fields["language"].label_from_instance = attrgetter("display_name") + self.fields["language"].queryset = Language.objects.filter( + judges__online=True + ).distinct() if judge_choices: - self.fields['judge'].widget = Select2Widget( - attrs={'style': 'width: 150px', 'data-placeholder': _('Any judge')}, + self.fields["judge"].widget = Select2Widget( + attrs={"style": "width: 150px", "data-placeholder": _("Any judge")}, ) - self.fields['judge'].choices = judge_choices + self.fields["judge"].choices = judge_choices class Meta: model = Submission - fields = ['language'] + fields = ["language"] class EditOrganizationForm(ModelForm): class Meta: model = Organization - fields = ['about', 'logo_override_image', 'admins'] - widgets = {'admins': Select2MultipleWidget(attrs={'style': 'width: 200px'})} + 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): def __init__(self, *args, **kwargs): super(CustomAuthenticationForm, self).__init__(*args, **kwargs) - self.fields['username'].widget.attrs.update({'placeholder': _('Username')}) - self.fields['password'].widget.attrs.update({'placeholder': _('Password')}) + self.fields["username"].widget.attrs.update({"placeholder": _("Username")}) + self.fields["password"].widget.attrs.update({"placeholder": _("Password")}) - self.has_google_auth = self._has_social_auth('GOOGLE_OAUTH2') - self.has_facebook_auth = self._has_social_auth('FACEBOOK') - self.has_github_auth = self._has_social_auth('GITHUB_SECURE') + self.has_google_auth = self._has_social_auth("GOOGLE_OAUTH2") + self.has_facebook_auth = self._has_social_auth("FACEBOOK") + 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): def widget_attrs(self, widget): attrs = super(NoAutoCompleteCharField, self).widget_attrs(widget) - attrs['autocomplete'] = 'off' + attrs["autocomplete"] = "off" return attrs 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') + self.profile = kwargs.pop("profile") super().__init__(*args, **kwargs) def clean(self): - totp_or_scratch_code = self.cleaned_data.get('totp_or_scratch_code') + totp_or_scratch_code = self.cleaned_data.get("totp_or_scratch_code") try: validator = two_factor_validators_by_length[len(totp_or_scratch_code)] except KeyError: - raise ValidationError(_('Invalid code length.')) - validator['regex_validator'](totp_or_scratch_code) - if validator['verify'](totp_or_scratch_code, self.profile): - raise ValidationError(validator['err']) + raise ValidationError(_("Invalid code length.")) + validator["regex_validator"](totp_or_scratch_code) + if validator["verify"](totp_or_scratch_code, self.profile): + raise ValidationError(validator["err"]) class TOTPEnableForm(TOTPForm): def __init__(self, *args, **kwargs): - self.totp_key = kwargs.pop('totp_key') + self.totp_key = kwargs.pop("totp_key") super().__init__(*args, **kwargs) 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): - raise ValidationError(totp_validate['err']) + 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 + ): + raise ValidationError(totp_validate["err"]) class TwoFactorLoginForm(TOTPForm): webauthn_response = forms.CharField(widget=forms.HiddenInput(), required=False) def __init__(self, *args, **kwargs): - self.webauthn_challenge = kwargs.pop('webauthn_challenge') - self.webauthn_origin = kwargs.pop('webauthn_origin') + self.webauthn_challenge = kwargs.pop("webauthn_challenge") + self.webauthn_origin = kwargs.pop("webauthn_origin") super().__init__(*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 len(self.cleaned_data['webauthn_response']) > 65536: - raise ValidationError(_('Invalid WebAuthn response.')) + 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 len(self.cleaned_data["webauthn_response"]) > 65536: + raise ValidationError(_("Invalid WebAuthn response.")) if not self.webauthn_challenge: - raise ValidationError(_('No WebAuthn challenge issued.')) + raise ValidationError(_("No WebAuthn challenge issued.")) - response = json.loads(self.cleaned_data['webauthn_response']) + 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.')) + raise ValidationError(_("Invalid WebAuthn credential ID.")) user = credential.webauthn_user # Work around a useless check in the webauthn package. user.credential_id = credential.cred_id assertion = webauthn.WebAuthnAssertionResponse( webauthn_user=user, - assertion_response=response.get('response'), + assertion_response=response.get("response"), challenge=self.webauthn_challenge, origin=self.webauthn_origin, uv_required=False, @@ -272,41 +340,57 @@ def clean(self): raise ValidationError(str(e)) credential.counter = sign_count - credential.save(update_fields=['counter']) + 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']) + 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.')) + 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'] + code = self.cleaned_data["code"] if Problem.objects.filter(code=code).exists(): - raise ValidationError(_('Problem with code already exists.')) + raise ValidationError(_("Problem with code already exists.")) return code 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'] + key = self.cleaned_data["key"] if Contest.objects.filter(key=key).exists(): - raise ValidationError(_('Contest with key already exists.')) + raise ValidationError(_("Contest with key already exists.")) return key @@ -315,4 +399,4 @@ class ProblemPointsVoteForm(ModelForm): class Meta: model = ProblemPointsVote - fields = ['points', 'note'] + fields = ["points", "note"] diff --git a/judge/fulltext.py b/judge/fulltext.py index 5b9f7d3d09..209a87e3d9 100644 --- a/judge/fulltext.py +++ b/judge/fulltext.py @@ -5,10 +5,10 @@ class SearchQuerySet(QuerySet): - DEFAULT = '' - BOOLEAN = ' IN BOOLEAN MODE' - NATURAL_LANGUAGE = ' IN NATURAL LANGUAGE MODE' - QUERY_EXPANSION = ' WITH QUERY EXPANSION' + DEFAULT = "" + BOOLEAN = " IN BOOLEAN MODE" + NATURAL_LANGUAGE = " IN NATURAL LANGUAGE MODE" + QUERY_EXPANSION = " WITH QUERY EXPANSION" def __init__(self, fields=None, **kwargs): super(SearchQuerySet, self).__init__(**kwargs) @@ -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)) + fulltext_columns = ", ".join(full_names) + 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..9c65568714 100644 --- a/judge/highlight_code.py +++ b/judge/highlight_code.py @@ -1,11 +1,11 @@ from django.utils.html import format_html from django.utils.safestring import mark_safe -__all__ = ['highlight_code'] +__all__ = ["highlight_code"] def _make_pre_code(code): - return format_html('
{0}
', code) + return format_html("
{0}
", code) try: @@ -14,15 +14,22 @@ 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'): + + def highlight_code(code, language, cssclass="codehilite"): try: lexer = pygments.lexers.get_lexer_by_name(language) except pygments.util.ClassNotFound: 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..dc946287cf 100644 --- a/judge/jinja2/__init__.py +++ b/judge/jinja2/__init__.py @@ -8,19 +8,33 @@ 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) -registry.filter('str', str) -registry.filter('json', json.dumps) -registry.filter('highlight', highlight_code) -registry.filter('urlquote', quote) -registry.filter('roundfloat', round) -registry.function('inlinei18n', inlinei18n) -registry.function('mptt_tree', get_cached_trees) -registry.function('user_trans', gettext) +registry.function("str", str) +registry.filter("str", str) +registry.filter("json", json.dumps) +registry.filter("highlight", highlight_code) +registry.filter("urlquote", quote) +registry.filter("roundfloat", round) +registry.function("inlinei18n", inlinei18n) +registry.function("mptt_tree", get_cached_trees) +registry.function("user_trans", gettext) @registry.function diff --git a/judge/jinja2/datetime.py b/judge/jinja2/datetime.py index 60b3f9753c..030596094b 100644 --- a/judge/jinja2/datetime.py +++ b/judge/jinja2/datetime.py @@ -13,7 +13,7 @@ def localtime_wrapper(func): @functools.wraps(func) def wrapper(datetime, *args, **kwargs): - if getattr(datetime, 'convert_to_local_time', True): + if getattr(datetime, "convert_to_local_time", True): datetime = localtime(datetime) return func(datetime, *args, **kwargs) @@ -26,7 +26,9 @@ 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))}') + 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))}' + ) diff --git a/judge/jinja2/filesize.py b/judge/jinja2/filesize.py index 7b27fdebb7..0cb0fef768 100644 --- a/judge/jinja2/filesize.py +++ b/judge/jinja2/filesize.py @@ -13,24 +13,28 @@ def _format_size(bytes, callback): PB = 1 << 50 if bytes < KB: - return callback('', bytes) + return callback("", bytes) elif bytes < MB: - return callback('K', bytes / KB) + return callback("K", bytes / KB) elif bytes < GB: - return callback('M', bytes / MB) + return callback("M", bytes / MB) elif bytes < TB: - return callback('G', bytes / GB) + return callback("G", bytes / GB) elif bytes < PB: - return callback('T', bytes / TB) + return callback("T", bytes / TB) else: - return callback('P', bytes / PB) + return callback("P", bytes / PB) @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 def kbsimpleformat(kb): - return _format_size(kb * 1024, lambda x, y: '%.0f%s' % (y, x or 'B')) + return _format_size(kb * 1024, lambda x, y: "%.0f%s" % (y, x or "B")) diff --git a/judge/jinja2/format.py b/judge/jinja2/format.py index 857f1cee0b..38995a9b46 100644 --- a/judge/jinja2/format.py +++ b/judge/jinja2/format.py @@ -5,4 +5,4 @@ @registry.function def bold(text): - return format_html('{0}', text) + return format_html("{0}", text) diff --git a/judge/jinja2/gravatar.py b/judge/jinja2/gravatar.py index 259bf3ef59..d5b97b919b 100644 --- a/judge/jinja2/gravatar.py +++ b/judge/jinja2/gravatar.py @@ -17,9 +17,13 @@ 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() + '?' - args = {'d': 'identicon', 's': str(size)} + 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' + args["f"] = "y" gravatar_url += urlencode(args) return gravatar_url diff --git a/judge/jinja2/language.py b/judge/jinja2/language.py index 344568a68d..dda4456759 100644 --- a/judge/jinja2/language.py +++ b/judge/jinja2/language.py @@ -3,7 +3,7 @@ from . import registry -@registry.function('language_info') +@registry.function("language_info") def get_language_info(language): # ``language`` is either a language code string or a sequence # with the language code as its first item @@ -13,6 +13,6 @@ def get_language_info(language): return translation.get_language_info(str(language)) -@registry.function('language_info_list') +@registry.function("language_info_list") def get_language_info_list(langs): return [get_language_info(lang) for lang in langs] diff --git a/judge/jinja2/markdown/__init__.py b/judge/jinja2/markdown/__init__.py index 6bb8d689b3..b15d510e70 100644 --- a/judge/jinja2/markdown/__init__.py +++ b/judge/jinja2/markdown/__init__.py @@ -19,14 +19,14 @@ from .bleach_whitelist import all_styles, mathml_attrs, mathml_tags from .. import registry -logger = logging.getLogger('judge.html') +logger = logging.getLogger("judge.html") NOFOLLOW_WHITELIST = settings.NOFOLLOW_EXCLUDED class CodeSafeInlineGrammar(mistune.InlineGrammar): - double_emphasis = re.compile(r'^\*{2}([\s\S]+?)()\*{2}(?!\*)') # **word** - emphasis = re.compile(r'^\*((?:\*\*|[^\*])+?)()\*(?!\*)') # *word* + double_emphasis = re.compile(r"^\*{2}([\s\S]+?)()\*{2}(?!\*)") # **word** + emphasis = re.compile(r"^\*((?:\*\*|[^\*])+?)()\*(?!\*)") # *word* class AwesomeInlineGrammar(MathInlineGrammar, CodeSafeInlineGrammar): @@ -39,8 +39,8 @@ class AwesomeInlineLexer(MathInlineLexer, mistune.InlineLexer): class AwesomeRenderer(MathRenderer, mistune.Renderer): def __init__(self, *args, **kwargs): - self.nofollow = kwargs.pop('nofollow', True) - self.texoid = TexoidRenderer() if kwargs.pop('texoid', False) else None + self.nofollow = kwargs.pop("nofollow", True) + self.texoid = TexoidRenderer() if kwargs.pop("texoid", False) else None super(AwesomeRenderer, self).__init__(*args, **kwargs) def _link_rel(self, href): @@ -52,18 +52,18 @@ def _link_rel(self, href): else: if url.netloc and url.netloc not in NOFOLLOW_WHITELIST: return ' rel="nofollow"' - return '' + return "" def autolink(self, link, is_email=False): text = link = mistune.escape(link) if is_email: - link = 'mailto:%s' % link + link = "mailto:%s" % link return '%s' % (link, self._link_rel(link), text) def table(self, header, body): return ( '\n%s\n' - '\n%s\n
\n' + "\n%s\n\n" ) % (header, body) def link(self, link, title, text): @@ -71,40 +71,53 @@ 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: - return '\n
%s
\n' % mistune.escape(code).rstrip() + return "\n
%s
\n" % mistune.escape(code).rstrip() return highlight_code(code, lang) def block_html(self, html): - if self.texoid and html.startswith('')] - latex = html[html.index('>') + 1:html.rindex('<')] + if self.texoid and html.startswith("")] + 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'], - 'tail': ' /' if self.options.get('use_xhtml') else '', + 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"], + "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']] - if 'inline' in attr: - tag = 'span' + 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: - tag = 'div' - style += ['text-align: center'] - return '<%s style="%s">%s' % (tag, ';'.join(style), img, tag) + tag = "div" + 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): @@ -118,26 +131,32 @@ def get_cleaner(name, params): if name in cleaner_cache: return cleaner_cache[name] - styles = params.pop('styles', None) + 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 - params['attributes'] = params.get('attributes', {}).copy() - params['attributes'].update(mathml_attrs) + if params.pop("mathml", False): + params["tags"] = params.get("tags", []) + mathml_tags + params["attributes"] = params.get("attributes", {}).copy() + params["attributes"].update(mathml_attrs) cleaner = cleaner_cache[name] = Cleaner(**params) return cleaner def fragments_to_tree(fragment): - tree = html.Element('div') + 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'): - logger.exception('Failed to parse HTML string') + if fragment and ( + not isinstance(e, ParserError) or e.args[0] != "Document is empty" + ): + logger.exception("Failed to parse HTML string") return tree if parsed and isinstance(parsed[0], str): @@ -148,41 +167,50 @@ def fragments_to_tree(fragment): def strip_paragraphs_tags(tree): - for p in tree.xpath('.//p'): + for p in tree.xpath(".//p"): for child in p.iterchildren(reversed=True): p.addnext(child) parent = p.getparent() prev = p.getprevious() if prev is not None: - prev.tail = (prev.tail or '') + p.text + prev.tail = (prev.tail or "") + p.text else: - parent.text = (parent.text or '') + p.text + parent.text = (parent.text or "") + p.text parent.remove(p) def fragment_tree_to_str(tree): - return html.tostring(tree, encoding='unicode')[len('
'):-len('
')] + return html.tostring(tree, encoding="unicode")[len("
") : -len("
")] @registry.filter def markdown(value, style, math_engine=None, lazy_load=False, strip_paragraphs=False): styles = settings.MARKDOWN_STYLES.get(style, settings.MARKDOWN_DEFAULT_STYLE) - escape = styles.get('safe_mode', True) - nofollow = styles.get('nofollow', True) - texoid = TEXOID_ENABLED and styles.get('texoid', False) - math = getattr(settings, 'MATHOID_URL') and styles.get('math', False) - bleach_params = styles.get('bleach', {}) + escape = styles.get("safe_mode", True) + nofollow = styles.get("nofollow", True) + texoid = TEXOID_ENABLED and styles.get("texoid", False) + math = getattr(settings, "MATHOID_URL") and styles.get("math", False) + bleach_params = styles.get("bleach", {}) post_processors = [] - if styles.get('use_camo', False) and camo_client is not None: + if styles.get("use_camo", False) and camo_client is not None: post_processors.append(camo_client.update_tree) 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..0b59a28bb5 100644 --- a/judge/jinja2/markdown/bleach_whitelist.py +++ b/judge/jinja2/markdown/bleach_whitelist.py @@ -5,523 +5,1548 @@ # 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', '