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 = ('') % html
+ html = (
+ ''
+ ) % 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'
+ "\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%s>' % (tag, ';'.join(style), img, tag)
+ tag = "div"
+ style += ["text-align: center"]
+ return '<%s style="%s">%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', '',
- '', 'top', 'touch-action', 'transform', 'transform-origin',
- 'transform-style', 'transition', 'transition-delay', 'transition-duration',
- 'transition-property', 'transition-timing-function', 'translate()',
- 'translatex()', 'translatey()', 'translatez()', 'translate3d()', 'turn',
- 'unicode-bidi', 'unicode-range', 'unset', '', 'url()', '',
- ':valid', '::value', 'var()', 'vertical-align', 'vh', '@viewport',
- 'visibility', ':visited', 'vmax', 'vmin', 'vw', 'white-space', 'widows',
- 'width', 'will-change', 'word-break', 'word-spacing', 'word-wrap',
- 'writing-mode', 'z-index',
+ ":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",
+ "",
+ "",
+ "top",
+ "touch-action",
+ "transform",
+ "transform-origin",
+ "transform-style",
+ "transition",
+ "transition-delay",
+ "transition-duration",
+ "transition-property",
+ "transition-timing-function",
+ "translate()",
+ "translatex()",
+ "translatey()",
+ "translatez()",
+ "translate3d()",
+ "turn",
+ "unicode-bidi",
+ "unicode-range",
+ "unset",
+ "",
+ "url()",
+ "",
+ ":valid",
+ "::value",
+ "var()",
+ "vertical-align",
+ "vh",
+ "@viewport",
+ "visibility",
+ ":visited",
+ "vmax",
+ "vmin",
+ "vw",
+ "white-space",
+ "widows",
+ "width",
+ "will-change",
+ "word-break",
+ "word-spacing",
+ "word-wrap",
+ "writing-mode",
+ "z-index",
]
all_prefixed_styles = [
# From http://peter.sh/experiments/vendor-prefixed-css-property-overview/
- '-ms-accelerator', '-webkit-app-region', '-webkit-appearance',
- '-webkit-appearance', '-moz-appearance', '-webkit-aspect-ratio',
- '-webkit-backdrop-filter', 'backface-visibility',
- '-webkit-backface-visibility', 'backface-visibility', 'backface-visibility',
- '-webkit-background-composite', '-webkit-background-composite', '-moz-binding',
- '-ms-block-progression', '-webkit-border-after', '-webkit-border-after',
- '-webkit-border-after-color', '-webkit-border-after-color',
- '-webkit-border-after-style', '-webkit-border-after-style',
- '-webkit-border-after-width', '-webkit-border-after-width',
- '-webkit-border-before', '-webkit-border-before',
- '-webkit-border-before-color', '-webkit-border-before-color',
- '-webkit-border-before-style', '-webkit-border-before-style',
- '-webkit-border-before-width', '-webkit-border-before-width',
- '-moz-border-bottom-colors', '-webkit-border-end', '-webkit-border-end',
- '-moz-border-end', '-webkit-border-end-color', '-webkit-border-end-color',
- '-moz-border-end-color', '-webkit-border-end-style',
- '-webkit-border-end-style', '-moz-border-end-style',
- '-webkit-border-end-width', '-webkit-border-end-width',
- '-moz-border-end-width', '-webkit-border-fit',
- '-webkit-border-horizontal-spacing', '-webkit-border-horizontal-spacing',
- '-moz-border-left-colors', '-moz-border-right-colors', '-webkit-border-start',
- '-webkit-border-start', '-moz-border-start', '-webkit-border-start-color',
- '-webkit-border-start-color', '-moz-border-start-color',
- '-webkit-border-start-style', '-webkit-border-start-style',
- '-moz-border-start-style', '-webkit-border-start-width',
- '-webkit-border-start-width', '-moz-border-start-width',
- '-moz-border-top-colors', '-webkit-border-vertical-spacing',
- '-webkit-border-vertical-spacing', '-webkit-box-align', '-webkit-box-align',
- '-moz-box-align', '-webkit-box-decoration-break',
- '-webkit-box-decoration-break', 'box-decoration-break',
- '-webkit-box-direction', '-webkit-box-direction', '-moz-box-direction',
- '-webkit-box-flex', '-webkit-box-flex', '-moz-box-flex',
- '-webkit-box-flex-group', '-webkit-box-flex-group', '-webkit-box-lines',
- '-webkit-box-lines', '-webkit-box-ordinal-group', '-webkit-box-ordinal-group',
- '-moz-box-ordinal-group', '-webkit-box-orient', '-webkit-box-orient',
- '-moz-box-orient', '-webkit-box-pack', '-webkit-box-pack', '-moz-box-pack',
- '-webkit-box-reflect', '-webkit-box-reflect', 'clip-path', '-webkit-clip-path',
- 'clip-path', 'clip-path', '-webkit-color-correction', '-webkit-column-axis',
- '-webkit-column-break-after', '-webkit-column-break-after',
- '-webkit-column-break-before', '-webkit-column-break-before',
- '-webkit-column-break-inside', '-webkit-column-break-inside',
- '-webkit-column-count', 'column-count', '-moz-column-count', 'column-count',
- 'column-fill', 'column-fill', '-moz-column-fill', 'column-fill',
- '-webkit-column-gap', 'column-gap', '-moz-column-gap', 'column-gap',
- '-webkit-column-rule', 'column-rule', '-moz-column-rule', 'column-rule',
- '-webkit-column-rule-color', 'column-rule-color', '-moz-column-rule-color',
- 'column-rule-color', '-webkit-column-rule-style', 'column-rule-style',
- '-moz-column-rule-style', 'column-rule-style', '-webkit-column-rule-width',
- 'column-rule-width', '-moz-column-rule-width', 'column-rule-width',
- '-webkit-column-span', 'column-span', 'column-span', '-webkit-column-width',
- 'column-width', '-moz-column-width', 'column-width', '-webkit-columns',
- 'columns', '-moz-columns', 'columns', '-ms-content-zoom-chaining',
- '-ms-content-zoom-limit', '-ms-content-zoom-limit-max',
- '-ms-content-zoom-limit-min', '-ms-content-zoom-snap',
- '-ms-content-zoom-snap-points', '-ms-content-zoom-snap-type',
- '-ms-content-zooming', '-moz-control-character-visibility',
- '-webkit-cursor-visibility', '-webkit-dashboard-region', 'filter',
- '-webkit-filter', 'filter', 'filter', '-ms-flex-align', '-ms-flex-item-align',
- '-ms-flex-line-pack', '-ms-flex-negative', '-ms-flex-order', '-ms-flex-pack',
- '-ms-flex-positive', '-ms-flex-preferred-size', '-moz-float-edge',
- '-webkit-flow-from', '-ms-flow-from', '-webkit-flow-into', '-ms-flow-into',
- '-webkit-font-feature-settings', '-webkit-font-feature-settings',
- 'font-feature-settings', 'font-feature-settings', 'font-kerning',
- '-webkit-font-kerning', 'font-kerning', '-webkit-font-size-delta',
- '-webkit-font-size-delta', '-webkit-font-smoothing', '-webkit-font-smoothing',
- 'font-variant-ligatures', '-webkit-font-variant-ligatures',
- 'font-variant-ligatures', '-moz-force-broken-image-icon', 'grid',
- '-webkit-grid', 'grid', 'grid-area', '-webkit-grid-area', 'grid-area',
- 'grid-auto-columns', '-webkit-grid-auto-columns', 'grid-auto-columns',
- 'grid-auto-flow', '-webkit-grid-auto-flow', 'grid-auto-flow', 'grid-auto-rows',
- '-webkit-grid-auto-rows', 'grid-auto-rows', 'grid-column',
- '-webkit-grid-column', 'grid-column', '-ms-grid-column',
- '-ms-grid-column-align', 'grid-column-end', '-webkit-grid-column-end',
- 'grid-column-end', '-ms-grid-column-span', 'grid-column-start',
- '-webkit-grid-column-start', 'grid-column-start', '-ms-grid-columns',
- 'grid-row', '-webkit-grid-row', 'grid-row', '-ms-grid-row',
- '-ms-grid-row-align', 'grid-row-end', '-webkit-grid-row-end', 'grid-row-end',
- '-ms-grid-row-span', 'grid-row-start', '-webkit-grid-row-start',
- 'grid-row-start', '-ms-grid-rows', 'grid-template', '-webkit-grid-template',
- 'grid-template', 'grid-template-areas', '-webkit-grid-template-areas',
- 'grid-template-areas', 'grid-template-columns',
- '-webkit-grid-template-columns', 'grid-template-columns', 'grid-template-rows',
- '-webkit-grid-template-rows', 'grid-template-rows', '-ms-high-contrast-adjust',
- '-webkit-highlight', '-webkit-hyphenate-character',
- '-webkit-hyphenate-character', '-webkit-hyphenate-limit-after',
- '-webkit-hyphenate-limit-before', '-ms-hyphenate-limit-chars',
- '-webkit-hyphenate-limit-lines', '-ms-hyphenate-limit-lines',
- '-ms-hyphenate-limit-zone', '-webkit-hyphens', '-moz-hyphens', '-ms-hyphens',
- '-moz-image-region', '-ms-ime-align', '-webkit-initial-letter',
- '-ms-interpolation-mode', 'justify-self', '-webkit-justify-self',
- '-webkit-line-align', '-webkit-line-box-contain', '-webkit-line-box-contain',
- '-webkit-line-break', '-webkit-line-break', 'line-break', '-webkit-line-clamp',
- '-webkit-line-clamp', '-webkit-line-grid', '-webkit-line-snap',
- '-webkit-locale', '-webkit-locale', '-webkit-logical-height',
- '-webkit-logical-height', '-webkit-logical-width', '-webkit-logical-width',
- '-webkit-margin-after', '-webkit-margin-after',
- '-webkit-margin-after-collapse', '-webkit-margin-after-collapse',
- '-webkit-margin-before', '-webkit-margin-before',
- '-webkit-margin-before-collapse', '-webkit-margin-before-collapse',
- '-webkit-margin-bottom-collapse', '-webkit-margin-bottom-collapse',
- '-webkit-margin-collapse', '-webkit-margin-collapse', '-webkit-margin-end',
- '-webkit-margin-end', '-moz-margin-end', '-webkit-margin-start',
- '-webkit-margin-start', '-moz-margin-start', '-webkit-margin-top-collapse',
- '-webkit-margin-top-collapse', '-webkit-marquee', '-webkit-marquee-direction',
- '-webkit-marquee-increment', '-webkit-marquee-repetition',
- '-webkit-marquee-speed', '-webkit-marquee-style', 'mask', '-webkit-mask',
- 'mask', '-webkit-mask-box-image', '-webkit-mask-box-image',
- '-webkit-mask-box-image-outset', '-webkit-mask-box-image-outset',
- '-webkit-mask-box-image-repeat', '-webkit-mask-box-image-repeat',
- '-webkit-mask-box-image-slice', '-webkit-mask-box-image-slice',
- '-webkit-mask-box-image-source', '-webkit-mask-box-image-source',
- '-webkit-mask-box-image-width', '-webkit-mask-box-image-width',
- '-webkit-mask-clip', '-webkit-mask-clip', '-webkit-mask-composite',
- '-webkit-mask-composite', '-webkit-mask-image', '-webkit-mask-image',
- '-webkit-mask-origin', '-webkit-mask-origin', '-webkit-mask-position',
- '-webkit-mask-position', '-webkit-mask-position-x', '-webkit-mask-position-x',
- '-webkit-mask-position-y', '-webkit-mask-position-y', '-webkit-mask-repeat',
- '-webkit-mask-repeat', '-webkit-mask-repeat-x', '-webkit-mask-repeat-x',
- '-webkit-mask-repeat-y', '-webkit-mask-repeat-y', '-webkit-mask-size',
- '-webkit-mask-size', 'mask-source-type', '-webkit-mask-source-type',
- '-moz-math-display', '-moz-math-variant', '-webkit-max-logical-height',
- '-webkit-max-logical-height', '-webkit-max-logical-width',
- '-webkit-max-logical-width', '-webkit-min-logical-height',
- '-webkit-min-logical-height', '-webkit-min-logical-width',
- '-webkit-min-logical-width', '-webkit-nbsp-mode', '-moz-orient',
- '-moz-osx-font-smoothing', '-moz-outline-radius',
- '-moz-outline-radius-bottomleft', '-moz-outline-radius-bottomright',
- '-moz-outline-radius-topleft', '-moz-outline-radius-topright',
- '-webkit-overflow-scrolling', '-ms-overflow-style', '-webkit-padding-after',
- '-webkit-padding-after', '-webkit-padding-before', '-webkit-padding-before',
- '-webkit-padding-end', '-webkit-padding-end', '-moz-padding-end',
- '-webkit-padding-start', '-webkit-padding-start', '-moz-padding-start',
- 'perspective', '-webkit-perspective', 'perspective', 'perspective',
- 'perspective-origin', '-webkit-perspective-origin', 'perspective-origin',
- 'perspective-origin', '-webkit-perspective-origin-x',
- '-webkit-perspective-origin-x', 'perspective-origin-x',
- '-webkit-perspective-origin-y', '-webkit-perspective-origin-y',
- 'perspective-origin-y', '-webkit-print-color-adjust',
- '-webkit-print-color-adjust', '-webkit-region-break-after',
- '-webkit-region-break-before', '-webkit-region-break-inside',
- '-webkit-region-fragment', '-webkit-rtl-ordering', '-webkit-rtl-ordering',
- '-webkit-ruby-position', '-webkit-ruby-position', 'ruby-position',
- '-moz-script-level', '-moz-script-min-size', '-moz-script-size-multiplier',
- '-ms-scroll-chaining', '-ms-scroll-limit', '-ms-scroll-limit-x-max',
- '-ms-scroll-limit-x-min', '-ms-scroll-limit-y-max', '-ms-scroll-limit-y-min',
- '-ms-scroll-rails', '-webkit-scroll-snap-coordinate',
- '-webkit-scroll-snap-destination', '-webkit-scroll-snap-points-x',
- '-ms-scroll-snap-points-x', '-webkit-scroll-snap-points-y',
- '-ms-scroll-snap-points-y', '-webkit-scroll-snap-type', '-ms-scroll-snap-type',
- '-ms-scroll-snap-x', '-ms-scroll-snap-y', '-ms-scroll-translation',
- '-ms-scrollbar-3dlight-color', 'shape-image-threshold',
- '-webkit-shape-image-threshold', 'shape-margin', '-webkit-shape-margin',
- 'shape-outside', '-webkit-shape-outside', '-moz-stack-sizing', 'tab-size',
- 'tab-size', '-moz-tab-size', '-webkit-tap-highlight-color',
- '-webkit-tap-highlight-color', 'text-align-last', '-webkit-text-align-last',
- '-moz-text-align-last', 'text-align-last', '-webkit-text-combine',
- '-webkit-text-combine', '-ms-text-combine-horizontal', 'text-decoration-color',
- '-webkit-text-decoration-color', 'text-decoration-color',
- 'text-decoration-color', 'text-decoration-line',
- '-webkit-text-decoration-line', 'text-decoration-line',
- '-webkit-text-decoration-skip', 'text-decoration-style',
- '-webkit-text-decoration-style', 'text-decoration-style',
- '-webkit-text-decorations-in-effect', '-webkit-text-decorations-in-effect',
- '-webkit-text-emphasis', 'text-emphasis', '-webkit-text-emphasis-color',
- 'text-emphasis-color', '-webkit-text-emphasis-position',
- 'text-emphasis-position', '-webkit-text-emphasis-style', 'text-emphasis-style',
- '-webkit-text-fill-color', '-webkit-text-fill-color', 'text-justify',
- '-webkit-text-justify', 'text-justify', '-webkit-text-orientation',
- '-webkit-text-orientation', 'text-orientation', '-webkit-text-security',
- '-webkit-text-security', '-webkit-text-size-adjust', '-moz-text-size-adjust',
- '-ms-text-size-adjust', '-webkit-text-stroke', '-webkit-text-stroke',
- '-webkit-text-stroke-color', '-webkit-text-stroke-color',
- '-webkit-text-stroke-width', '-webkit-text-stroke-width',
- 'text-underline-position', '-webkit-text-underline-position',
- 'text-underline-position', '-webkit-touch-callout', '-ms-touch-select',
- 'transform', '-webkit-transform', 'transform', 'transform', 'transform-origin',
- '-webkit-transform-origin', 'transform-origin', 'transform-origin',
- '-webkit-transform-origin-x', '-webkit-transform-origin-x',
- 'transform-origin-x', '-webkit-transform-origin-y',
- '-webkit-transform-origin-y', 'transform-origin-y',
- '-webkit-transform-origin-z', '-webkit-transform-origin-z',
- 'transform-origin-z', 'transform-style', '-webkit-transform-style',
- 'transform-style', 'transform-style', '-webkit-user-drag', '-webkit-user-drag',
- '-moz-user-focus', '-moz-user-input', '-webkit-user-modify',
- '-webkit-user-modify', '-moz-user-modify', '-webkit-user-select',
- '-webkit-user-select', '-moz-user-select', '-ms-user-select',
- '-moz-window-dragging', '-moz-window-shadow', '-ms-wrap-flow',
- '-ms-wrap-margin', '-ms-wrap-through', 'writing-mode', '-webkit-writing-mode',
- 'writing-mode', 'writing-mode',
+ "-ms-accelerator",
+ "-webkit-app-region",
+ "-webkit-appearance",
+ "-webkit-appearance",
+ "-moz-appearance",
+ "-webkit-aspect-ratio",
+ "-webkit-backdrop-filter",
+ "backface-visibility",
+ "-webkit-backface-visibility",
+ "backface-visibility",
+ "backface-visibility",
+ "-webkit-background-composite",
+ "-webkit-background-composite",
+ "-moz-binding",
+ "-ms-block-progression",
+ "-webkit-border-after",
+ "-webkit-border-after",
+ "-webkit-border-after-color",
+ "-webkit-border-after-color",
+ "-webkit-border-after-style",
+ "-webkit-border-after-style",
+ "-webkit-border-after-width",
+ "-webkit-border-after-width",
+ "-webkit-border-before",
+ "-webkit-border-before",
+ "-webkit-border-before-color",
+ "-webkit-border-before-color",
+ "-webkit-border-before-style",
+ "-webkit-border-before-style",
+ "-webkit-border-before-width",
+ "-webkit-border-before-width",
+ "-moz-border-bottom-colors",
+ "-webkit-border-end",
+ "-webkit-border-end",
+ "-moz-border-end",
+ "-webkit-border-end-color",
+ "-webkit-border-end-color",
+ "-moz-border-end-color",
+ "-webkit-border-end-style",
+ "-webkit-border-end-style",
+ "-moz-border-end-style",
+ "-webkit-border-end-width",
+ "-webkit-border-end-width",
+ "-moz-border-end-width",
+ "-webkit-border-fit",
+ "-webkit-border-horizontal-spacing",
+ "-webkit-border-horizontal-spacing",
+ "-moz-border-left-colors",
+ "-moz-border-right-colors",
+ "-webkit-border-start",
+ "-webkit-border-start",
+ "-moz-border-start",
+ "-webkit-border-start-color",
+ "-webkit-border-start-color",
+ "-moz-border-start-color",
+ "-webkit-border-start-style",
+ "-webkit-border-start-style",
+ "-moz-border-start-style",
+ "-webkit-border-start-width",
+ "-webkit-border-start-width",
+ "-moz-border-start-width",
+ "-moz-border-top-colors",
+ "-webkit-border-vertical-spacing",
+ "-webkit-border-vertical-spacing",
+ "-webkit-box-align",
+ "-webkit-box-align",
+ "-moz-box-align",
+ "-webkit-box-decoration-break",
+ "-webkit-box-decoration-break",
+ "box-decoration-break",
+ "-webkit-box-direction",
+ "-webkit-box-direction",
+ "-moz-box-direction",
+ "-webkit-box-flex",
+ "-webkit-box-flex",
+ "-moz-box-flex",
+ "-webkit-box-flex-group",
+ "-webkit-box-flex-group",
+ "-webkit-box-lines",
+ "-webkit-box-lines",
+ "-webkit-box-ordinal-group",
+ "-webkit-box-ordinal-group",
+ "-moz-box-ordinal-group",
+ "-webkit-box-orient",
+ "-webkit-box-orient",
+ "-moz-box-orient",
+ "-webkit-box-pack",
+ "-webkit-box-pack",
+ "-moz-box-pack",
+ "-webkit-box-reflect",
+ "-webkit-box-reflect",
+ "clip-path",
+ "-webkit-clip-path",
+ "clip-path",
+ "clip-path",
+ "-webkit-color-correction",
+ "-webkit-column-axis",
+ "-webkit-column-break-after",
+ "-webkit-column-break-after",
+ "-webkit-column-break-before",
+ "-webkit-column-break-before",
+ "-webkit-column-break-inside",
+ "-webkit-column-break-inside",
+ "-webkit-column-count",
+ "column-count",
+ "-moz-column-count",
+ "column-count",
+ "column-fill",
+ "column-fill",
+ "-moz-column-fill",
+ "column-fill",
+ "-webkit-column-gap",
+ "column-gap",
+ "-moz-column-gap",
+ "column-gap",
+ "-webkit-column-rule",
+ "column-rule",
+ "-moz-column-rule",
+ "column-rule",
+ "-webkit-column-rule-color",
+ "column-rule-color",
+ "-moz-column-rule-color",
+ "column-rule-color",
+ "-webkit-column-rule-style",
+ "column-rule-style",
+ "-moz-column-rule-style",
+ "column-rule-style",
+ "-webkit-column-rule-width",
+ "column-rule-width",
+ "-moz-column-rule-width",
+ "column-rule-width",
+ "-webkit-column-span",
+ "column-span",
+ "column-span",
+ "-webkit-column-width",
+ "column-width",
+ "-moz-column-width",
+ "column-width",
+ "-webkit-columns",
+ "columns",
+ "-moz-columns",
+ "columns",
+ "-ms-content-zoom-chaining",
+ "-ms-content-zoom-limit",
+ "-ms-content-zoom-limit-max",
+ "-ms-content-zoom-limit-min",
+ "-ms-content-zoom-snap",
+ "-ms-content-zoom-snap-points",
+ "-ms-content-zoom-snap-type",
+ "-ms-content-zooming",
+ "-moz-control-character-visibility",
+ "-webkit-cursor-visibility",
+ "-webkit-dashboard-region",
+ "filter",
+ "-webkit-filter",
+ "filter",
+ "filter",
+ "-ms-flex-align",
+ "-ms-flex-item-align",
+ "-ms-flex-line-pack",
+ "-ms-flex-negative",
+ "-ms-flex-order",
+ "-ms-flex-pack",
+ "-ms-flex-positive",
+ "-ms-flex-preferred-size",
+ "-moz-float-edge",
+ "-webkit-flow-from",
+ "-ms-flow-from",
+ "-webkit-flow-into",
+ "-ms-flow-into",
+ "-webkit-font-feature-settings",
+ "-webkit-font-feature-settings",
+ "font-feature-settings",
+ "font-feature-settings",
+ "font-kerning",
+ "-webkit-font-kerning",
+ "font-kerning",
+ "-webkit-font-size-delta",
+ "-webkit-font-size-delta",
+ "-webkit-font-smoothing",
+ "-webkit-font-smoothing",
+ "font-variant-ligatures",
+ "-webkit-font-variant-ligatures",
+ "font-variant-ligatures",
+ "-moz-force-broken-image-icon",
+ "grid",
+ "-webkit-grid",
+ "grid",
+ "grid-area",
+ "-webkit-grid-area",
+ "grid-area",
+ "grid-auto-columns",
+ "-webkit-grid-auto-columns",
+ "grid-auto-columns",
+ "grid-auto-flow",
+ "-webkit-grid-auto-flow",
+ "grid-auto-flow",
+ "grid-auto-rows",
+ "-webkit-grid-auto-rows",
+ "grid-auto-rows",
+ "grid-column",
+ "-webkit-grid-column",
+ "grid-column",
+ "-ms-grid-column",
+ "-ms-grid-column-align",
+ "grid-column-end",
+ "-webkit-grid-column-end",
+ "grid-column-end",
+ "-ms-grid-column-span",
+ "grid-column-start",
+ "-webkit-grid-column-start",
+ "grid-column-start",
+ "-ms-grid-columns",
+ "grid-row",
+ "-webkit-grid-row",
+ "grid-row",
+ "-ms-grid-row",
+ "-ms-grid-row-align",
+ "grid-row-end",
+ "-webkit-grid-row-end",
+ "grid-row-end",
+ "-ms-grid-row-span",
+ "grid-row-start",
+ "-webkit-grid-row-start",
+ "grid-row-start",
+ "-ms-grid-rows",
+ "grid-template",
+ "-webkit-grid-template",
+ "grid-template",
+ "grid-template-areas",
+ "-webkit-grid-template-areas",
+ "grid-template-areas",
+ "grid-template-columns",
+ "-webkit-grid-template-columns",
+ "grid-template-columns",
+ "grid-template-rows",
+ "-webkit-grid-template-rows",
+ "grid-template-rows",
+ "-ms-high-contrast-adjust",
+ "-webkit-highlight",
+ "-webkit-hyphenate-character",
+ "-webkit-hyphenate-character",
+ "-webkit-hyphenate-limit-after",
+ "-webkit-hyphenate-limit-before",
+ "-ms-hyphenate-limit-chars",
+ "-webkit-hyphenate-limit-lines",
+ "-ms-hyphenate-limit-lines",
+ "-ms-hyphenate-limit-zone",
+ "-webkit-hyphens",
+ "-moz-hyphens",
+ "-ms-hyphens",
+ "-moz-image-region",
+ "-ms-ime-align",
+ "-webkit-initial-letter",
+ "-ms-interpolation-mode",
+ "justify-self",
+ "-webkit-justify-self",
+ "-webkit-line-align",
+ "-webkit-line-box-contain",
+ "-webkit-line-box-contain",
+ "-webkit-line-break",
+ "-webkit-line-break",
+ "line-break",
+ "-webkit-line-clamp",
+ "-webkit-line-clamp",
+ "-webkit-line-grid",
+ "-webkit-line-snap",
+ "-webkit-locale",
+ "-webkit-locale",
+ "-webkit-logical-height",
+ "-webkit-logical-height",
+ "-webkit-logical-width",
+ "-webkit-logical-width",
+ "-webkit-margin-after",
+ "-webkit-margin-after",
+ "-webkit-margin-after-collapse",
+ "-webkit-margin-after-collapse",
+ "-webkit-margin-before",
+ "-webkit-margin-before",
+ "-webkit-margin-before-collapse",
+ "-webkit-margin-before-collapse",
+ "-webkit-margin-bottom-collapse",
+ "-webkit-margin-bottom-collapse",
+ "-webkit-margin-collapse",
+ "-webkit-margin-collapse",
+ "-webkit-margin-end",
+ "-webkit-margin-end",
+ "-moz-margin-end",
+ "-webkit-margin-start",
+ "-webkit-margin-start",
+ "-moz-margin-start",
+ "-webkit-margin-top-collapse",
+ "-webkit-margin-top-collapse",
+ "-webkit-marquee",
+ "-webkit-marquee-direction",
+ "-webkit-marquee-increment",
+ "-webkit-marquee-repetition",
+ "-webkit-marquee-speed",
+ "-webkit-marquee-style",
+ "mask",
+ "-webkit-mask",
+ "mask",
+ "-webkit-mask-box-image",
+ "-webkit-mask-box-image",
+ "-webkit-mask-box-image-outset",
+ "-webkit-mask-box-image-outset",
+ "-webkit-mask-box-image-repeat",
+ "-webkit-mask-box-image-repeat",
+ "-webkit-mask-box-image-slice",
+ "-webkit-mask-box-image-slice",
+ "-webkit-mask-box-image-source",
+ "-webkit-mask-box-image-source",
+ "-webkit-mask-box-image-width",
+ "-webkit-mask-box-image-width",
+ "-webkit-mask-clip",
+ "-webkit-mask-clip",
+ "-webkit-mask-composite",
+ "-webkit-mask-composite",
+ "-webkit-mask-image",
+ "-webkit-mask-image",
+ "-webkit-mask-origin",
+ "-webkit-mask-origin",
+ "-webkit-mask-position",
+ "-webkit-mask-position",
+ "-webkit-mask-position-x",
+ "-webkit-mask-position-x",
+ "-webkit-mask-position-y",
+ "-webkit-mask-position-y",
+ "-webkit-mask-repeat",
+ "-webkit-mask-repeat",
+ "-webkit-mask-repeat-x",
+ "-webkit-mask-repeat-x",
+ "-webkit-mask-repeat-y",
+ "-webkit-mask-repeat-y",
+ "-webkit-mask-size",
+ "-webkit-mask-size",
+ "mask-source-type",
+ "-webkit-mask-source-type",
+ "-moz-math-display",
+ "-moz-math-variant",
+ "-webkit-max-logical-height",
+ "-webkit-max-logical-height",
+ "-webkit-max-logical-width",
+ "-webkit-max-logical-width",
+ "-webkit-min-logical-height",
+ "-webkit-min-logical-height",
+ "-webkit-min-logical-width",
+ "-webkit-min-logical-width",
+ "-webkit-nbsp-mode",
+ "-moz-orient",
+ "-moz-osx-font-smoothing",
+ "-moz-outline-radius",
+ "-moz-outline-radius-bottomleft",
+ "-moz-outline-radius-bottomright",
+ "-moz-outline-radius-topleft",
+ "-moz-outline-radius-topright",
+ "-webkit-overflow-scrolling",
+ "-ms-overflow-style",
+ "-webkit-padding-after",
+ "-webkit-padding-after",
+ "-webkit-padding-before",
+ "-webkit-padding-before",
+ "-webkit-padding-end",
+ "-webkit-padding-end",
+ "-moz-padding-end",
+ "-webkit-padding-start",
+ "-webkit-padding-start",
+ "-moz-padding-start",
+ "perspective",
+ "-webkit-perspective",
+ "perspective",
+ "perspective",
+ "perspective-origin",
+ "-webkit-perspective-origin",
+ "perspective-origin",
+ "perspective-origin",
+ "-webkit-perspective-origin-x",
+ "-webkit-perspective-origin-x",
+ "perspective-origin-x",
+ "-webkit-perspective-origin-y",
+ "-webkit-perspective-origin-y",
+ "perspective-origin-y",
+ "-webkit-print-color-adjust",
+ "-webkit-print-color-adjust",
+ "-webkit-region-break-after",
+ "-webkit-region-break-before",
+ "-webkit-region-break-inside",
+ "-webkit-region-fragment",
+ "-webkit-rtl-ordering",
+ "-webkit-rtl-ordering",
+ "-webkit-ruby-position",
+ "-webkit-ruby-position",
+ "ruby-position",
+ "-moz-script-level",
+ "-moz-script-min-size",
+ "-moz-script-size-multiplier",
+ "-ms-scroll-chaining",
+ "-ms-scroll-limit",
+ "-ms-scroll-limit-x-max",
+ "-ms-scroll-limit-x-min",
+ "-ms-scroll-limit-y-max",
+ "-ms-scroll-limit-y-min",
+ "-ms-scroll-rails",
+ "-webkit-scroll-snap-coordinate",
+ "-webkit-scroll-snap-destination",
+ "-webkit-scroll-snap-points-x",
+ "-ms-scroll-snap-points-x",
+ "-webkit-scroll-snap-points-y",
+ "-ms-scroll-snap-points-y",
+ "-webkit-scroll-snap-type",
+ "-ms-scroll-snap-type",
+ "-ms-scroll-snap-x",
+ "-ms-scroll-snap-y",
+ "-ms-scroll-translation",
+ "-ms-scrollbar-3dlight-color",
+ "shape-image-threshold",
+ "-webkit-shape-image-threshold",
+ "shape-margin",
+ "-webkit-shape-margin",
+ "shape-outside",
+ "-webkit-shape-outside",
+ "-moz-stack-sizing",
+ "tab-size",
+ "tab-size",
+ "-moz-tab-size",
+ "-webkit-tap-highlight-color",
+ "-webkit-tap-highlight-color",
+ "text-align-last",
+ "-webkit-text-align-last",
+ "-moz-text-align-last",
+ "text-align-last",
+ "-webkit-text-combine",
+ "-webkit-text-combine",
+ "-ms-text-combine-horizontal",
+ "text-decoration-color",
+ "-webkit-text-decoration-color",
+ "text-decoration-color",
+ "text-decoration-color",
+ "text-decoration-line",
+ "-webkit-text-decoration-line",
+ "text-decoration-line",
+ "-webkit-text-decoration-skip",
+ "text-decoration-style",
+ "-webkit-text-decoration-style",
+ "text-decoration-style",
+ "-webkit-text-decorations-in-effect",
+ "-webkit-text-decorations-in-effect",
+ "-webkit-text-emphasis",
+ "text-emphasis",
+ "-webkit-text-emphasis-color",
+ "text-emphasis-color",
+ "-webkit-text-emphasis-position",
+ "text-emphasis-position",
+ "-webkit-text-emphasis-style",
+ "text-emphasis-style",
+ "-webkit-text-fill-color",
+ "-webkit-text-fill-color",
+ "text-justify",
+ "-webkit-text-justify",
+ "text-justify",
+ "-webkit-text-orientation",
+ "-webkit-text-orientation",
+ "text-orientation",
+ "-webkit-text-security",
+ "-webkit-text-security",
+ "-webkit-text-size-adjust",
+ "-moz-text-size-adjust",
+ "-ms-text-size-adjust",
+ "-webkit-text-stroke",
+ "-webkit-text-stroke",
+ "-webkit-text-stroke-color",
+ "-webkit-text-stroke-color",
+ "-webkit-text-stroke-width",
+ "-webkit-text-stroke-width",
+ "text-underline-position",
+ "-webkit-text-underline-position",
+ "text-underline-position",
+ "-webkit-touch-callout",
+ "-ms-touch-select",
+ "transform",
+ "-webkit-transform",
+ "transform",
+ "transform",
+ "transform-origin",
+ "-webkit-transform-origin",
+ "transform-origin",
+ "transform-origin",
+ "-webkit-transform-origin-x",
+ "-webkit-transform-origin-x",
+ "transform-origin-x",
+ "-webkit-transform-origin-y",
+ "-webkit-transform-origin-y",
+ "transform-origin-y",
+ "-webkit-transform-origin-z",
+ "-webkit-transform-origin-z",
+ "transform-origin-z",
+ "transform-style",
+ "-webkit-transform-style",
+ "transform-style",
+ "transform-style",
+ "-webkit-user-drag",
+ "-webkit-user-drag",
+ "-moz-user-focus",
+ "-moz-user-input",
+ "-webkit-user-modify",
+ "-webkit-user-modify",
+ "-moz-user-modify",
+ "-webkit-user-select",
+ "-webkit-user-select",
+ "-moz-user-select",
+ "-ms-user-select",
+ "-moz-window-dragging",
+ "-moz-window-shadow",
+ "-ms-wrap-flow",
+ "-ms-wrap-margin",
+ "-ms-wrap-through",
+ "writing-mode",
+ "-webkit-writing-mode",
+ "writing-mode",
+ "writing-mode",
]
all_styles = standard_styles + all_prefixed_styles
mathml_tags = [
- 'abs', 'and', 'annotation', 'annotation-xml', 'apply', 'approx', 'arccos', 'arccosh', 'arccot', 'arccoth', 'arccsc',
- 'arccsch', 'arcsec', 'arcsech', 'arcsin', 'arcsinh', 'arctan', 'arctanh', 'arg', 'bind', 'bvar', 'card',
- 'cartesianproduct', 'cbytes', 'ceiling', 'cerror', 'ci', 'cn', 'codomain', 'complexes', 'compose', 'condition',
- 'conjugate', 'cos', 'cosh', 'cot', 'coth', 'cs', 'csc', 'csch', 'csymbol', 'curl', 'declare', 'degree',
- 'determinant', 'diff', 'divergence', 'divide', 'domain', 'domainofapplication', 'emptyset', 'encoding', 'eq',
- 'equivalent', 'eulergamma', 'exists', 'exp', 'exponentiale', 'factorial', 'factorof', 'false', 'floor', 'fn',
- 'forall', 'function', 'gcd', 'geq', 'grad', 'gt', 'ident', 'image', 'imaginary', 'imaginaryi', 'implies', 'in',
- 'infinity', 'int', 'integers', 'intersect', 'interval', 'inverse', 'lambda', 'laplacian', 'lcm', 'leq', 'limit',
- 'list', 'ln', 'log', 'logbase', 'lowlimit', 'lt', 'maction', 'malign', 'maligngroup', 'malignmark', 'malignscope',
- 'math', 'matrix', 'matrixrow', 'max', 'mean', 'median', 'menclose', 'merror', 'mfenced', 'mfrac', 'mfraction',
- 'mglyph', 'mi', 'min', 'minus', 'mlabeledtr', 'mlongdiv', 'mmultiscripts', 'mn', 'mo', 'mode', 'moment',
- 'momentabout', 'mover', 'mpadded', 'mphantom', 'mprescripts', 'mroot', 'mrow', 'ms', 'mscarries', 'mscarry',
- 'msgroup', 'msline', 'mspace', 'msqrt', 'msrow', 'mstack', 'mstyle', 'msub', 'msubsup', 'msup', 'mtable', 'mtd',
- 'mtext', 'mtr', 'munder', 'munderover', 'naturalnumbers', 'neq', 'none', 'not', 'notanumber', 'notin',
- 'notprsubset', 'notsubset', 'or', 'otherwise', 'outerproduct', 'partialdiff', 'pi', 'piece', 'piecewice',
- 'piecewise', 'plus', 'power', 'primes', 'product', 'prsubset', 'quotient', 'rationals', 'real', 'reals', 'reln',
- 'rem', 'root', 'scalarproduct', 'sdev', 'sec', 'sech', 'select', 'selector', 'semantics', 'sep', 'set', 'setdiff',
- 'share', 'sin', 'sinh', 'span', 'subset', 'sum', 'tan', 'tanh', 'tendsto', 'times', 'transpose', 'true', 'union',
- 'uplimit', 'var', 'variance', 'vector', 'vectorproduct', 'xor',
+ "abs",
+ "and",
+ "annotation",
+ "annotation-xml",
+ "apply",
+ "approx",
+ "arccos",
+ "arccosh",
+ "arccot",
+ "arccoth",
+ "arccsc",
+ "arccsch",
+ "arcsec",
+ "arcsech",
+ "arcsin",
+ "arcsinh",
+ "arctan",
+ "arctanh",
+ "arg",
+ "bind",
+ "bvar",
+ "card",
+ "cartesianproduct",
+ "cbytes",
+ "ceiling",
+ "cerror",
+ "ci",
+ "cn",
+ "codomain",
+ "complexes",
+ "compose",
+ "condition",
+ "conjugate",
+ "cos",
+ "cosh",
+ "cot",
+ "coth",
+ "cs",
+ "csc",
+ "csch",
+ "csymbol",
+ "curl",
+ "declare",
+ "degree",
+ "determinant",
+ "diff",
+ "divergence",
+ "divide",
+ "domain",
+ "domainofapplication",
+ "emptyset",
+ "encoding",
+ "eq",
+ "equivalent",
+ "eulergamma",
+ "exists",
+ "exp",
+ "exponentiale",
+ "factorial",
+ "factorof",
+ "false",
+ "floor",
+ "fn",
+ "forall",
+ "function",
+ "gcd",
+ "geq",
+ "grad",
+ "gt",
+ "ident",
+ "image",
+ "imaginary",
+ "imaginaryi",
+ "implies",
+ "in",
+ "infinity",
+ "int",
+ "integers",
+ "intersect",
+ "interval",
+ "inverse",
+ "lambda",
+ "laplacian",
+ "lcm",
+ "leq",
+ "limit",
+ "list",
+ "ln",
+ "log",
+ "logbase",
+ "lowlimit",
+ "lt",
+ "maction",
+ "malign",
+ "maligngroup",
+ "malignmark",
+ "malignscope",
+ "math",
+ "matrix",
+ "matrixrow",
+ "max",
+ "mean",
+ "median",
+ "menclose",
+ "merror",
+ "mfenced",
+ "mfrac",
+ "mfraction",
+ "mglyph",
+ "mi",
+ "min",
+ "minus",
+ "mlabeledtr",
+ "mlongdiv",
+ "mmultiscripts",
+ "mn",
+ "mo",
+ "mode",
+ "moment",
+ "momentabout",
+ "mover",
+ "mpadded",
+ "mphantom",
+ "mprescripts",
+ "mroot",
+ "mrow",
+ "ms",
+ "mscarries",
+ "mscarry",
+ "msgroup",
+ "msline",
+ "mspace",
+ "msqrt",
+ "msrow",
+ "mstack",
+ "mstyle",
+ "msub",
+ "msubsup",
+ "msup",
+ "mtable",
+ "mtd",
+ "mtext",
+ "mtr",
+ "munder",
+ "munderover",
+ "naturalnumbers",
+ "neq",
+ "none",
+ "not",
+ "notanumber",
+ "notin",
+ "notprsubset",
+ "notsubset",
+ "or",
+ "otherwise",
+ "outerproduct",
+ "partialdiff",
+ "pi",
+ "piece",
+ "piecewice",
+ "piecewise",
+ "plus",
+ "power",
+ "primes",
+ "product",
+ "prsubset",
+ "quotient",
+ "rationals",
+ "real",
+ "reals",
+ "reln",
+ "rem",
+ "root",
+ "scalarproduct",
+ "sdev",
+ "sec",
+ "sech",
+ "select",
+ "selector",
+ "semantics",
+ "sep",
+ "set",
+ "setdiff",
+ "share",
+ "sin",
+ "sinh",
+ "span",
+ "subset",
+ "sum",
+ "tan",
+ "tanh",
+ "tendsto",
+ "times",
+ "transpose",
+ "true",
+ "union",
+ "uplimit",
+ "var",
+ "variance",
+ "vector",
+ "vectorproduct",
+ "xor",
]
mathml_attrs = {
- 'mo': ['accent', 'dir', 'fence', 'form', 'indentalign', 'indentalignfirst', 'indentalignlast', 'indentshift',
- 'indentshiftfirst', 'indentshiftlast', 'indenttarget', 'largeop', 'linebreak', 'linebreakmultchar',
- 'linebreakstyle', 'lineleading', 'lspace', 'mathsize', 'mathvariant', 'maxsize', 'minsize', 'movablelimits',
- 'rspace', 'separator', 'stretchy', 'symmetric', 'href', 'id', 'mathbackground', 'mathcolor'],
- 'mover': ['accent', 'align', 'href', 'id', 'mathbackground', 'mathcolor'],
- 'munderover': ['accent', 'accentunder', 'align', 'href', 'id', 'mathbackground', 'mathcolor'],
- 'munder': ['accentunder', 'align', 'href', 'id', 'mathbackground', 'mathcolor'],
- 'maction': ['actiontype', 'selection', 'href', 'id', 'mathbackground', 'mathcolor'],
- 'mtable': ['align', 'alignmentscope', 'columnalign', 'columnlines', 'columnspacing', 'columnwidth', 'displaystyle',
- 'equalcolumns', 'equalrows', 'frame', 'framespacing', 'groupalign', 'minlabelspacing', 'rowalign',
- 'rowlines', 'rowspacing', 'side', 'width', 'href', 'id', 'mathbackground', 'mathcolor'],
- 'mstack': ['align', 'charalign', 'stackalign', 'href', 'id', 'mathbackground', 'mathcolor'],
- 'math': ['altimg', 'altimg-width', 'altimg-height', 'altimg-valign', 'alttext', 'dir', 'display', 'overflow',
- 'xmlns', 'href', 'id', 'mathbackground', 'mathcolor'],
- 'mfrac': ['bevelled', 'denomalign', 'linethickness', 'numalign', 'href', 'id', 'mathbackground', 'mathcolor'],
- 'mfenced': ['close', 'open', 'separators', 'href', 'id', 'mathbackground', 'mathcolor'],
- 'mtd': ['columnalign', 'columnspan', 'groupalign', 'rowalign', 'rowspan', 'href', 'id', 'mathbackground',
- 'mathcolor'],
- 'mtr': ['columnalign', 'groupalign', 'rowalign', 'href', 'id', 'mathbackground', 'mathcolor'],
- 'mlabeledtr': ['columnalign', 'href', 'id', 'mathbackground', 'mathcolor'],
- 'mscarry': ['crossout', 'href', 'id', 'mathbackground', 'mathcolor'],
- 'mstyle': ['decimalpoint', 'displaystyle', 'infixlinebreakstyle', 'scriptlevel', 'scriptminsize',
- 'scriptsizemultiplier', 'href', 'id', 'mathbackground', 'mathcolor'],
- 'mpadded': ['depth', 'height', 'lspace', 'voffset', 'width', 'href', 'id', 'mathbackground', 'mathcolor'],
- 'mi': ['dir', 'mathsize', 'mathvariant', 'href', 'id', 'mathbackground', 'mathcolor'],
- 'mrow': ['dir', 'href', 'id', 'mathbackground', 'mathcolor'],
- 'ms': ['dir', 'lquote', 'mathsize', 'mathvariant', 'rquote', 'href', 'id', 'mathbackground', 'mathcolor'],
- 'mtext': ['dir', 'mathsize', 'mathvariant', 'href', 'id', 'mathbackground', 'mathcolor'],
- 'malignmark': ['edge', 'href', 'id', 'mathbackground', 'mathcolor'],
- 'maligngroup': ['groupalign', 'href', 'id', 'mathbackground', 'mathcolor'],
- 'mglyph': ['height', 'src', 'width', 'href', 'id', 'mathbackground', 'mathcolor'],
- 'mspace': ['height', 'indentalign', 'indentalignfirst', 'indentalignlast', 'indentshift', 'indentshiftfirst',
- 'indentshiftlast', 'indenttarget', 'linebreak', 'linebreakmultchar', 'linebreakstyle', 'lineleading',
- 'width', 'href', 'id', 'mathbackground', 'mathcolor'],
- 'msline': ['length', 'position', 'href', 'id', 'mathbackground', 'mathcolor'],
- 'mscarries': ['location', 'position', 'href', 'id', 'mathbackground', 'mathcolor'],
- 'mlongdiv': ['longdivstyle', 'href', 'id', 'mathbackground', 'mathcolor'],
- 'mn': ['mathsize', 'mathvariant', 'href', 'id', 'mathbackground', 'mathcolor'],
- 'menclose': ['notation', 'href', 'id', 'mathbackground', 'mathcolor'],
- 'msgroup': ['position', 'shift', 'href', 'id', 'mathbackground', 'mathcolor'],
- 'msrow': ['position', 'href', 'id', 'mathbackground', 'mathcolor'],
- 'mmultiscripts': ['subscriptshift', 'supscriptshift', 'href', 'id', 'mathbackground', 'mathcolor'],
- 'msub': ['subscriptshift', 'href', 'id', 'mathbackground', 'mathcolor'],
- 'msubsup': ['subscriptshift', 'supscriptshift', 'href', 'id', 'mathbackground', 'mathcolor'],
- 'msup': ['supscriptshift', 'href', 'id', 'mathbackground', 'mathcolor'],
- 'abs': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'and': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'annotation': ['href', 'id', 'mathbackground', 'mathcolor', 'encoding'],
- 'annotation-xml': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'apply': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'approx': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'arccos': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'arccosh': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'arccot': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'arccoth': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'arccsc': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'arccsch': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'arcsec': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'arcsech': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'arcsin': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'arcsinh': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'arctan': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'arctanh': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'arg': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'bind': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'bvar': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'card': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'cartesianproduct': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'cbytes': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'ceiling': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'cerror': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'ci': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'cn': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'codomain': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'complexes': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'compose': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'condition': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'conjugate': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'cos': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'cosh': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'cot': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'coth': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'cs': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'csc': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'csch': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'csymbol': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'curl': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'declare': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'degree': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'determinant': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'diff': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'divergence': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'divide': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'domain': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'domainofapplication': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'emptyset': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'encoding': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'eq': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'equivalent': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'eulergamma': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'exists': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'exp': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'exponentiale': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'factorial': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'factorof': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'false': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'floor': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'fn': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'forall': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'function': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'gcd': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'geq': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'grad': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'gt': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'ident': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'image': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'imaginary': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'imaginaryi': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'implies': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'in': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'infinity': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'int': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'integers': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'intersect': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'interval': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'inverse': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'lambda': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'laplacian': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'lcm': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'leq': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'limit': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'list': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'ln': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'log': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'logbase': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'lowlimit': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'lt': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'malign': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'malignscope': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'matrix': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'matrixrow': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'max': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'mean': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'median': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'merror': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'mfraction': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'min': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'minus': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'mode': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'moment': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'momentabout': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'mphantom': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'mprescripts': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'mroot': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'msqrt': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'naturalnumbers': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'neq': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'none': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'not': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'notanumber': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'notin': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'notprsubset': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'notsubset': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'or': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'otherwise': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'outerproduct': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'partialdiff': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'pi': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'piece': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'piecewice': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'piecewise': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'plus': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'power': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'primes': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'product': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'prsubset': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'quotient': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'rationals': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'real': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'reals': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'reln': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'rem': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'root': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'scalarproduct': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'sdev': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'sec': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'sech': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'select': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'selector': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'semantics': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'sep': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'set': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'setdiff': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'share': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'sin': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'sinh': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'span': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'subset': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'sum': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'tan': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'tanh': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'tendsto': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'times': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'transpose': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'true': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'union': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'uplimit': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'var': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'variance': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'vector': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'vectorproduct': ['href', 'id', 'mathbackground', 'mathcolor'],
- 'xor': ['href', 'id', 'mathbackground', 'mathcolor'],
+ "mo": [
+ "accent",
+ "dir",
+ "fence",
+ "form",
+ "indentalign",
+ "indentalignfirst",
+ "indentalignlast",
+ "indentshift",
+ "indentshiftfirst",
+ "indentshiftlast",
+ "indenttarget",
+ "largeop",
+ "linebreak",
+ "linebreakmultchar",
+ "linebreakstyle",
+ "lineleading",
+ "lspace",
+ "mathsize",
+ "mathvariant",
+ "maxsize",
+ "minsize",
+ "movablelimits",
+ "rspace",
+ "separator",
+ "stretchy",
+ "symmetric",
+ "href",
+ "id",
+ "mathbackground",
+ "mathcolor",
+ ],
+ "mover": ["accent", "align", "href", "id", "mathbackground", "mathcolor"],
+ "munderover": [
+ "accent",
+ "accentunder",
+ "align",
+ "href",
+ "id",
+ "mathbackground",
+ "mathcolor",
+ ],
+ "munder": ["accentunder", "align", "href", "id", "mathbackground", "mathcolor"],
+ "maction": ["actiontype", "selection", "href", "id", "mathbackground", "mathcolor"],
+ "mtable": [
+ "align",
+ "alignmentscope",
+ "columnalign",
+ "columnlines",
+ "columnspacing",
+ "columnwidth",
+ "displaystyle",
+ "equalcolumns",
+ "equalrows",
+ "frame",
+ "framespacing",
+ "groupalign",
+ "minlabelspacing",
+ "rowalign",
+ "rowlines",
+ "rowspacing",
+ "side",
+ "width",
+ "href",
+ "id",
+ "mathbackground",
+ "mathcolor",
+ ],
+ "mstack": [
+ "align",
+ "charalign",
+ "stackalign",
+ "href",
+ "id",
+ "mathbackground",
+ "mathcolor",
+ ],
+ "math": [
+ "altimg",
+ "altimg-width",
+ "altimg-height",
+ "altimg-valign",
+ "alttext",
+ "dir",
+ "display",
+ "overflow",
+ "xmlns",
+ "href",
+ "id",
+ "mathbackground",
+ "mathcolor",
+ ],
+ "mfrac": [
+ "bevelled",
+ "denomalign",
+ "linethickness",
+ "numalign",
+ "href",
+ "id",
+ "mathbackground",
+ "mathcolor",
+ ],
+ "mfenced": [
+ "close",
+ "open",
+ "separators",
+ "href",
+ "id",
+ "mathbackground",
+ "mathcolor",
+ ],
+ "mtd": [
+ "columnalign",
+ "columnspan",
+ "groupalign",
+ "rowalign",
+ "rowspan",
+ "href",
+ "id",
+ "mathbackground",
+ "mathcolor",
+ ],
+ "mtr": [
+ "columnalign",
+ "groupalign",
+ "rowalign",
+ "href",
+ "id",
+ "mathbackground",
+ "mathcolor",
+ ],
+ "mlabeledtr": ["columnalign", "href", "id", "mathbackground", "mathcolor"],
+ "mscarry": ["crossout", "href", "id", "mathbackground", "mathcolor"],
+ "mstyle": [
+ "decimalpoint",
+ "displaystyle",
+ "infixlinebreakstyle",
+ "scriptlevel",
+ "scriptminsize",
+ "scriptsizemultiplier",
+ "href",
+ "id",
+ "mathbackground",
+ "mathcolor",
+ ],
+ "mpadded": [
+ "depth",
+ "height",
+ "lspace",
+ "voffset",
+ "width",
+ "href",
+ "id",
+ "mathbackground",
+ "mathcolor",
+ ],
+ "mi": [
+ "dir",
+ "mathsize",
+ "mathvariant",
+ "href",
+ "id",
+ "mathbackground",
+ "mathcolor",
+ ],
+ "mrow": ["dir", "href", "id", "mathbackground", "mathcolor"],
+ "ms": [
+ "dir",
+ "lquote",
+ "mathsize",
+ "mathvariant",
+ "rquote",
+ "href",
+ "id",
+ "mathbackground",
+ "mathcolor",
+ ],
+ "mtext": [
+ "dir",
+ "mathsize",
+ "mathvariant",
+ "href",
+ "id",
+ "mathbackground",
+ "mathcolor",
+ ],
+ "malignmark": ["edge", "href", "id", "mathbackground", "mathcolor"],
+ "maligngroup": ["groupalign", "href", "id", "mathbackground", "mathcolor"],
+ "mglyph": ["height", "src", "width", "href", "id", "mathbackground", "mathcolor"],
+ "mspace": [
+ "height",
+ "indentalign",
+ "indentalignfirst",
+ "indentalignlast",
+ "indentshift",
+ "indentshiftfirst",
+ "indentshiftlast",
+ "indenttarget",
+ "linebreak",
+ "linebreakmultchar",
+ "linebreakstyle",
+ "lineleading",
+ "width",
+ "href",
+ "id",
+ "mathbackground",
+ "mathcolor",
+ ],
+ "msline": ["length", "position", "href", "id", "mathbackground", "mathcolor"],
+ "mscarries": ["location", "position", "href", "id", "mathbackground", "mathcolor"],
+ "mlongdiv": ["longdivstyle", "href", "id", "mathbackground", "mathcolor"],
+ "mn": ["mathsize", "mathvariant", "href", "id", "mathbackground", "mathcolor"],
+ "menclose": ["notation", "href", "id", "mathbackground", "mathcolor"],
+ "msgroup": ["position", "shift", "href", "id", "mathbackground", "mathcolor"],
+ "msrow": ["position", "href", "id", "mathbackground", "mathcolor"],
+ "mmultiscripts": [
+ "subscriptshift",
+ "supscriptshift",
+ "href",
+ "id",
+ "mathbackground",
+ "mathcolor",
+ ],
+ "msub": ["subscriptshift", "href", "id", "mathbackground", "mathcolor"],
+ "msubsup": [
+ "subscriptshift",
+ "supscriptshift",
+ "href",
+ "id",
+ "mathbackground",
+ "mathcolor",
+ ],
+ "msup": ["supscriptshift", "href", "id", "mathbackground", "mathcolor"],
+ "abs": ["href", "id", "mathbackground", "mathcolor"],
+ "and": ["href", "id", "mathbackground", "mathcolor"],
+ "annotation": ["href", "id", "mathbackground", "mathcolor", "encoding"],
+ "annotation-xml": ["href", "id", "mathbackground", "mathcolor"],
+ "apply": ["href", "id", "mathbackground", "mathcolor"],
+ "approx": ["href", "id", "mathbackground", "mathcolor"],
+ "arccos": ["href", "id", "mathbackground", "mathcolor"],
+ "arccosh": ["href", "id", "mathbackground", "mathcolor"],
+ "arccot": ["href", "id", "mathbackground", "mathcolor"],
+ "arccoth": ["href", "id", "mathbackground", "mathcolor"],
+ "arccsc": ["href", "id", "mathbackground", "mathcolor"],
+ "arccsch": ["href", "id", "mathbackground", "mathcolor"],
+ "arcsec": ["href", "id", "mathbackground", "mathcolor"],
+ "arcsech": ["href", "id", "mathbackground", "mathcolor"],
+ "arcsin": ["href", "id", "mathbackground", "mathcolor"],
+ "arcsinh": ["href", "id", "mathbackground", "mathcolor"],
+ "arctan": ["href", "id", "mathbackground", "mathcolor"],
+ "arctanh": ["href", "id", "mathbackground", "mathcolor"],
+ "arg": ["href", "id", "mathbackground", "mathcolor"],
+ "bind": ["href", "id", "mathbackground", "mathcolor"],
+ "bvar": ["href", "id", "mathbackground", "mathcolor"],
+ "card": ["href", "id", "mathbackground", "mathcolor"],
+ "cartesianproduct": ["href", "id", "mathbackground", "mathcolor"],
+ "cbytes": ["href", "id", "mathbackground", "mathcolor"],
+ "ceiling": ["href", "id", "mathbackground", "mathcolor"],
+ "cerror": ["href", "id", "mathbackground", "mathcolor"],
+ "ci": ["href", "id", "mathbackground", "mathcolor"],
+ "cn": ["href", "id", "mathbackground", "mathcolor"],
+ "codomain": ["href", "id", "mathbackground", "mathcolor"],
+ "complexes": ["href", "id", "mathbackground", "mathcolor"],
+ "compose": ["href", "id", "mathbackground", "mathcolor"],
+ "condition": ["href", "id", "mathbackground", "mathcolor"],
+ "conjugate": ["href", "id", "mathbackground", "mathcolor"],
+ "cos": ["href", "id", "mathbackground", "mathcolor"],
+ "cosh": ["href", "id", "mathbackground", "mathcolor"],
+ "cot": ["href", "id", "mathbackground", "mathcolor"],
+ "coth": ["href", "id", "mathbackground", "mathcolor"],
+ "cs": ["href", "id", "mathbackground", "mathcolor"],
+ "csc": ["href", "id", "mathbackground", "mathcolor"],
+ "csch": ["href", "id", "mathbackground", "mathcolor"],
+ "csymbol": ["href", "id", "mathbackground", "mathcolor"],
+ "curl": ["href", "id", "mathbackground", "mathcolor"],
+ "declare": ["href", "id", "mathbackground", "mathcolor"],
+ "degree": ["href", "id", "mathbackground", "mathcolor"],
+ "determinant": ["href", "id", "mathbackground", "mathcolor"],
+ "diff": ["href", "id", "mathbackground", "mathcolor"],
+ "divergence": ["href", "id", "mathbackground", "mathcolor"],
+ "divide": ["href", "id", "mathbackground", "mathcolor"],
+ "domain": ["href", "id", "mathbackground", "mathcolor"],
+ "domainofapplication": ["href", "id", "mathbackground", "mathcolor"],
+ "emptyset": ["href", "id", "mathbackground", "mathcolor"],
+ "encoding": ["href", "id", "mathbackground", "mathcolor"],
+ "eq": ["href", "id", "mathbackground", "mathcolor"],
+ "equivalent": ["href", "id", "mathbackground", "mathcolor"],
+ "eulergamma": ["href", "id", "mathbackground", "mathcolor"],
+ "exists": ["href", "id", "mathbackground", "mathcolor"],
+ "exp": ["href", "id", "mathbackground", "mathcolor"],
+ "exponentiale": ["href", "id", "mathbackground", "mathcolor"],
+ "factorial": ["href", "id", "mathbackground", "mathcolor"],
+ "factorof": ["href", "id", "mathbackground", "mathcolor"],
+ "false": ["href", "id", "mathbackground", "mathcolor"],
+ "floor": ["href", "id", "mathbackground", "mathcolor"],
+ "fn": ["href", "id", "mathbackground", "mathcolor"],
+ "forall": ["href", "id", "mathbackground", "mathcolor"],
+ "function": ["href", "id", "mathbackground", "mathcolor"],
+ "gcd": ["href", "id", "mathbackground", "mathcolor"],
+ "geq": ["href", "id", "mathbackground", "mathcolor"],
+ "grad": ["href", "id", "mathbackground", "mathcolor"],
+ "gt": ["href", "id", "mathbackground", "mathcolor"],
+ "ident": ["href", "id", "mathbackground", "mathcolor"],
+ "image": ["href", "id", "mathbackground", "mathcolor"],
+ "imaginary": ["href", "id", "mathbackground", "mathcolor"],
+ "imaginaryi": ["href", "id", "mathbackground", "mathcolor"],
+ "implies": ["href", "id", "mathbackground", "mathcolor"],
+ "in": ["href", "id", "mathbackground", "mathcolor"],
+ "infinity": ["href", "id", "mathbackground", "mathcolor"],
+ "int": ["href", "id", "mathbackground", "mathcolor"],
+ "integers": ["href", "id", "mathbackground", "mathcolor"],
+ "intersect": ["href", "id", "mathbackground", "mathcolor"],
+ "interval": ["href", "id", "mathbackground", "mathcolor"],
+ "inverse": ["href", "id", "mathbackground", "mathcolor"],
+ "lambda": ["href", "id", "mathbackground", "mathcolor"],
+ "laplacian": ["href", "id", "mathbackground", "mathcolor"],
+ "lcm": ["href", "id", "mathbackground", "mathcolor"],
+ "leq": ["href", "id", "mathbackground", "mathcolor"],
+ "limit": ["href", "id", "mathbackground", "mathcolor"],
+ "list": ["href", "id", "mathbackground", "mathcolor"],
+ "ln": ["href", "id", "mathbackground", "mathcolor"],
+ "log": ["href", "id", "mathbackground", "mathcolor"],
+ "logbase": ["href", "id", "mathbackground", "mathcolor"],
+ "lowlimit": ["href", "id", "mathbackground", "mathcolor"],
+ "lt": ["href", "id", "mathbackground", "mathcolor"],
+ "malign": ["href", "id", "mathbackground", "mathcolor"],
+ "malignscope": ["href", "id", "mathbackground", "mathcolor"],
+ "matrix": ["href", "id", "mathbackground", "mathcolor"],
+ "matrixrow": ["href", "id", "mathbackground", "mathcolor"],
+ "max": ["href", "id", "mathbackground", "mathcolor"],
+ "mean": ["href", "id", "mathbackground", "mathcolor"],
+ "median": ["href", "id", "mathbackground", "mathcolor"],
+ "merror": ["href", "id", "mathbackground", "mathcolor"],
+ "mfraction": ["href", "id", "mathbackground", "mathcolor"],
+ "min": ["href", "id", "mathbackground", "mathcolor"],
+ "minus": ["href", "id", "mathbackground", "mathcolor"],
+ "mode": ["href", "id", "mathbackground", "mathcolor"],
+ "moment": ["href", "id", "mathbackground", "mathcolor"],
+ "momentabout": ["href", "id", "mathbackground", "mathcolor"],
+ "mphantom": ["href", "id", "mathbackground", "mathcolor"],
+ "mprescripts": ["href", "id", "mathbackground", "mathcolor"],
+ "mroot": ["href", "id", "mathbackground", "mathcolor"],
+ "msqrt": ["href", "id", "mathbackground", "mathcolor"],
+ "naturalnumbers": ["href", "id", "mathbackground", "mathcolor"],
+ "neq": ["href", "id", "mathbackground", "mathcolor"],
+ "none": ["href", "id", "mathbackground", "mathcolor"],
+ "not": ["href", "id", "mathbackground", "mathcolor"],
+ "notanumber": ["href", "id", "mathbackground", "mathcolor"],
+ "notin": ["href", "id", "mathbackground", "mathcolor"],
+ "notprsubset": ["href", "id", "mathbackground", "mathcolor"],
+ "notsubset": ["href", "id", "mathbackground", "mathcolor"],
+ "or": ["href", "id", "mathbackground", "mathcolor"],
+ "otherwise": ["href", "id", "mathbackground", "mathcolor"],
+ "outerproduct": ["href", "id", "mathbackground", "mathcolor"],
+ "partialdiff": ["href", "id", "mathbackground", "mathcolor"],
+ "pi": ["href", "id", "mathbackground", "mathcolor"],
+ "piece": ["href", "id", "mathbackground", "mathcolor"],
+ "piecewice": ["href", "id", "mathbackground", "mathcolor"],
+ "piecewise": ["href", "id", "mathbackground", "mathcolor"],
+ "plus": ["href", "id", "mathbackground", "mathcolor"],
+ "power": ["href", "id", "mathbackground", "mathcolor"],
+ "primes": ["href", "id", "mathbackground", "mathcolor"],
+ "product": ["href", "id", "mathbackground", "mathcolor"],
+ "prsubset": ["href", "id", "mathbackground", "mathcolor"],
+ "quotient": ["href", "id", "mathbackground", "mathcolor"],
+ "rationals": ["href", "id", "mathbackground", "mathcolor"],
+ "real": ["href", "id", "mathbackground", "mathcolor"],
+ "reals": ["href", "id", "mathbackground", "mathcolor"],
+ "reln": ["href", "id", "mathbackground", "mathcolor"],
+ "rem": ["href", "id", "mathbackground", "mathcolor"],
+ "root": ["href", "id", "mathbackground", "mathcolor"],
+ "scalarproduct": ["href", "id", "mathbackground", "mathcolor"],
+ "sdev": ["href", "id", "mathbackground", "mathcolor"],
+ "sec": ["href", "id", "mathbackground", "mathcolor"],
+ "sech": ["href", "id", "mathbackground", "mathcolor"],
+ "select": ["href", "id", "mathbackground", "mathcolor"],
+ "selector": ["href", "id", "mathbackground", "mathcolor"],
+ "semantics": ["href", "id", "mathbackground", "mathcolor"],
+ "sep": ["href", "id", "mathbackground", "mathcolor"],
+ "set": ["href", "id", "mathbackground", "mathcolor"],
+ "setdiff": ["href", "id", "mathbackground", "mathcolor"],
+ "share": ["href", "id", "mathbackground", "mathcolor"],
+ "sin": ["href", "id", "mathbackground", "mathcolor"],
+ "sinh": ["href", "id", "mathbackground", "mathcolor"],
+ "span": ["href", "id", "mathbackground", "mathcolor"],
+ "subset": ["href", "id", "mathbackground", "mathcolor"],
+ "sum": ["href", "id", "mathbackground", "mathcolor"],
+ "tan": ["href", "id", "mathbackground", "mathcolor"],
+ "tanh": ["href", "id", "mathbackground", "mathcolor"],
+ "tendsto": ["href", "id", "mathbackground", "mathcolor"],
+ "times": ["href", "id", "mathbackground", "mathcolor"],
+ "transpose": ["href", "id", "mathbackground", "mathcolor"],
+ "true": ["href", "id", "mathbackground", "mathcolor"],
+ "union": ["href", "id", "mathbackground", "mathcolor"],
+ "uplimit": ["href", "id", "mathbackground", "mathcolor"],
+ "var": ["href", "id", "mathbackground", "mathcolor"],
+ "variance": ["href", "id", "mathbackground", "mathcolor"],
+ "vector": ["href", "id", "mathbackground", "mathcolor"],
+ "vectorproduct": ["href", "id", "mathbackground", "mathcolor"],
+ "xor": ["href", "id", "mathbackground", "mathcolor"],
}
diff --git a/judge/jinja2/markdown/lazy_load.py b/judge/jinja2/markdown/lazy_load.py
index cf9849cbc4..56f5347678 100644
--- a/judge/jinja2/markdown/lazy_load.py
+++ b/judge/jinja2/markdown/lazy_load.py
@@ -5,16 +5,16 @@
def lazy_load(tree):
- blank = static('blank.gif')
- for img in tree.xpath('.//img'):
- src = img.get('src', '')
- if src.startswith('data') or '-math' in img.get('class', ''):
+ blank = static("blank.gif")
+ for img in tree.xpath(".//img"):
+ src = img.get("src", "")
+ if src.startswith("data") or "-math" in img.get("class", ""):
continue
- noscript = html.Element('noscript')
+ noscript = html.Element("noscript")
copy = deepcopy(img)
- copy.tail = ''
+ copy.tail = ""
noscript.append(copy)
img.addprevious(noscript)
- img.set('data-src', src)
- img.set('src', blank)
- img.set('class', img.get('class') + ' unveil' if img.get('class') else 'unveil')
+ img.set("data-src", src)
+ img.set("src", blank)
+ img.set("class", img.get("class") + " unveil" if img.get("class") else "unveil")
diff --git a/judge/jinja2/markdown/math.py b/judge/jinja2/markdown/math.py
index d619d5bd83..2a490779f9 100644
--- a/judge/jinja2/markdown/math.py
+++ b/judge/jinja2/markdown/math.py
@@ -4,13 +4,13 @@
from judge.utils.mathoid import MathoidMathParser
-mistune._pre_tags.append('latex')
+mistune._pre_tags.append("latex")
class MathInlineGrammar(mistune.InlineGrammar):
- block_math = re.compile(r'^\$\$(.*?)\$\$|^\\\[(.*?)\\\]', re.DOTALL)
- math = re.compile(r'^~(.*?)~|^\\\((.*?)\\\)', re.DOTALL)
- text = re.compile(r'^[\s\S]+?(?=[\\%s%s>' % (tag, extra, text, tag)
+ extra = m.group(2) or ""
+ html = "<%s%s>%s%s>" % (tag, extra, text, tag)
else:
html = m.group(0)
return self.renderer.inline_html(html)
@@ -48,18 +50,18 @@ def output_inline_html(self, m):
class MathRenderer(mistune.Renderer):
def __init__(self, *args, **kwargs):
- if kwargs.pop('math', False):
- self.mathoid = MathoidMathParser(kwargs.pop('math_engine', None) or 'svg')
+ if kwargs.pop("math", False):
+ self.mathoid = MathoidMathParser(kwargs.pop("math_engine", None) or "svg")
else:
self.mathoid = None
super(MathRenderer, self).__init__(*args, **kwargs)
def block_math(self, math):
if self.mathoid is None or not math:
- return r'\[%s\]' % mistune.escape(str(math))
+ return r"\[%s\]" % mistune.escape(str(math))
return self.mathoid.display_math(math)
def math(self, math):
if self.mathoid is None or not math:
- return r'\(%s\)' % mistune.escape(str(math))
+ return r"\(%s\)" % mistune.escape(str(math))
return self.mathoid.inline_math(math)
diff --git a/judge/jinja2/markdown/test_markdown.py b/judge/jinja2/markdown/test_markdown.py
index 689ca54f3e..a02df1c819 100644
--- a/judge/jinja2/markdown/test_markdown.py
+++ b/judge/jinja2/markdown/test_markdown.py
@@ -105,16 +105,24 @@
class TestMarkdown(SimpleTestCase):
- BLEACHED_STYLE = 'problem'
- UNBLEACHED_STYLE = 'problem-full'
+ BLEACHED_STYLE = "problem"
+ UNBLEACHED_STYLE = "problem-full"
def test_bleach(self):
- self.assertHTMLEqual(markdown('', self.BLEACHED_STYLE),
- '<script>void(0)</script>')
- self.assertHTMLEqual(markdown(' ', self.BLEACHED_STYLE),
- '
')
- self.assertHTMLEqual(markdown('', self.BLEACHED_STYLE),
- '')
+ self.assertHTMLEqual(
+ markdown("", self.BLEACHED_STYLE),
+ "<script>void(0)</script>",
+ )
+ self.assertHTMLEqual(
+ markdown(
+ ' ', self.BLEACHED_STYLE
+ ),
+ '
',
+ )
+ self.assertHTMLEqual(
+ markdown("", self.BLEACHED_STYLE),
+ "",
+ )
def test_bleach_mathml(self):
self.assertHTMLEqual(markdown(MATHML_N, self.BLEACHED_STYLE), MATHML_N)
@@ -122,35 +130,39 @@ def test_bleach_mathml(self):
self.assertHTMLEqual(cleaner.clean(MATHML_CHUDNOVSKY), MATHML_CHUDNOVSKY)
def test_no_bleach(self):
- self.assertHTMLEqual(markdown('', self.UNBLEACHED_STYLE),
- '')
+ self.assertHTMLEqual(
+ markdown("", self.UNBLEACHED_STYLE),
+ "",
+ )
def test_post_process(self):
- self.assertHTMLEqual(markdown(' ', self.UNBLEACHED_STYLE, lazy_load=True),
- ' '
- '
')
+ self.assertHTMLEqual(
+ markdown(' ', self.UNBLEACHED_STYLE, lazy_load=True),
+ ' '
+ '
',
+ )
class TestFragmentUtils(SimpleTestCase):
def test_simple(self):
- tree = fragments_to_tree('a
b
')
+ tree = fragments_to_tree("a
b
")
self.assertIsInstance(tree, html.HtmlElement)
self.assertEqual(len(tree.getchildren()), 2)
self.assertIsInstance(tree[0], html.HtmlElement)
- self.assertEqual(tree[0].tag, 'p')
- self.assertEqual(tree[0].text, 'a')
+ self.assertEqual(tree[0].tag, "p")
+ self.assertEqual(tree[0].text, "a")
self.assertIsInstance(tree[1], html.HtmlElement)
- self.assertEqual(tree[1].tag, 'p')
- self.assertEqual(tree[1].text, 'b')
+ self.assertEqual(tree[1].tag, "p")
+ self.assertEqual(tree[1].text, "b")
- self.assertHTMLEqual(fragment_tree_to_str(tree), 'a
b
')
+ self.assertHTMLEqual(fragment_tree_to_str(tree), "a
b
")
def test_text_prefix(self):
- tree = fragments_to_tree('za
b
')
+ tree = fragments_to_tree("za
b
")
self.assertIsInstance(tree, html.HtmlElement)
self.assertEqual(len(tree.getchildren()), 2)
- self.assertEqual(tree.text, 'z')
+ self.assertEqual(tree.text, "z")
- self.assertHTMLEqual(fragment_tree_to_str(tree), 'za
b
')
+ self.assertHTMLEqual(fragment_tree_to_str(tree), "za
b
")
diff --git a/judge/jinja2/rating.py b/judge/jinja2/rating.py
index 3dacea92ce..ee40e984a3 100644
--- a/judge/jinja2/rating.py
+++ b/judge/jinja2/rating.py
@@ -14,22 +14,22 @@ def _get_rating_value(func, obj):
return func(obj.rating)
-@registry.function('rating_class')
+@registry.function("rating_class")
def get_rating_class(obj):
- return _get_rating_value(rating_class, obj) or 'rate-none'
+ return _get_rating_value(rating_class, obj) or "rate-none"
-@registry.function(name='rating_name')
+@registry.function(name="rating_name")
def get_name(obj):
- return _get_rating_value(rating_name, obj) or _('Unrated')
+ return _get_rating_value(rating_name, obj) or _("Unrated")
-@registry.function(name='rating_progress')
+@registry.function(name="rating_progress")
def get_progress(obj):
return _get_rating_value(rating_progress, obj) or 0.0
@registry.function
-@registry.render_with('user/rating.html')
+@registry.render_with("user/rating.html")
def rating_number(obj):
- return {'rating': obj}
+ return {"rating": obj}
diff --git a/judge/jinja2/reference.py b/judge/jinja2/reference.py
index 5dd0c84a12..8d68619023 100644
--- a/judge/jinja2/reference.py
+++ b/judge/jinja2/reference.py
@@ -14,17 +14,17 @@
from judge.ratings import rating_class, rating_progress
from . import registry
-rereference = re.compile(r'\[(r?user):(\w+)\]')
+rereference = re.compile(r"\[(r?user):(\w+)\]")
def get_user(username, data):
if not data:
- element = Element('span', {'class': 'deleted-user'})
+ element = Element("span", {"class": "deleted-user"})
element.text = username
return element
- element = Element('span', {'class': Profile.get_user_css_class(*data)})
- link = Element('a', {'href': reverse('user_page', args=[username])})
+ element = Element("span", {"class": Profile.get_user_css_class(*data)})
+ link = Element("a", {"href": reverse("user_page", args=[username])})
link.text = username
element.append(link)
return element
@@ -32,17 +32,21 @@ def get_user(username, data):
def get_user_rating(username, data):
if not data:
- element = Element('span')
+ element = Element("span")
element.text = username
return element
rating = data[1]
- element = Element('a', {'class': 'rate-group', 'href': reverse('user_page', args=[username])})
+ element = Element(
+ "a", {"class": "rate-group", "href": reverse("user_page", args=[username])}
+ )
if rating:
rating_css = rating_class(rating)
- rate_box = Element('span', {'class': 'rate-box ' + rating_css})
- rate_box.append(Element('span', {'style': 'height: %3.fem' % rating_progress(rating)}))
- user = Element('span', {'class': 'rating ' + rating_css})
+ rate_box = Element("span", {"class": "rate-box " + rating_css})
+ rate_box.append(
+ Element("span", {"style": "height: %3.fem" % rating_progress(rating)})
+ )
+ user = Element("span", {"class": "rating " + rating_css})
user.text = username
element.append(rate_box)
element.append(user)
@@ -52,14 +56,17 @@ def get_user_rating(username, data):
def get_user_info(usernames):
- return {name: (rank, rating) for name, rank, rating in
- Profile.objects.filter(user__username__in=usernames)
- .values_list('user__username', 'display_rank', 'rating')}
+ return {
+ name: (rank, rating)
+ for name, rank, rating in Profile.objects.filter(
+ user__username__in=usernames
+ ).values_list("user__username", "display_rank", "rating")
+ }
reference_map = {
- 'user': (get_user, get_user_info),
- 'ruser': (get_user_rating, get_user_info),
+ "user": (get_user, get_user_info),
+ "ruser": (get_user_rating, get_user_info),
}
@@ -71,9 +78,9 @@ def process_reference(text):
elements = []
for piece in rereference.finditer(text):
if prev is None:
- tail = text[last:piece.start()]
+ tail = text[last : piece.start()]
else:
- prev.append(text[last:piece.start()])
+ prev.append(text[last : piece.start()])
prev = list(piece.groups())
elements.append(prev)
last = piece.end()
@@ -137,7 +144,7 @@ def item_title(item):
return item.name
elif isinstance(item, Contest):
return item.name
- return ''
+ return ""
@registry.function
@@ -146,44 +153,46 @@ def link_user(user):
user, profile = user.user, user
elif isinstance(user, AbstractUser):
profile = user.profile
- elif type(user).__name__ == 'ContestRankingProfile':
+ elif type(user).__name__ == "ContestRankingProfile":
user, profile = user.user, user
else:
- raise ValueError('Expected profile or user, got %s' % (type(user),))
- return mark_safe(f''
- f''
- f'{escape(profile.display_name)} ')
+ raise ValueError("Expected profile or user, got %s" % (type(user),))
+ return mark_safe(
+ f''
+ f''
+ f"{escape(profile.display_name)} "
+ )
@registry.function
-@registry.render_with('user/link-list.html')
+@registry.render_with("user/link-list.html")
def link_users(users):
- return {'users': users}
+ return {"users": users}
@registry.function
-@registry.render_with('runtime-version-fragment.html')
+@registry.render_with("runtime-version-fragment.html")
def runtime_versions(versions):
- return {'runtime_versions': versions}
+ return {"runtime_versions": versions}
-@registry.filter(name='absolutify')
+@registry.filter(name="absolutify")
def absolute_links(text, url):
tree = lxml_tree.fromstring(text)
- for anchor in tree.xpath('.//a'):
- href = anchor.get('href')
+ for anchor in tree.xpath(".//a"):
+ href = anchor.get("href")
if href:
- anchor.set('href', urljoin(url, href))
+ anchor.set("href", urljoin(url, href))
return tree
-@registry.function(name='urljoin')
+@registry.function(name="urljoin")
def join(first, second, *rest):
if not rest:
return urljoin(first, second)
return urljoin(urljoin(first, second), *rest)
-@registry.filter(name='ansi2html')
+@registry.filter(name="ansi2html")
def ansi2html(s):
return mark_safe(Ansi2HTMLConverter(inline=True).convert(s, full=False))
diff --git a/judge/jinja2/registry.py b/judge/jinja2/registry.py
index da121666f9..21d3b859e0 100644
--- a/judge/jinja2/registry.py
+++ b/judge/jinja2/registry.py
@@ -5,7 +5,7 @@
filters = {}
extensions = []
-__all__ = ['render_with', 'function', 'filter', 'test', 'extension']
+__all__ = ["render_with", "function", "filter", "test", "extension"]
def _store_function(store, func, name=None):
@@ -16,6 +16,7 @@ def _store_function(store, func, name=None):
def _register_function(store, name, func):
if name is None and func is None:
+
def decorator(func):
_store_function(store, func)
return func
@@ -26,6 +27,7 @@ def decorator(func):
_store_function(store, name)
return name
else:
+
def decorator(func):
_store_function(store, func, name)
return func
diff --git a/judge/jinja2/render.py b/judge/jinja2/render.py
index 778e26ad6c..bea8c7ce0d 100644
--- a/judge/jinja2/render.py
+++ b/judge/jinja2/render.py
@@ -1,5 +1,9 @@
-from django.template import (Context, Template as DjangoTemplate, TemplateSyntaxError as DjangoTemplateSyntaxError,
- VariableDoesNotExist)
+from django.template import (
+ Context,
+ Template as DjangoTemplate,
+ TemplateSyntaxError as DjangoTemplateSyntaxError,
+ VariableDoesNotExist,
+)
from . import registry
@@ -24,4 +28,4 @@ def render_django(template, **context):
try:
return compile_template(template).render(Context(context))
except (VariableDoesNotExist, DjangoTemplateSyntaxError):
- return 'Error rendering: %r' % template
+ return "Error rendering: %r" % template
diff --git a/judge/jinja2/social.py b/judge/jinja2/social.py
index 7a20ec3dd4..a1358b83c4 100644
--- a/judge/jinja2/social.py
+++ b/judge/jinja2/social.py
@@ -1,12 +1,23 @@
from django.template.loader import get_template
from django.utils.safestring import mark_safe
-from django_social_share.templatetags.social_share import post_to_facebook_url, post_to_twitter_url
+from django_social_share.templatetags.social_share import (
+ post_to_facebook_url,
+ post_to_twitter_url,
+)
from . import registry
SHARES = [
- ('post_to_twitter', 'django_social_share/templatetags/post_to_twitter.html', post_to_twitter_url),
- ('post_to_facebook', 'django_social_share/templatetags/post_to_facebook.html', post_to_facebook_url),
+ (
+ "post_to_twitter",
+ "django_social_share/templatetags/post_to_twitter.html",
+ post_to_twitter_url,
+ ),
+ (
+ "post_to_facebook",
+ "django_social_share/templatetags/post_to_facebook.html",
+ post_to_facebook_url,
+ ),
# Deprecated:
# ('post_to_gplus', 'django_social_share/templatetags/post_to_gplus.html', post_to_gplus_url),
# For future versions:
@@ -18,7 +29,7 @@
def make_func(name, template, url_func):
def func(request, *args):
link_text = args[-1]
- context = {'request': request, 'link_text': mark_safe(link_text)}
+ context = {"request": request, "link_text": mark_safe(link_text)}
context = url_func(context, *args[:-1])
return mark_safe(get_template(template).render(context))
@@ -32,6 +43,10 @@ def func(request, *args):
@registry.function
def recaptcha_init(language=None):
- from snowpenguin.django.recaptcha2.templatetags.recaptcha2 import recaptcha_common_init
- return get_template('snowpenguin/recaptcha/recaptcha_init.html').render(
- recaptcha_common_init(language, {'explicit': False}))
+ from snowpenguin.django.recaptcha2.templatetags.recaptcha2 import (
+ recaptcha_common_init,
+ )
+
+ return get_template("snowpenguin/recaptcha/recaptcha_init.html").render(
+ recaptcha_common_init(language, {"explicit": False})
+ )
diff --git a/judge/jinja2/spaceless.py b/judge/jinja2/spaceless.py
index 90b6f36c3f..01eb5d8f4b 100644
--- a/judge/jinja2/spaceless.py
+++ b/judge/jinja2/spaceless.py
@@ -16,15 +16,17 @@ class SpacelessExtension(Extension):
https://stackoverflow.com/a/23741298/1090657
"""
- tags = {'spaceless'}
+ tags = {"spaceless"}
def parse(self, parser):
lineno = next(parser.stream).lineno
- body = parser.parse_statements(['name:endspaceless'], drop_needle=True)
+ body = parser.parse_statements(["name:endspaceless"], drop_needle=True)
return nodes.CallBlock(
- self.call_method('_strip_spaces', [], [], None, None),
- [], [], body,
+ self.call_method("_strip_spaces", [], [], None, None),
+ [],
+ [],
+ body,
).set_lineno(lineno)
def _strip_spaces(self, caller=None):
- return Markup(re.sub(r'>\s+<', '><', caller().unescape().strip()))
+ return Markup(re.sub(r">\s+<", "><", caller().unescape().strip()))
diff --git a/judge/jinja2/submission.py b/judge/jinja2/submission.py
index 39dff8f963..a6c5623801 100644
--- a/judge/jinja2/submission.py
+++ b/judge/jinja2/submission.py
@@ -6,29 +6,43 @@
# TODO: maybe refactor this?
def get_editor_ids(contest):
- return set(map(attrgetter('id'), contest.authors.all())) | set(map(attrgetter('id'), contest.curators.all()))
+ return set(map(attrgetter("id"), contest.authors.all())) | set(
+ map(attrgetter("id"), contest.curators.all())
+ )
@registry.function
-def submission_layout(submission, profile_id, user, completed_problem_ids, editable_problem_ids, tester_problem_ids):
+def submission_layout(
+ submission,
+ profile_id,
+ user,
+ completed_problem_ids,
+ editable_problem_ids,
+ tester_problem_ids,
+):
problem_id = submission.problem_id
submission_source_visibility = submission.problem.submission_source_visibility
can_view = False
can_edit = False
- if (user.has_perm('judge.edit_all_problem') or
- (user.has_perm('judge.edit_public_problem') and submission.problem.is_public) or
- # We try to avoid evaluating this as much as possible to keep it lazy.
- problem_id in editable_problem_ids):
+ if (
+ user.has_perm("judge.edit_all_problem")
+ or (user.has_perm("judge.edit_public_problem") and submission.problem.is_public)
+ or
+ # We try to avoid evaluating this as much as possible to keep it lazy.
+ problem_id in editable_problem_ids
+ ):
can_view = True
can_edit = True
- elif user.has_perm('judge.view_all_submission'):
+ elif user.has_perm("judge.view_all_submission"):
can_view = True
elif profile_id == submission.user_id:
can_view = True
elif submission_source_visibility == SubmissionSourceAccess.ALWAYS:
can_view = True
- elif submission.contest_object is not None and profile_id in get_editor_ids(submission.contest_object):
+ elif submission.contest_object is not None and profile_id in get_editor_ids(
+ submission.contest_object
+ ):
can_view = True
elif submission.problem_id in completed_problem_ids:
can_view = submission.problem_id in tester_problem_ids
diff --git a/judge/jinja2/timedelta.py b/judge/jinja2/timedelta.py
index f294033717..a68ec3af9f 100644
--- a/judge/jinja2/timedelta.py
+++ b/judge/jinja2/timedelta.py
@@ -5,14 +5,14 @@
@registry.filter
-def timedelta(value, display='long'):
+def timedelta(value, display="long"):
if value is None:
return value
return nice_repr(value, display)
@registry.filter
-def timestampdelta(value, display='long'):
+def timestampdelta(value, display="long"):
value = datetime.timedelta(seconds=value)
return timedelta(value, display)
@@ -23,6 +23,6 @@ def seconds(timedelta):
@registry.function
-@registry.render_with('time-remaining-fragment.html')
+@registry.render_with("time-remaining-fragment.html")
def as_countdown(timedelta):
- return {'countdown': timedelta}
+ return {"countdown": timedelta}
diff --git a/judge/judgeapi.py b/judge/judgeapi.py
index 25a34db3af..2a42e65ace 100644
--- a/judge/judgeapi.py
+++ b/judge/judgeapi.py
@@ -8,58 +8,83 @@
from django.utils import timezone
from judge import event_poster as event
-from judge.judge_priority import BATCH_REJUDGE_PRIORITY, CONTEST_SUBMISSION_PRIORITY, DEFAULT_PRIORITY, REJUDGE_PRIORITY
+from judge.judge_priority import (
+ BATCH_REJUDGE_PRIORITY,
+ CONTEST_SUBMISSION_PRIORITY,
+ DEFAULT_PRIORITY,
+ REJUDGE_PRIORITY,
+)
-logger = logging.getLogger('judge.judgeapi')
-size_pack = struct.Struct('!I')
+logger = logging.getLogger("judge.judgeapi")
+size_pack = struct.Struct("!I")
def _post_update_submission(submission, done=False):
if submission.problem.is_public:
- event.post('submissions', {'type': 'done-submission' if done else 'update-submission',
- 'id': submission.id,
- 'contest': submission.contest_key,
- 'user': submission.user_id, 'problem': submission.problem_id,
- 'status': submission.status, 'language': submission.language.key})
+ event.post(
+ "submissions",
+ {
+ "type": "done-submission" if done else "update-submission",
+ "id": submission.id,
+ "contest": submission.contest_key,
+ "user": submission.user_id,
+ "problem": submission.problem_id,
+ "status": submission.status,
+ "language": submission.language.key,
+ },
+ )
def judge_request(packet, reply=True):
- sock = socket.create_connection(settings.BRIDGED_DJANGO_CONNECT or
- settings.BRIDGED_DJANGO_ADDRESS[0])
+ sock = socket.create_connection(
+ settings.BRIDGED_DJANGO_CONNECT or settings.BRIDGED_DJANGO_ADDRESS[0]
+ )
- output = json.dumps(packet, separators=(',', ':'))
- output = zlib.compress(output.encode('utf-8'))
- writer = sock.makefile('wb')
+ output = json.dumps(packet, separators=(",", ":"))
+ output = zlib.compress(output.encode("utf-8"))
+ writer = sock.makefile("wb")
writer.write(size_pack.pack(len(output)))
writer.write(output)
writer.close()
if reply:
- reader = sock.makefile('rb', -1)
+ reader = sock.makefile("rb", -1)
input = reader.read(size_pack.size)
if not input:
- raise ValueError('Judge did not respond')
+ raise ValueError("Judge did not respond")
length = size_pack.unpack(input)[0]
input = reader.read(length)
if not input:
- raise ValueError('Judge did not respond')
+ raise ValueError("Judge did not respond")
reader.close()
sock.close()
- result = json.loads(zlib.decompress(input).decode('utf-8'))
+ result = json.loads(zlib.decompress(input).decode("utf-8"))
return result
def judge_submission(submission, rejudge=False, batch_rejudge=False, judge_id=None):
from .models import ContestSubmission, Submission, SubmissionTestCase
- updates = {'time': None, 'memory': None, 'points': None, 'result': None, 'case_points': 0, 'case_total': 0,
- 'error': None, 'rejudged_date': timezone.now() if rejudge or batch_rejudge else None, 'status': 'QU'}
+ updates = {
+ "time": None,
+ "memory": None,
+ "points": None,
+ "result": None,
+ "case_points": 0,
+ "case_total": 0,
+ "error": None,
+ "rejudged_date": timezone.now() if rejudge or batch_rejudge else None,
+ "status": "QU",
+ }
try:
# This is set proactively; it might get unset in judgecallback's on_grading_begin if the problem doesn't
# actually have pretests stored on the judge.
- updates['is_pretested'] = all(ContestSubmission.objects.filter(submission=submission)
- .values_list('problem__contest__run_pretests_only', 'problem__is_pretested')[0])
+ updates["is_pretested"] = all(
+ ContestSubmission.objects.filter(submission=submission).values_list(
+ "problem__contest__run_pretests_only", "problem__is_pretested"
+ )[0]
+ )
except IndexError:
priority = DEFAULT_PRIORITY
else:
@@ -73,51 +98,78 @@ def judge_submission(submission, rejudge=False, batch_rejudge=False, judge_id=No
# as that would prevent people from knowing a submission is being scheduled for rejudging.
# It is worth noting that this mechanism does not prevent a new rejudge from being scheduled
# while already queued, but that does not lead to data corruption.
- if not Submission.objects.filter(id=submission.id).exclude(status__in=('P', 'G')).update(**updates):
+ if (
+ not Submission.objects.filter(id=submission.id)
+ .exclude(status__in=("P", "G"))
+ .update(**updates)
+ ):
return False
SubmissionTestCase.objects.filter(submission_id=submission.id).delete()
try:
- response = judge_request({
- 'name': 'submission-request',
- 'submission-id': submission.id,
- 'problem-id': submission.problem.code,
- 'language': submission.language.key,
- 'source': submission.source.source,
- 'judge-id': judge_id,
- 'priority': BATCH_REJUDGE_PRIORITY if batch_rejudge else (REJUDGE_PRIORITY if rejudge else priority),
- })
+ response = judge_request(
+ {
+ "name": "submission-request",
+ "submission-id": submission.id,
+ "problem-id": submission.problem.code,
+ "language": submission.language.key,
+ "source": submission.source.source,
+ "judge-id": judge_id,
+ "priority": BATCH_REJUDGE_PRIORITY
+ if batch_rejudge
+ else (REJUDGE_PRIORITY if rejudge else priority),
+ }
+ )
except BaseException:
- logger.exception('Failed to send request to judge')
- Submission.objects.filter(id=submission.id).update(status='IE', result='IE')
+ logger.exception("Failed to send request to judge")
+ Submission.objects.filter(id=submission.id).update(status="IE", result="IE")
success = False
else:
- if response['name'] != 'submission-received' or response['submission-id'] != submission.id:
- Submission.objects.filter(id=submission.id).update(status='IE', result='IE')
+ if (
+ response["name"] != "submission-received"
+ or response["submission-id"] != submission.id
+ ):
+ Submission.objects.filter(id=submission.id).update(status="IE", result="IE")
_post_update_submission(submission)
success = True
return success
def disconnect_judge(judge, force=False):
- judge_request({'name': 'disconnect-judge', 'judge-id': judge.name, 'force': force}, reply=False)
+ judge_request(
+ {"name": "disconnect-judge", "judge-id": judge.name, "force": force},
+ reply=False,
+ )
def update_disable_judge(judge):
- judge_request({'name': 'disable-judge', 'judge-id': judge.name, 'is-disabled': judge.is_disabled})
+ judge_request(
+ {
+ "name": "disable-judge",
+ "judge-id": judge.name,
+ "is-disabled": judge.is_disabled,
+ }
+ )
def abort_submission(submission):
from .models import Submission
+
# We only want to try to abort a submission if it's still grading, otherwise this can lead to fully graded
# submissions marked as aborted.
- if submission.status == 'D':
+ if submission.status == "D":
return
- response = judge_request({'name': 'terminate-submission', 'submission-id': submission.id})
+ response = judge_request(
+ {"name": "terminate-submission", "submission-id": submission.id}
+ )
# This defaults to true, so that in the case the JudgeList fails to remove the submission from the queue,
# and returns a bad-request, the submission is not falsely shown as "Aborted" when it will still be judged.
- if not response.get('judge-aborted', True):
- Submission.objects.filter(id=submission.id).update(status='AB', result='AB', points=0)
- event.post('sub_%s' % Submission.get_id_secret(submission.id), {'type': 'aborted'})
+ if not response.get("judge-aborted", True):
+ Submission.objects.filter(id=submission.id).update(
+ status="AB", result="AB", points=0
+ )
+ event.post(
+ "sub_%s" % Submission.get_id_secret(submission.id), {"type": "aborted"}
+ )
_post_update_submission(submission, done=True)
diff --git a/judge/lxml_tree.py b/judge/lxml_tree.py
index 014f7dc6e3..bb71840b23 100644
--- a/judge/lxml_tree.py
+++ b/judge/lxml_tree.py
@@ -4,7 +4,7 @@
from lxml import html
from lxml.etree import ParserError, XMLSyntaxError
-logger = logging.getLogger('judge.html')
+logger = logging.getLogger("judge.html")
class HTMLTreeString(SafeData):
@@ -12,9 +12,11 @@ def __init__(self, str):
try:
self._tree = html.fromstring(str, parser=html.HTMLParser(recover=True))
except (XMLSyntaxError, ParserError) as e:
- if str and (not isinstance(e, ParserError) or e.args[0] != 'Document is empty'):
- logger.exception('Failed to parse HTML string')
- self._tree = html.Element('div')
+ if str and (
+ not isinstance(e, ParserError) or e.args[0] != "Document is empty"
+ ):
+ logger.exception("Failed to parse HTML string")
+ self._tree = html.Element("div")
def __getattr__(self, attr):
try:
@@ -23,15 +25,15 @@ def __getattr__(self, attr):
return getattr(str(self), attr)
def __setattr__(self, key, value):
- if key[0] == '_':
+ if key[0] == "_":
super(HTMLTreeString, self).__setattr__(key, value)
setattr(self._tree, key, value)
def __repr__(self):
- return '' % str(self)
+ return "" % str(self)
def __str__(self):
- return mark_safe(html.tostring(self._tree, encoding='unicode'))
+ return mark_safe(html.tostring(self._tree, encoding="unicode"))
def __radd__(self, other):
return other + str(self)
diff --git a/judge/management/commands/addjudge.py b/judge/management/commands/addjudge.py
index b659364e5c..f9ad589729 100644
--- a/judge/management/commands/addjudge.py
+++ b/judge/management/commands/addjudge.py
@@ -4,14 +4,14 @@
class Command(BaseCommand):
- help = 'create a judge'
+ help = "create a judge"
def add_arguments(self, parser):
- parser.add_argument('name', help='the name of the judge')
- parser.add_argument('auth_key', help='authentication key for the judge')
+ parser.add_argument("name", help="the name of the judge")
+ parser.add_argument("auth_key", help="authentication key for the judge")
def handle(self, *args, **options):
judge = Judge()
- judge.name = options['name']
- judge.auth_key = options['auth_key']
+ judge.name = options["name"]
+ judge.auth_key = options["auth_key"]
judge.save()
diff --git a/judge/management/commands/adduser.py b/judge/management/commands/adduser.py
index c89737e4ef..640625c96f 100644
--- a/judge/management/commands/adduser.py
+++ b/judge/management/commands/adduser.py
@@ -6,27 +6,39 @@
class Command(BaseCommand):
- help = 'creates a user'
+ help = "creates a user"
def add_arguments(self, parser):
- parser.add_argument('name', help='username')
- parser.add_argument('email', help='email, not necessary to be resolvable')
- parser.add_argument('password', help='password for the user')
- parser.add_argument('language', nargs='?', default=settings.DEFAULT_USER_LANGUAGE,
- help='default language ID for user')
+ parser.add_argument("name", help="username")
+ parser.add_argument("email", help="email, not necessary to be resolvable")
+ parser.add_argument("password", help="password for the user")
+ parser.add_argument(
+ "language",
+ nargs="?",
+ default=settings.DEFAULT_USER_LANGUAGE,
+ help="default language ID for user",
+ )
- parser.add_argument('--superuser', action='store_true', default=False,
- help='if specified, creates user with superuser privileges')
- parser.add_argument('--staff', action='store_true', default=False,
- help='if specified, creates user with staff privileges')
+ parser.add_argument(
+ "--superuser",
+ action="store_true",
+ default=False,
+ help="if specified, creates user with superuser privileges",
+ )
+ parser.add_argument(
+ "--staff",
+ action="store_true",
+ default=False,
+ help="if specified, creates user with staff privileges",
+ )
def handle(self, *args, **options):
- usr = User(username=options['name'], email=options['email'], is_active=True)
- usr.set_password(options['password'])
- usr.is_superuser = options['superuser']
- usr.is_staff = options['staff']
+ usr = User(username=options["name"], email=options["email"], is_active=True)
+ usr.set_password(options["password"])
+ usr.is_superuser = options["superuser"]
+ usr.is_staff = options["staff"]
usr.save()
profile = Profile(user=usr)
- profile.language = Language.objects.get(key=options['language'])
+ profile.language = Language.objects.get(key=options["language"])
profile.save()
diff --git a/judge/management/commands/camo.py b/judge/management/commands/camo.py
index aa65521736..774837ae26 100644
--- a/judge/management/commands/camo.py
+++ b/judge/management/commands/camo.py
@@ -4,13 +4,13 @@
class Command(BaseCommand):
- help = 'obtains the camo url for the specified url'
+ help = "obtains the camo url for the specified url"
def add_arguments(self, parser):
- parser.add_argument('url', help='url to use camo on')
+ parser.add_argument("url", help="url to use camo on")
def handle(self, *args, **options):
if camo_client is None:
- raise CommandError('Camo not available')
+ raise CommandError("Camo not available")
- print(camo_client.image_url(options['url']))
+ print(camo_client.image_url(options["url"]))
diff --git a/judge/management/commands/copy_language.py b/judge/management/commands/copy_language.py
index 233ccc7e0d..09120a6f81 100644
--- a/judge/management/commands/copy_language.py
+++ b/judge/management/commands/copy_language.py
@@ -4,24 +4,30 @@
class Command(BaseCommand):
- help = 'allows the problems that allow to be submitted in '
+ help = "allows the problems that allow to be submitted in "
def add_arguments(self, parser):
- parser.add_argument('source', help='language to copy from')
- parser.add_argument('target', help='language to copy to')
+ parser.add_argument("source", help="language to copy from")
+ parser.add_argument("target", help="language to copy to")
def handle(self, *args, **options):
try:
- source = Language.objects.get(key=options['source'])
+ source = Language.objects.get(key=options["source"])
except Language.DoesNotExist:
- raise CommandError('Invalid source language: %s' % options['source'])
+ raise CommandError("Invalid source language: %s" % options["source"])
try:
- target = Language.objects.get(key=options['target'])
+ target = Language.objects.get(key=options["target"])
except Language.DoesNotExist:
- raise CommandError('Invalid target language: %s' % options['target'])
+ raise CommandError("Invalid target language: %s" % options["target"])
target.problem_set.set(source.problem_set.all())
- LanguageLimit.objects.bulk_create(LanguageLimit(problem=ll.problem, language=target, time_limit=ll.time_limit,
- memory_limit=ll.memory_limit)
- for ll in LanguageLimit.objects.filter(language=source))
+ LanguageLimit.objects.bulk_create(
+ LanguageLimit(
+ problem=ll.problem,
+ language=target,
+ time_limit=ll.time_limit,
+ memory_limit=ll.memory_limit,
+ )
+ for ll in LanguageLimit.objects.filter(language=source)
+ )
diff --git a/judge/management/commands/create_problem.py b/judge/management/commands/create_problem.py
index b095e0820d..568103f39c 100644
--- a/judge/management/commands/create_problem.py
+++ b/judge/management/commands/create_problem.py
@@ -4,20 +4,20 @@
class Command(BaseCommand):
- help = 'create an empty problem'
+ help = "create an empty problem"
def add_arguments(self, parser):
- parser.add_argument('code', help='problem code')
- parser.add_argument('name', help='problem title')
- parser.add_argument('body', help='problem description')
- parser.add_argument('type', help='problem type')
- parser.add_argument('group', help='problem group')
+ parser.add_argument("code", help="problem code")
+ parser.add_argument("name", help="problem title")
+ parser.add_argument("body", help="problem description")
+ parser.add_argument("type", help="problem type")
+ parser.add_argument("group", help="problem group")
def handle(self, *args, **options):
problem = Problem()
- problem.code = options['code']
- problem.name = options['name']
- problem.description = options['body']
- problem.group = ProblemGroup.objects.get(name=options['group'])
- problem.types = [ProblemType.objects.get(name=options['type'])]
+ problem.code = options["code"]
+ problem.name = options["name"]
+ problem.description = options["body"]
+ problem.group = ProblemGroup.objects.get(name=options["group"])
+ problem.types = [ProblemType.objects.get(name=options["type"])]
problem.save()
diff --git a/judge/management/commands/generate_api_token.py b/judge/management/commands/generate_api_token.py
index c65055421c..89c4292dea 100644
--- a/judge/management/commands/generate_api_token.py
+++ b/judge/management/commands/generate_api_token.py
@@ -11,10 +11,12 @@ class Command(BaseCommand):
access admin pages."""
def add_arguments(self, parser):
- parser.add_argument('name', help='username')
+ parser.add_argument("name", help="username")
def handle(self, *args, **options):
try:
- print(Profile.objects.get(user__username=options['name']).generate_api_token())
+ print(
+ Profile.objects.get(user__username=options["name"]).generate_api_token()
+ )
except Profile.DoesNotExist:
- raise User.DoesNotExist('User %s does not exist' % options['name'])
+ raise User.DoesNotExist("User %s does not exist" % options["name"])
diff --git a/judge/management/commands/generate_sitemap.py b/judge/management/commands/generate_sitemap.py
index d71912b184..b7a4567fd9 100644
--- a/judge/management/commands/generate_sitemap.py
+++ b/judge/management/commands/generate_sitemap.py
@@ -13,57 +13,76 @@ class Command(BaseCommand):
requires_system_checks = False
def add_arguments(self, parser):
- parser.add_argument('directory', help='directory to generate the sitemap in')
- parser.add_argument('-s', '--site', type=int, help='ID of the site to generate the sitemap for')
- parser.add_argument('-p', '--protocol', default='https', help='protocol to use for links')
- parser.add_argument('-d', '--subdir', '--subdirectory', default='sitemaps',
- help='subdirectory for individual sitemap files')
- parser.add_argument('-P', '--prefix', help='URL prefix for individual sitemaps')
+ parser.add_argument("directory", help="directory to generate the sitemap in")
+ parser.add_argument(
+ "-s", "--site", type=int, help="ID of the site to generate the sitemap for"
+ )
+ parser.add_argument(
+ "-p", "--protocol", default="https", help="protocol to use for links"
+ )
+ parser.add_argument(
+ "-d",
+ "--subdir",
+ "--subdirectory",
+ default="sitemaps",
+ help="subdirectory for individual sitemap files",
+ )
+ parser.add_argument("-P", "--prefix", help="URL prefix for individual sitemaps")
def handle(self, *args, **options):
- directory = Path(options['directory'])
- protocol = options['protocol']
- subdirectory = options['subdir']
- verbose = options['verbosity'] > 1
+ directory = Path(options["directory"])
+ protocol = options["protocol"]
+ subdirectory = options["subdir"]
+ verbose = options["verbosity"] > 1
try:
- site = Site.objects.get(id=options['site']) if options['site'] else Site.objects.get_current()
+ site = (
+ Site.objects.get(id=options["site"])
+ if options["site"]
+ else Site.objects.get_current()
+ )
except Site.DoesNotExist:
- self.stderr.write('Pass a valid site ID for -s/--site.')
+ self.stderr.write("Pass a valid site ID for -s/--site.")
sys.exit(1)
if site is None:
- self.stderr.write('Pass -s/--site to set a site ID.')
+ self.stderr.write("Pass -s/--site to set a site ID.")
sys.exit(1)
- prefix = options['prefix'] or f'{protocol}://{site.domain}/{subdirectory}/'
- if not prefix.endswith('/'):
- self.stderr.write('-P/--prefix needs to end with a / or bad things will happen.')
+ prefix = options["prefix"] or f"{protocol}://{site.domain}/{subdirectory}/"
+ if not prefix.endswith("/"):
+ self.stderr.write(
+ "-P/--prefix needs to end with a / or bad things will happen."
+ )
sys.exit(1)
maps = []
maps_dir = directory / subdirectory
maps_dir.mkdir(parents=True, exist_ok=True)
- map_template = get_template('sitemap.xml')
- index_template = get_template('sitemap_index.xml')
+ map_template = get_template("sitemap.xml")
+ index_template = get_template("sitemap_index.xml")
for name, sitemap in sitemaps.items():
if callable(sitemap):
sitemap = sitemap()
for page in range(1, sitemap.paginator.num_pages + 1):
- file = f'sitemap-{name}-{page}.xml'
+ file = f"sitemap-{name}-{page}.xml"
if verbose:
- self.stdout.write(f'Rendering sitemap {file}...\n')
+ self.stdout.write(f"Rendering sitemap {file}...\n")
urls = sitemap.get_urls(page=page, site=site, protocol=protocol)
- with open(maps_dir / file, 'w', encoding='utf-8') as f:
- f.write(map_template.render({'urlset': urls}))
+ with open(maps_dir / file, "w", encoding="utf-8") as f:
+ f.write(map_template.render({"urlset": urls}))
maps.append(file)
if verbose:
- self.stdout.write('Rendering sitemap index file...')
-
- with open(directory / 'sitemap.xml', 'w', encoding='utf-8') as f:
- f.write(index_template.render({'sitemaps': [urljoin(prefix, file) for file in maps]}))
+ self.stdout.write("Rendering sitemap index file...")
+
+ with open(directory / "sitemap.xml", "w", encoding="utf-8") as f:
+ f.write(
+ index_template.render(
+ {"sitemaps": [urljoin(prefix, file) for file in maps]}
+ )
+ )
diff --git a/judge/management/commands/makedmojmessages.py b/judge/management/commands/makedmojmessages.py
index f162974bf3..981726f534 100644
--- a/judge/management/commands/makedmojmessages.py
+++ b/judge/management/commands/makedmojmessages.py
@@ -5,33 +5,69 @@
from django.conf import settings
from django.core.management import CommandError
-from django.core.management.commands.makemessages import Command as MakeMessagesCommand, check_programs
+from django.core.management.commands.makemessages import (
+ Command as MakeMessagesCommand,
+ check_programs,
+)
from judge.models import NavigationBar, ProblemType
class Command(MakeMessagesCommand):
def add_arguments(self, parser):
- parser.add_argument('--locale', '-l', default=[], dest='locale', action='append',
- help='Creates or updates the message files for the given locale(s) (e.g. pt_BR). '
- 'Can be used multiple times.')
- parser.add_argument('--exclude', '-x', default=[], dest='exclude', action='append',
- help='Locales to exclude. Default is none. Can be used multiple times.')
- parser.add_argument('--all', '-a', action='store_true', dest='all',
- default=False, help='Updates the message files for all existing locales.')
- parser.add_argument('--no-wrap', action='store_true', dest='no_wrap',
- default=False, help="Don't break long message lines into several lines.")
- parser.add_argument('--no-obsolete', action='store_true', dest='no_obsolete',
- default=False, help='Remove obsolete message strings.')
- parser.add_argument('--keep-pot', action='store_true', dest='keep_pot',
- default=False, help='Keep .pot file after making messages. Useful when debugging.')
+ parser.add_argument(
+ "--locale",
+ "-l",
+ default=[],
+ dest="locale",
+ action="append",
+ help="Creates or updates the message files for the given locale(s) (e.g. pt_BR). "
+ "Can be used multiple times.",
+ )
+ parser.add_argument(
+ "--exclude",
+ "-x",
+ default=[],
+ dest="exclude",
+ action="append",
+ help="Locales to exclude. Default is none. Can be used multiple times.",
+ )
+ parser.add_argument(
+ "--all",
+ "-a",
+ action="store_true",
+ dest="all",
+ default=False,
+ help="Updates the message files for all existing locales.",
+ )
+ parser.add_argument(
+ "--no-wrap",
+ action="store_true",
+ dest="no_wrap",
+ default=False,
+ help="Don't break long message lines into several lines.",
+ )
+ parser.add_argument(
+ "--no-obsolete",
+ action="store_true",
+ dest="no_obsolete",
+ default=False,
+ help="Remove obsolete message strings.",
+ )
+ parser.add_argument(
+ "--keep-pot",
+ action="store_true",
+ dest="keep_pot",
+ default=False,
+ help="Keep .pot file after making messages. Useful when debugging.",
+ )
def handle(self, *args, **options):
- locale = options.get('locale')
- exclude = options.get('exclude')
- self.domain = 'dmoj-user'
- self.verbosity = options.get('verbosity')
- process_all = options.get('all')
+ locale = options.get("locale")
+ exclude = options.get("exclude")
+ self.domain = "dmoj-user"
+ self.verbosity = options.get("verbosity")
+ process_all = options.get("all")
# Need to ensure that the i18n framework is enabled
if settings.configured:
@@ -40,43 +76,47 @@ def handle(self, *args, **options):
settings.configure(USE_I18N=True)
# Avoid messing with mutable class variables
- if options.get('no_wrap'):
- self.msgmerge_options = self.msgmerge_options[:] + ['--no-wrap']
- self.msguniq_options = self.msguniq_options[:] + ['--no-wrap']
- self.msgattrib_options = self.msgattrib_options[:] + ['--no-wrap']
- self.xgettext_options = self.xgettext_options[:] + ['--no-wrap']
- if options.get('no_location'):
- self.msgmerge_options = self.msgmerge_options[:] + ['--no-location']
- self.msguniq_options = self.msguniq_options[:] + ['--no-location']
- self.msgattrib_options = self.msgattrib_options[:] + ['--no-location']
- self.xgettext_options = self.xgettext_options[:] + ['--no-location']
-
- self.no_obsolete = options.get('no_obsolete')
- self.keep_pot = options.get('keep_pot')
+ if options.get("no_wrap"):
+ self.msgmerge_options = self.msgmerge_options[:] + ["--no-wrap"]
+ self.msguniq_options = self.msguniq_options[:] + ["--no-wrap"]
+ self.msgattrib_options = self.msgattrib_options[:] + ["--no-wrap"]
+ self.xgettext_options = self.xgettext_options[:] + ["--no-wrap"]
+ if options.get("no_location"):
+ self.msgmerge_options = self.msgmerge_options[:] + ["--no-location"]
+ self.msguniq_options = self.msguniq_options[:] + ["--no-location"]
+ self.msgattrib_options = self.msgattrib_options[:] + ["--no-location"]
+ self.xgettext_options = self.xgettext_options[:] + ["--no-location"]
+
+ self.no_obsolete = options.get("no_obsolete")
+ self.keep_pot = options.get("keep_pot")
if locale is None and not exclude and not process_all:
- raise CommandError("Type '%s help %s' for usage information." % (
- os.path.basename(sys.argv[0]), sys.argv[1]))
+ raise CommandError(
+ "Type '%s help %s' for usage information."
+ % (os.path.basename(sys.argv[0]), sys.argv[1])
+ )
self.invoked_for_django = False
self.locale_paths = []
self.default_locale_path = None
- if os.path.isdir(os.path.join('conf', 'locale')):
- self.locale_paths = [os.path.abspath(os.path.join('conf', 'locale'))]
+ if os.path.isdir(os.path.join("conf", "locale")):
+ self.locale_paths = [os.path.abspath(os.path.join("conf", "locale"))]
self.default_locale_path = self.locale_paths[0]
self.invoked_for_django = True
else:
self.locale_paths.extend(settings.LOCALE_PATHS)
# Allow to run makemessages inside an app dir
- if os.path.isdir('locale'):
- self.locale_paths.append(os.path.abspath('locale'))
+ if os.path.isdir("locale"):
+ self.locale_paths.append(os.path.abspath("locale"))
if self.locale_paths:
self.default_locale_path = self.locale_paths[0]
if not os.path.exists(self.default_locale_path):
os.makedirs(self.default_locale_path)
# Build locale list
- locale_dirs = list(filter(os.path.isdir, glob.glob('%s/*' % self.default_locale_path)))
+ locale_dirs = list(
+ filter(os.path.isdir, glob.glob("%s/*" % self.default_locale_path))
+ )
all_locales = list(map(os.path.basename, locale_dirs))
# Account for excluded locales
@@ -87,9 +127,9 @@ def handle(self, *args, **options):
locales = set(locales) - set(exclude)
if locales:
- check_programs('msguniq', 'msgmerge', 'msgattrib')
+ check_programs("msguniq", "msgmerge", "msgattrib")
- check_programs('xgettext')
+ check_programs("xgettext")
try:
potfiles = self.build_potfiles()
@@ -97,7 +137,7 @@ def handle(self, *args, **options):
# Build po files for each selected locale
for locale in locales:
if self.verbosity > 0:
- self.stdout.write('processing locale %s\n' % locale)
+ self.stdout.write("processing locale %s\n" % locale)
for potfile in potfiles:
self.write_po_file(potfile, locale)
finally:
@@ -108,23 +148,33 @@ def find_files(self, root):
return []
def _emit_message(self, potfile, string):
- potfile.write("""
+ potfile.write(
+ """
msgid "%s"
msgstr ""
-""" % string.replace('\\', r'\\').replace('\t', '\\t').replace('\n', '\\n').replace('"', '\\"'))
+"""
+ % string.replace("\\", r"\\")
+ .replace("\t", "\\t")
+ .replace("\n", "\\n")
+ .replace('"', '\\"')
+ )
def process_files(self, file_list):
- with io.open(os.path.join(self.default_locale_path, 'dmoj-user.pot'), 'w', encoding='utf-8') as potfile:
+ with io.open(
+ os.path.join(self.default_locale_path, "dmoj-user.pot"),
+ "w",
+ encoding="utf-8",
+ ) as potfile:
if self.verbosity > 1:
- self.stdout.write('processing navigation bar')
- for label in NavigationBar.objects.values_list('label', flat=True):
+ self.stdout.write("processing navigation bar")
+ for label in NavigationBar.objects.values_list("label", flat=True):
if self.verbosity > 2:
self.stdout.write('processing navigation item label "%s"\n' % label)
self._emit_message(potfile, label)
if self.verbosity > 1:
- self.stdout.write('processing problem types')
- for name in ProblemType.objects.values_list('full_name', flat=True):
+ self.stdout.write("processing problem types")
+ for name in ProblemType.objects.values_list("full_name", flat=True):
if self.verbosity > 2:
self.stdout.write('processing problem type name "%s"\n' % name)
self._emit_message(potfile, name)
diff --git a/judge/management/commands/move_user_content.py b/judge/management/commands/move_user_content.py
index aad026732a..d4772fdb08 100644
--- a/judge/management/commands/move_user_content.py
+++ b/judge/management/commands/move_user_content.py
@@ -5,25 +5,27 @@
class Command(BaseCommand):
- help = 'moves comments and submissions from to '
+ help = "moves comments and submissions from to "
def add_arguments(self, parser):
- parser.add_argument('source', help='user to copy from')
- parser.add_argument('target', help='user to copy to')
+ parser.add_argument("source", help="user to copy from")
+ parser.add_argument("target", help="user to copy to")
def handle(self, *args, **options):
try:
- source = Profile.objects.get(user__username=options['source'])
+ source = Profile.objects.get(user__username=options["source"])
except Profile.DoesNotExist:
raise CommandError(f'Invalid source user: {options["source"]}')
try:
- target = Profile.objects.get(user__username=options['target'])
+ target = Profile.objects.get(user__username=options["target"])
except Profile.DoesNotExist:
raise CommandError(f'Invalid target user: {options["target"]}')
if ContestParticipation.objects.filter(user=source).exists():
- raise CommandError(f'Cannot move user {options["source"]} because it has contest participations.')
+ raise CommandError(
+ f'Cannot move user {options["source"]} because it has contest participations.'
+ )
with transaction.atomic():
Submission.objects.filter(user=source).update(user=target)
diff --git a/judge/management/commands/render_pdf.py b/judge/management/commands/render_pdf.py
index 7f34d36f4a..a8dcf3884e 100644
--- a/judge/management/commands/render_pdf.py
+++ b/judge/management/commands/render_pdf.py
@@ -8,33 +8,48 @@
class Command(BaseCommand):
- help = 'renders a PDF file of a problem'
+ help = "renders a PDF file of a problem"
def add_arguments(self, parser):
- parser.add_argument('code', help='code of problem to render')
- parser.add_argument('-l', '--language', default=settings.LANGUAGE_CODE,
- help='language to render PDF in')
+ parser.add_argument("code", help="code of problem to render")
+ parser.add_argument(
+ "-l",
+ "--language",
+ default=settings.LANGUAGE_CODE,
+ help="language to render PDF in",
+ )
def handle(self, *args, **options):
try:
- problem = Problem.objects.get(code=options['code'])
+ problem = Problem.objects.get(code=options["code"])
except Problem.DoesNotExist:
- print('Bad problem code')
+ print("Bad problem code")
return
try:
- trans = problem.translations.get(language=options['language'])
+ trans = problem.translations.get(language=options["language"])
except ProblemTranslation.DoesNotExist:
trans = None
- with open(problem.code + '.pdf', 'wb') as f, translation.override(options['language']):
+ with open(problem.code + ".pdf", "wb") as f, translation.override(
+ options["language"]
+ ):
problem_name = problem.name if trans is None else trans.name
- f.write(render_pdf(
- html=get_template('problem/raw.html').render({
- 'problem': problem,
- 'problem_name': problem_name,
- 'description': problem.description if trans is None else trans.description,
- 'url': '',
- }).replace('"//', '"https://').replace("'//", "'https://"),
- title=problem_name,
- ))
+ f.write(
+ render_pdf(
+ html=get_template("problem/raw.html")
+ .render(
+ {
+ "problem": problem,
+ "problem_name": problem_name,
+ "description": problem.description
+ if trans is None
+ else trans.description,
+ "url": "",
+ }
+ )
+ .replace('"//', '"https://')
+ .replace("'//", "'https://"),
+ title=problem_name,
+ )
+ )
diff --git a/judge/management/commands/runmoss.py b/judge/management/commands/runmoss.py
index 2404984f08..27587277a6 100644
--- a/judge/management/commands/runmoss.py
+++ b/judge/management/commands/runmoss.py
@@ -6,41 +6,49 @@
class Command(BaseCommand):
- help = 'Checks for duplicate code using MOSS'
+ help = "Checks for duplicate code using MOSS"
LANG_MAPPING = {
- ('C++', MOSS_LANG_CC),
- ('C', MOSS_LANG_C),
- ('Java', MOSS_LANG_JAVA),
- ('Python', MOSS_LANG_PYTHON),
+ ("C++", MOSS_LANG_CC),
+ ("C", MOSS_LANG_C),
+ ("Java", MOSS_LANG_JAVA),
+ ("Python", MOSS_LANG_PYTHON),
}
def add_arguments(self, parser):
- parser.add_argument('contest', help='the id of the contest')
+ parser.add_argument("contest", help="the id of the contest")
def handle(self, *args, **options):
moss_api_key = settings.MOSS_API_KEY
if moss_api_key is None:
- print('No MOSS API Key supplied')
+ print("No MOSS API Key supplied")
return
- contest = options['contest']
+ contest = options["contest"]
- for problem in Contest.objects.get(key=contest).problems.order_by('code'):
- print('========== %s / %s ==========' % (problem.code, problem.name))
+ for problem in Contest.objects.get(key=contest).problems.order_by("code"):
+ print("========== %s / %s ==========" % (problem.code, problem.name))
for dmoj_lang, moss_lang in self.LANG_MAPPING:
- print('%s: ' % dmoj_lang, end=' ')
+ print("%s: " % dmoj_lang, end=" ")
subs = Submission.objects.filter(
- contest__participation__virtual__in=(ContestParticipation.LIVE, ContestParticipation.SPECTATE),
+ contest__participation__virtual__in=(
+ ContestParticipation.LIVE,
+ ContestParticipation.SPECTATE,
+ ),
contest__participation__contest__key=contest,
- result='AC', problem__id=problem.id,
+ result="AC",
+ problem__id=problem.id,
language__common_name=dmoj_lang,
- ).values_list('user__user__username', 'source__source')
+ ).values_list("user__user__username", "source__source")
if not subs:
- print('')
+ print("")
continue
- moss_call = MOSS(moss_api_key, language=moss_lang, matching_file_limit=100,
- comment='%s - %s' % (contest, problem.code))
+ moss_call = MOSS(
+ moss_api_key,
+ language=moss_lang,
+ matching_file_limit=100,
+ comment="%s - %s" % (contest, problem.code),
+ )
users = set()
@@ -48,6 +56,6 @@ def handle(self, *args, **options):
if username in users:
continue
users.add(username)
- moss_call.add_file_from_memory(username, source.encode('utf-8'))
+ moss_call.add_file_from_memory(username, source.encode("utf-8"))
- print('(%d): %s' % (subs.count(), moss_call.process()))
+ print("(%d): %s" % (subs.count(), moss_call.process()))
diff --git a/judge/middleware.py b/judge/middleware.py
index abad636af4..8be6819c11 100644
--- a/judge/middleware.py
+++ b/judge/middleware.py
@@ -27,11 +27,13 @@ def __init__(self, get_response):
def __call__(self, request):
try:
- callback, args, kwargs = resolve(request.path_info, getattr(request, 'urlconf', None))
+ callback, args, kwargs = resolve(
+ request.path_info, getattr(request, "urlconf", None)
+ )
except Resolver404:
callback, args, kwargs = None, None, None
- if getattr(callback, 'short_circuit_middleware', False):
+ if getattr(callback, "short_circuit_middleware", False):
return callback(request, *args, **kwargs)
return self.get_response(request)
@@ -44,24 +46,38 @@ def __call__(self, request):
if request.user.is_authenticated:
profile = request.profile = request.user.profile
if uwsgi:
- uwsgi.set_logvar('username', request.user.username)
- uwsgi.set_logvar('language', request.LANGUAGE_CODE)
-
- logout_path = reverse('auth_logout')
- login_2fa_path = reverse('login_2fa')
- webauthn_path = reverse('webauthn_assert')
- change_password_path = reverse('password_change')
- change_password_done_path = reverse('password_change_done')
+ uwsgi.set_logvar("username", request.user.username)
+ uwsgi.set_logvar("language", request.LANGUAGE_CODE)
+
+ logout_path = reverse("auth_logout")
+ login_2fa_path = reverse("login_2fa")
+ webauthn_path = reverse("webauthn_assert")
+ change_password_path = reverse("password_change")
+ change_password_done_path = reverse("password_change_done")
has_2fa = profile.is_totp_enabled or profile.is_webauthn_enabled
- if (has_2fa and not request.session.get('2fa_passed', False) and
- request.path not in (login_2fa_path, logout_path, webauthn_path) and
- not request.path.startswith(settings.STATIC_URL)):
- return HttpResponseRedirect(login_2fa_path + '?next=' + quote(request.get_full_path()))
- elif (request.session.get('password_pwned', False) and
- request.path not in (change_password_path, change_password_done_path,
- login_2fa_path, logout_path) and
- not request.path.startswith(settings.STATIC_URL)):
- return HttpResponseRedirect(change_password_path + '?next=' + quote(request.get_full_path()))
+ if (
+ has_2fa
+ and not request.session.get("2fa_passed", False)
+ and request.path not in (login_2fa_path, logout_path, webauthn_path)
+ and not request.path.startswith(settings.STATIC_URL)
+ ):
+ return HttpResponseRedirect(
+ login_2fa_path + "?next=" + quote(request.get_full_path())
+ )
+ elif (
+ request.session.get("password_pwned", False)
+ and request.path
+ not in (
+ change_password_path,
+ change_password_done_path,
+ login_2fa_path,
+ logout_path,
+ )
+ and not request.path.startswith(settings.STATIC_URL)
+ ):
+ return HttpResponseRedirect(
+ change_password_path + "?next=" + quote(request.get_full_path())
+ )
else:
request.profile = None
return self.get_response(request)
@@ -74,7 +90,10 @@ def __init__(self, get_response):
def __call__(self, request):
if request.user.is_impersonate:
if uwsgi:
- uwsgi.set_logvar('username', f'{request.impersonator.username} as {request.user.username}')
+ uwsgi.set_logvar(
+ "username",
+ f"{request.impersonator.username} as {request.user.username}",
+ )
request.no_profile_update = True
request.profile = request.user.profile
return self.get_response(request)
@@ -97,24 +116,26 @@ def __call__(self, request):
class APIMiddleware(object):
- header_pattern = re.compile('^Bearer ([a-zA-Z0-9_-]{48})$')
+ header_pattern = re.compile("^Bearer ([a-zA-Z0-9_-]{48})$")
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
- full_token = request.META.get('HTTP_AUTHORIZATION', '')
+ full_token = request.META.get("HTTP_AUTHORIZATION", "")
if not full_token:
return self.get_response(request)
token = self.header_pattern.match(full_token)
if not token:
- return HttpResponse('Invalid authorization header', status=400)
- if request.path.startswith(reverse('admin:index')):
- return HttpResponse('Admin inaccessible', status=403)
+ return HttpResponse("Invalid authorization header", status=400)
+ if request.path.startswith(reverse("admin:index")):
+ return HttpResponse("Admin inaccessible", status=403)
try:
- id, secret = struct.unpack('>I32s', base64.urlsafe_b64decode(token.group(1)))
+ id, secret = struct.unpack(
+ ">I32s", base64.urlsafe_b64decode(token.group(1))
+ )
request.user = User.objects.get(id=id)
# User hasn't generated a token
@@ -122,25 +143,27 @@ def __call__(self, request):
raise HTTPError()
# Token comparison
- digest = hmac.new(force_bytes(settings.SECRET_KEY), msg=secret, digestmod='sha256').hexdigest()
+ digest = hmac.new(
+ force_bytes(settings.SECRET_KEY), msg=secret, digestmod="sha256"
+ ).hexdigest()
if not hmac.compare_digest(digest, request.user.profile.api_token):
raise HTTPError()
request._cached_user = request.user
request.csrf_processing_done = True
- request.session['2fa_passed'] = True
+ request.session["2fa_passed"] = True
except (User.DoesNotExist, HTTPError):
- response = HttpResponse('Invalid token')
- response['WWW-Authenticate'] = 'Bearer realm="API"'
+ response = HttpResponse("Invalid token")
+ response["WWW-Authenticate"] = 'Bearer realm="API"'
response.status_code = 401
return response
return self.get_response(request)
class MiscConfigDict(dict):
- __slots__ = ('language', 'site', 'backing')
+ __slots__ = ("language", "site", "backing")
- def __init__(self, language='', domain=None):
+ def __init__(self, language="", domain=None):
self.language = language
self.site = domain
self.backing = None
@@ -148,23 +171,23 @@ def __init__(self, language='', domain=None):
def __missing__(self, key):
if self.backing is None:
- cache_key = 'misc_config'
+ cache_key = "misc_config"
backing = cache.get(cache_key)
if backing is None:
- backing = dict(MiscConfig.objects.values_list('key', 'value'))
+ backing = dict(MiscConfig.objects.values_list("key", "value"))
cache.set(cache_key, backing, 86400)
self.backing = backing
- keys = ['%s.%s' % (key, self.language), key] if self.language else [key]
+ keys = ["%s.%s" % (key, self.language), key] if self.language else [key]
if self.site is not None:
- keys = ['%s:%s' % (self.site, key) for key in keys] + keys
+ keys = ["%s:%s" % (self.site, key) for key in keys] + keys
for attempt in keys:
result = self.backing.get(attempt)
if result is not None:
break
else:
- result = ''
+ result = ""
self[key] = result
return result
@@ -176,5 +199,7 @@ def __init__(self, get_response):
def __call__(self, request):
domain = get_current_site(request).domain
- request.misc_config = MiscConfigDict(language=request.LANGUAGE_CODE, domain=domain)
+ request.misc_config = MiscConfigDict(
+ language=request.LANGUAGE_CODE, domain=domain
+ )
return self.get_response(request)
diff --git a/judge/migrations/0001_squashed_0084_contest_formats.py b/judge/migrations/0001_squashed_0084_contest_formats.py
index 22aa65974d..c7a5cc6fca 100644
--- a/judge/migrations/0001_squashed_0084_contest_formats.py
+++ b/judge/migrations/0001_squashed_0084_contest_formats.py
@@ -13,749 +13,3275 @@
class Migration(migrations.Migration):
-
- replaces = [('judge', '0001_initial'), ('judge', '0002_license'), ('judge', '0003_license_key'), ('judge', '0004_language_limit'), ('judge', '0005_nav_path_len'), ('judge', '0006_language_extension'), ('judge', '0007_test_site_perm'), ('judge', '0008_contestproblem_order'), ('judge', '0009_solution_problem'), ('judge', '0010_comment_page_index'), ('judge', '0011_organization_is_open'), ('judge', '0012_organization_perms'), ('judge', '0013_private_contests'), ('judge', '0014_multi_organization'), ('judge', '0015_remove_single_organization'), ('judge', '0016_organizationrequest'), ('judge', '0017_edit_public_problem_perm'), ('judge', '0018_django_1_9'), ('judge', '0019_og_images'), ('judge', '0020_profile_user_script'), ('judge', '0021_output_prefix_override'), ('judge', '0022_judge_last_ping'), ('judge', '0023_contest_tag'), ('judge', '0024_submission_judge'), ('judge', '0025_submission_rejudge_flag'), ('judge', '0026_change_public_visibility_perm'), ('judge', '0027_bridge_revert'), ('judge', '0028_judge_ip'), ('judge', '0029_problem_translation'), ('judge', '0030_remove_contest_profile'), ('judge', '0031_judge_versions'), ('judge', '0032_hide_problem_tags_in_contest'), ('judge', '0033_proper_pretest_support'), ('judge', '0034_submission_is_pretested'), ('judge', '0035_contest_spectate_mode'), ('judge', '0036_contest_participation_unique'), ('judge', '0037_user_count_ac_rate_field'), ('judge', '0038_profile_problem_count'), ('judge', '0039_remove_contest_is_external'), ('judge', '0040_profile_math_engine'), ('judge', '0041_virtual_contest_participation'), ('judge', '0042_remove_spectate_field'), ('judge', '0043_contest_user_count'), ('judge', '0044_organization_slots'), ('judge', '0045_organization_access_code'), ('judge', '0046_blogpost_authors'), ('judge', '0047_site_managed_data'), ('judge', '0048_site_managed_checkers'), ('judge', '0049_contest_summary'), ('judge', '0050_problem_tester_field'), ('judge', '0051_was_rejudged_field'), ('judge', '0052_switch_to_durationfield'), ('judge', '0053_opengraph_problems'), ('judge', '0054_tickets'), ('judge', '0055_add_performance_points'), ('judge', '0056_ticket_is_open'), ('judge', '0057_blue_pretests'), ('judge', '0058_problem_curator_field'), ('judge', '0059_problem_is_manually_managed'), ('judge', '0060_contest_clarifications'), ('judge', '0061_language_template'), ('judge', '0062_add_contest_submission_limit'), ('judge', '0063_new_solutions'), ('judge', '0064_unique_solution'), ('judge', '0065_blogpost_perms'), ('judge', '0066_submission_date_index'), ('judge', '0067_contest_access_code'), ('judge', '0068_hide_scoreboard'), ('judge', '0069_judge_blocking'), ('judge', '0070_organization_slug'), ('judge', '0071_organization_private_problems'), ('judge', '0072_contest_logo_override_image'), ('judge', '0073_comment_lock'), ('judge', '0074_totp'), ('judge', '0075_organization_admin_reverse'), ('judge', '0076_problem_statistics'), ('judge', '0077_remove_organization_key'), ('judge', '0078_add_user_notes'), ('judge', '0079_remove_comment_title'), ('judge', '0080_contest_banned_users'), ('judge', '0081_unlisted_users'), ('judge', '0082_remove_profile_name'), ('judge', '0083_extended_feedback'), ('judge', '0084_contest_formats')]
+ replaces = [
+ ("judge", "0001_initial"),
+ ("judge", "0002_license"),
+ ("judge", "0003_license_key"),
+ ("judge", "0004_language_limit"),
+ ("judge", "0005_nav_path_len"),
+ ("judge", "0006_language_extension"),
+ ("judge", "0007_test_site_perm"),
+ ("judge", "0008_contestproblem_order"),
+ ("judge", "0009_solution_problem"),
+ ("judge", "0010_comment_page_index"),
+ ("judge", "0011_organization_is_open"),
+ ("judge", "0012_organization_perms"),
+ ("judge", "0013_private_contests"),
+ ("judge", "0014_multi_organization"),
+ ("judge", "0015_remove_single_organization"),
+ ("judge", "0016_organizationrequest"),
+ ("judge", "0017_edit_public_problem_perm"),
+ ("judge", "0018_django_1_9"),
+ ("judge", "0019_og_images"),
+ ("judge", "0020_profile_user_script"),
+ ("judge", "0021_output_prefix_override"),
+ ("judge", "0022_judge_last_ping"),
+ ("judge", "0023_contest_tag"),
+ ("judge", "0024_submission_judge"),
+ ("judge", "0025_submission_rejudge_flag"),
+ ("judge", "0026_change_public_visibility_perm"),
+ ("judge", "0027_bridge_revert"),
+ ("judge", "0028_judge_ip"),
+ ("judge", "0029_problem_translation"),
+ ("judge", "0030_remove_contest_profile"),
+ ("judge", "0031_judge_versions"),
+ ("judge", "0032_hide_problem_tags_in_contest"),
+ ("judge", "0033_proper_pretest_support"),
+ ("judge", "0034_submission_is_pretested"),
+ ("judge", "0035_contest_spectate_mode"),
+ ("judge", "0036_contest_participation_unique"),
+ ("judge", "0037_user_count_ac_rate_field"),
+ ("judge", "0038_profile_problem_count"),
+ ("judge", "0039_remove_contest_is_external"),
+ ("judge", "0040_profile_math_engine"),
+ ("judge", "0041_virtual_contest_participation"),
+ ("judge", "0042_remove_spectate_field"),
+ ("judge", "0043_contest_user_count"),
+ ("judge", "0044_organization_slots"),
+ ("judge", "0045_organization_access_code"),
+ ("judge", "0046_blogpost_authors"),
+ ("judge", "0047_site_managed_data"),
+ ("judge", "0048_site_managed_checkers"),
+ ("judge", "0049_contest_summary"),
+ ("judge", "0050_problem_tester_field"),
+ ("judge", "0051_was_rejudged_field"),
+ ("judge", "0052_switch_to_durationfield"),
+ ("judge", "0053_opengraph_problems"),
+ ("judge", "0054_tickets"),
+ ("judge", "0055_add_performance_points"),
+ ("judge", "0056_ticket_is_open"),
+ ("judge", "0057_blue_pretests"),
+ ("judge", "0058_problem_curator_field"),
+ ("judge", "0059_problem_is_manually_managed"),
+ ("judge", "0060_contest_clarifications"),
+ ("judge", "0061_language_template"),
+ ("judge", "0062_add_contest_submission_limit"),
+ ("judge", "0063_new_solutions"),
+ ("judge", "0064_unique_solution"),
+ ("judge", "0065_blogpost_perms"),
+ ("judge", "0066_submission_date_index"),
+ ("judge", "0067_contest_access_code"),
+ ("judge", "0068_hide_scoreboard"),
+ ("judge", "0069_judge_blocking"),
+ ("judge", "0070_organization_slug"),
+ ("judge", "0071_organization_private_problems"),
+ ("judge", "0072_contest_logo_override_image"),
+ ("judge", "0073_comment_lock"),
+ ("judge", "0074_totp"),
+ ("judge", "0075_organization_admin_reverse"),
+ ("judge", "0076_problem_statistics"),
+ ("judge", "0077_remove_organization_key"),
+ ("judge", "0078_add_user_notes"),
+ ("judge", "0079_remove_comment_title"),
+ ("judge", "0080_contest_banned_users"),
+ ("judge", "0081_unlisted_users"),
+ ("judge", "0082_remove_profile_name"),
+ ("judge", "0083_extended_feedback"),
+ ("judge", "0084_contest_formats"),
+ ]
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ('contenttypes', '0002_remove_content_type_name'),
+ ("contenttypes", "0002_remove_content_type_name"),
]
operations = [
migrations.CreateModel(
- name='BlogPost',
+ name="BlogPost",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('title', models.CharField(max_length=100, verbose_name='post title')),
- ('slug', models.SlugField(verbose_name='slug')),
- ('visible', models.BooleanField(default=False, verbose_name='public visibility')),
- ('sticky', models.BooleanField(default=False, verbose_name='sticky')),
- ('publish_on', models.DateTimeField(verbose_name='publish after')),
- ('content', models.TextField(verbose_name='post content')),
- ('summary', models.TextField(blank=True, verbose_name='post summary')),
- ('og_image', models.CharField(blank=True, default='', max_length=150, verbose_name='openGraph image')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("title", models.CharField(max_length=100, verbose_name="post title")),
+ ("slug", models.SlugField(verbose_name="slug")),
+ (
+ "visible",
+ models.BooleanField(
+ default=False, verbose_name="public visibility"
+ ),
+ ),
+ ("sticky", models.BooleanField(default=False, verbose_name="sticky")),
+ ("publish_on", models.DateTimeField(verbose_name="publish after")),
+ ("content", models.TextField(verbose_name="post content")),
+ ("summary", models.TextField(blank=True, verbose_name="post summary")),
+ (
+ "og_image",
+ models.CharField(
+ blank=True,
+ default="",
+ max_length=150,
+ verbose_name="openGraph image",
+ ),
+ ),
],
options={
- 'verbose_name_plural': 'blog posts',
- 'permissions': (('edit_all_post', 'Edit all posts'),),
- 'verbose_name': 'blog post',
+ "verbose_name_plural": "blog posts",
+ "permissions": (("edit_all_post", "Edit all posts"),),
+ "verbose_name": "blog post",
},
),
migrations.CreateModel(
- name='Comment',
+ name="Comment",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('time', models.DateTimeField(auto_now_add=True, verbose_name='posted time')),
- ('page', models.CharField(db_index=True, max_length=30, validators=[django.core.validators.RegexValidator('^[pcs]:[a-z0-9]+$|^b:\\d+$', 'Page code must be ^[pcs]:[a-z0-9]+$|^b:\\d+$')], verbose_name='associated page')),
- ('score', models.IntegerField(default=0, verbose_name='votes')),
- ('body', models.TextField(max_length=8192, verbose_name='body of comment')),
- ('hidden', models.BooleanField(default=0, verbose_name='hide the comment')),
- ('lft', models.PositiveIntegerField(db_index=True, editable=False)),
- ('rght', models.PositiveIntegerField(db_index=True, editable=False)),
- ('tree_id', models.PositiveIntegerField(db_index=True, editable=False)),
- ('level', models.PositiveIntegerField(db_index=True, editable=False)),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "time",
+ models.DateTimeField(auto_now_add=True, verbose_name="posted time"),
+ ),
+ (
+ "page",
+ models.CharField(
+ db_index=True,
+ max_length=30,
+ validators=[
+ django.core.validators.RegexValidator(
+ "^[pcs]:[a-z0-9]+$|^b:\\d+$",
+ "Page code must be ^[pcs]:[a-z0-9]+$|^b:\\d+$",
+ )
+ ],
+ verbose_name="associated page",
+ ),
+ ),
+ ("score", models.IntegerField(default=0, verbose_name="votes")),
+ (
+ "body",
+ models.TextField(max_length=8192, verbose_name="body of comment"),
+ ),
+ (
+ "hidden",
+ models.BooleanField(default=0, verbose_name="hide the comment"),
+ ),
+ ("lft", models.PositiveIntegerField(db_index=True, editable=False)),
+ ("rght", models.PositiveIntegerField(db_index=True, editable=False)),
+ ("tree_id", models.PositiveIntegerField(db_index=True, editable=False)),
+ ("level", models.PositiveIntegerField(db_index=True, editable=False)),
],
options={
- 'verbose_name_plural': 'comments',
- 'verbose_name': 'comment',
+ "verbose_name_plural": "comments",
+ "verbose_name": "comment",
},
),
migrations.CreateModel(
- name='CommentLock',
+ name="CommentLock",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('page', models.CharField(db_index=True, max_length=30, validators=[django.core.validators.RegexValidator('^[pcs]:[a-z0-9]+$|^b:\\d+$', 'Page code must be ^[pcs]:[a-z0-9]+$|^b:\\d+$')], verbose_name='associated page')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "page",
+ models.CharField(
+ db_index=True,
+ max_length=30,
+ validators=[
+ django.core.validators.RegexValidator(
+ "^[pcs]:[a-z0-9]+$|^b:\\d+$",
+ "Page code must be ^[pcs]:[a-z0-9]+$|^b:\\d+$",
+ )
+ ],
+ verbose_name="associated page",
+ ),
+ ),
],
options={
- 'permissions': (('override_comment_lock', 'Override comment lock'),),
+ "permissions": (("override_comment_lock", "Override comment lock"),),
},
),
migrations.CreateModel(
- name='CommentVote',
+ name="CommentVote",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('score', models.IntegerField()),
- ('comment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='votes', to='judge.Comment')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("score", models.IntegerField()),
+ (
+ "comment",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="votes",
+ to="judge.Comment",
+ ),
+ ),
],
options={
- 'verbose_name_plural': 'comment votes',
- 'verbose_name': 'comment vote',
+ "verbose_name_plural": "comment votes",
+ "verbose_name": "comment vote",
},
),
migrations.CreateModel(
- name='Contest',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('key', models.CharField(max_length=20, unique=True, validators=[django.core.validators.RegexValidator('^[a-z0-9]+$', 'Contest id must be ^[a-z0-9]+$')], verbose_name='contest id')),
- ('name', models.CharField(db_index=True, max_length=100, verbose_name='contest name')),
- ('description', models.TextField(blank=True, verbose_name='description')),
- ('start_time', models.DateTimeField(db_index=True, verbose_name='start time')),
- ('end_time', models.DateTimeField(db_index=True, verbose_name='end time')),
- ('time_limit', models.DurationField(blank=True, null=True, verbose_name='time limit')),
- ('is_public', models.BooleanField(default=False, help_text='Should be set even for organization-private contests, where it determines whether the contest is visible to members of the specified organizations.', verbose_name='publicly visible')),
- ('is_rated', models.BooleanField(default=False, help_text='Whether this contest can be rated.', verbose_name='contest rated')),
- ('hide_scoreboard', models.BooleanField(default=False, help_text='Whether the scoreboard should remain hidden for the duration of the contest.', verbose_name='hide scoreboard')),
- ('use_clarifications', models.BooleanField(default=True, help_text='Use clarification system instead of comments.', verbose_name='no comments')),
- ('rate_all', models.BooleanField(default=False, help_text='Rate all users who joined.', verbose_name='rate all')),
- ('is_private', models.BooleanField(default=False, verbose_name='private to organizations')),
- ('hide_problem_tags', models.BooleanField(default=False, help_text='Whether problem tags should be hidden by default.', verbose_name='hide problem tags')),
- ('run_pretests_only', models.BooleanField(default=False, help_text='Whether judges should grade pretests only, versus all testcases. Commonly set during a contest, then unset prior to rejudging user submissions when the contest ends.', verbose_name='run pretests only')),
- ('og_image', models.CharField(blank=True, default='', max_length=150, verbose_name='OpenGraph image')),
- ('logo_override_image', models.CharField(blank=True, default='', help_text='This image will replace the default site logo for users inside the contest.', max_length=150, verbose_name='Logo override image')),
- ('user_count', models.IntegerField(default=0, verbose_name='the amount of live participants')),
- ('summary', models.TextField(blank=True, help_text='Plain-text, shown in meta description tag, e.g. for social media.', verbose_name='contest summary')),
- ('access_code', models.CharField(blank=True, default='', help_text='An optional code to prompt contestants before they are allowed to join the contest. Leave it blank to disable.', max_length=255, verbose_name='access code')),
- ('format_name', models.CharField(choices=[('default', 'Default')], default='default', help_text='The contest format module to use.', max_length=32, verbose_name='contest format')),
- ('format_config', jsonfield.fields.JSONField(blank=True, help_text='A JSON object to serve as the configuration for the chosen contest format module. Leave empty to use None. Exact format depends on the contest format selected.', null=True, verbose_name='contest format configuration')),
+ name="Contest",
+ fields=[
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "key",
+ models.CharField(
+ max_length=20,
+ unique=True,
+ validators=[
+ django.core.validators.RegexValidator(
+ "^[a-z0-9]+$", "Contest id must be ^[a-z0-9]+$"
+ )
+ ],
+ verbose_name="contest id",
+ ),
+ ),
+ (
+ "name",
+ models.CharField(
+ db_index=True, max_length=100, verbose_name="contest name"
+ ),
+ ),
+ (
+ "description",
+ models.TextField(blank=True, verbose_name="description"),
+ ),
+ (
+ "start_time",
+ models.DateTimeField(db_index=True, verbose_name="start time"),
+ ),
+ (
+ "end_time",
+ models.DateTimeField(db_index=True, verbose_name="end time"),
+ ),
+ (
+ "time_limit",
+ models.DurationField(
+ blank=True, null=True, verbose_name="time limit"
+ ),
+ ),
+ (
+ "is_public",
+ models.BooleanField(
+ default=False,
+ help_text="Should be set even for organization-private contests, where it determines whether the contest is visible to members of the specified organizations.",
+ verbose_name="publicly visible",
+ ),
+ ),
+ (
+ "is_rated",
+ models.BooleanField(
+ default=False,
+ help_text="Whether this contest can be rated.",
+ verbose_name="contest rated",
+ ),
+ ),
+ (
+ "hide_scoreboard",
+ models.BooleanField(
+ default=False,
+ help_text="Whether the scoreboard should remain hidden for the duration of the contest.",
+ verbose_name="hide scoreboard",
+ ),
+ ),
+ (
+ "use_clarifications",
+ models.BooleanField(
+ default=True,
+ help_text="Use clarification system instead of comments.",
+ verbose_name="no comments",
+ ),
+ ),
+ (
+ "rate_all",
+ models.BooleanField(
+ default=False,
+ help_text="Rate all users who joined.",
+ verbose_name="rate all",
+ ),
+ ),
+ (
+ "is_private",
+ models.BooleanField(
+ default=False, verbose_name="private to organizations"
+ ),
+ ),
+ (
+ "hide_problem_tags",
+ models.BooleanField(
+ default=False,
+ help_text="Whether problem tags should be hidden by default.",
+ verbose_name="hide problem tags",
+ ),
+ ),
+ (
+ "run_pretests_only",
+ models.BooleanField(
+ default=False,
+ help_text="Whether judges should grade pretests only, versus all testcases. Commonly set during a contest, then unset prior to rejudging user submissions when the contest ends.",
+ verbose_name="run pretests only",
+ ),
+ ),
+ (
+ "og_image",
+ models.CharField(
+ blank=True,
+ default="",
+ max_length=150,
+ verbose_name="OpenGraph image",
+ ),
+ ),
+ (
+ "logo_override_image",
+ models.CharField(
+ blank=True,
+ default="",
+ help_text="This image will replace the default site logo for users inside the contest.",
+ max_length=150,
+ verbose_name="Logo override image",
+ ),
+ ),
+ (
+ "user_count",
+ models.IntegerField(
+ default=0, verbose_name="the amount of live participants"
+ ),
+ ),
+ (
+ "summary",
+ models.TextField(
+ blank=True,
+ help_text="Plain-text, shown in meta description tag, e.g. for social media.",
+ verbose_name="contest summary",
+ ),
+ ),
+ (
+ "access_code",
+ models.CharField(
+ blank=True,
+ default="",
+ help_text="An optional code to prompt contestants before they are allowed to join the contest. Leave it blank to disable.",
+ max_length=255,
+ verbose_name="access code",
+ ),
+ ),
+ (
+ "format_name",
+ models.CharField(
+ choices=[("default", "Default")],
+ default="default",
+ help_text="The contest format module to use.",
+ max_length=32,
+ verbose_name="contest format",
+ ),
+ ),
+ (
+ "format_config",
+ jsonfield.fields.JSONField(
+ blank=True,
+ help_text="A JSON object to serve as the configuration for the chosen contest format module. Leave empty to use None. Exact format depends on the contest format selected.",
+ null=True,
+ verbose_name="contest format configuration",
+ ),
+ ),
],
options={
- 'verbose_name_plural': 'contests',
- 'permissions': (('see_private_contest', 'See private contests'), ('edit_own_contest', 'Edit own contests'), ('edit_all_contest', 'Edit all contests'), ('contest_rating', 'Rate contests'), ('contest_access_code', 'Contest access codes')),
- 'verbose_name': 'contest',
+ "verbose_name_plural": "contests",
+ "permissions": (
+ ("see_private_contest", "See private contests"),
+ ("edit_own_contest", "Edit own contests"),
+ ("edit_all_contest", "Edit all contests"),
+ ("contest_rating", "Rate contests"),
+ ("contest_access_code", "Contest access codes"),
+ ),
+ "verbose_name": "contest",
},
),
migrations.CreateModel(
- name='ContestParticipation',
+ name="ContestParticipation",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('real_start', models.DateTimeField(db_column='start', default=django.utils.timezone.now, verbose_name='start time')),
- ('score', models.IntegerField(db_index=True, default=0, verbose_name='score')),
- ('cumtime', models.PositiveIntegerField(default=0, verbose_name='cumulative time')),
- ('virtual', models.IntegerField(default=0, help_text='0 means non-virtual, otherwise the n-th virtual participation', verbose_name='virtual participation id')),
- ('format_data', jsonfield.fields.JSONField(blank=True, null=True, verbose_name='contest format specific data')),
- ('contest', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='users', to='judge.Contest', verbose_name='associated contest')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "real_start",
+ models.DateTimeField(
+ db_column="start",
+ default=django.utils.timezone.now,
+ verbose_name="start time",
+ ),
+ ),
+ (
+ "score",
+ models.IntegerField(db_index=True, default=0, verbose_name="score"),
+ ),
+ (
+ "cumtime",
+ models.PositiveIntegerField(
+ default=0, verbose_name="cumulative time"
+ ),
+ ),
+ (
+ "virtual",
+ models.IntegerField(
+ default=0,
+ help_text="0 means non-virtual, otherwise the n-th virtual participation",
+ verbose_name="virtual participation id",
+ ),
+ ),
+ (
+ "format_data",
+ jsonfield.fields.JSONField(
+ blank=True,
+ null=True,
+ verbose_name="contest format specific data",
+ ),
+ ),
+ (
+ "contest",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="users",
+ to="judge.Contest",
+ verbose_name="associated contest",
+ ),
+ ),
],
options={
- 'verbose_name_plural': 'contest participations',
- 'verbose_name': 'contest participation',
+ "verbose_name_plural": "contest participations",
+ "verbose_name": "contest participation",
},
),
migrations.CreateModel(
- name='ContestProblem',
+ name="ContestProblem",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('points', models.IntegerField(verbose_name='points')),
- ('partial', models.BooleanField(default=True, verbose_name='partial')),
- ('is_pretested', models.BooleanField(default=False, verbose_name='is pretested')),
- ('order', models.PositiveIntegerField(db_index=True, verbose_name='order')),
- ('output_prefix_override', models.IntegerField(blank=True, null=True, verbose_name='output prefix length override')),
- ('max_submissions', models.IntegerField(default=0, help_text='Maximum number of submissions for this problem, or 0 for no limit.', validators=[django.core.validators.MinValueValidator(0, "Why include a problem you can't submit to?")])),
- ('contest', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contest_problems', to='judge.Contest', verbose_name='contest')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("points", models.IntegerField(verbose_name="points")),
+ ("partial", models.BooleanField(default=True, verbose_name="partial")),
+ (
+ "is_pretested",
+ models.BooleanField(default=False, verbose_name="is pretested"),
+ ),
+ (
+ "order",
+ models.PositiveIntegerField(db_index=True, verbose_name="order"),
+ ),
+ (
+ "output_prefix_override",
+ models.IntegerField(
+ blank=True,
+ null=True,
+ verbose_name="output prefix length override",
+ ),
+ ),
+ (
+ "max_submissions",
+ models.IntegerField(
+ default=0,
+ help_text="Maximum number of submissions for this problem, or 0 for no limit.",
+ validators=[
+ django.core.validators.MinValueValidator(
+ 0, "Why include a problem you can't submit to?"
+ )
+ ],
+ ),
+ ),
+ (
+ "contest",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="contest_problems",
+ to="judge.Contest",
+ verbose_name="contest",
+ ),
+ ),
],
options={
- 'verbose_name_plural': 'contest problems',
- 'verbose_name': 'contest problem',
+ "verbose_name_plural": "contest problems",
+ "verbose_name": "contest problem",
},
),
migrations.CreateModel(
- name='ContestSubmission',
+ name="ContestSubmission",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('points', models.FloatField(default=0.0, verbose_name='points')),
- ('is_pretest', models.BooleanField(default=False, help_text='Whether this submission was ran only on pretests.', verbose_name='is pretested')),
- ('participation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submissions', related_query_name='submission', to='judge.ContestParticipation', verbose_name='participation')),
- ('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submissions', related_query_name='submission', to='judge.ContestProblem', verbose_name='problem')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("points", models.FloatField(default=0.0, verbose_name="points")),
+ (
+ "is_pretest",
+ models.BooleanField(
+ default=False,
+ help_text="Whether this submission was ran only on pretests.",
+ verbose_name="is pretested",
+ ),
+ ),
+ (
+ "participation",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="submissions",
+ related_query_name="submission",
+ to="judge.ContestParticipation",
+ verbose_name="participation",
+ ),
+ ),
+ (
+ "problem",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="submissions",
+ related_query_name="submission",
+ to="judge.ContestProblem",
+ verbose_name="problem",
+ ),
+ ),
],
options={
- 'verbose_name_plural': 'contest submissions',
- 'verbose_name': 'contest submission',
+ "verbose_name_plural": "contest submissions",
+ "verbose_name": "contest submission",
},
),
migrations.CreateModel(
- name='ContestTag',
+ name="ContestTag",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('name', models.CharField(max_length=20, unique=True, validators=[django.core.validators.RegexValidator('^[a-z-]+$', message='Lowercase letters and hyphens only.')], verbose_name='tag name')),
- ('color', models.CharField(max_length=7, validators=[django.core.validators.RegexValidator('^#(?:[A-Fa-f0-9]{3}){1,2}$', 'Invalid colour.')], verbose_name='tag colour')),
- ('description', models.TextField(blank=True, verbose_name='tag description')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "name",
+ models.CharField(
+ max_length=20,
+ unique=True,
+ validators=[
+ django.core.validators.RegexValidator(
+ "^[a-z-]+$",
+ message="Lowercase letters and hyphens only.",
+ )
+ ],
+ verbose_name="tag name",
+ ),
+ ),
+ (
+ "color",
+ models.CharField(
+ max_length=7,
+ validators=[
+ django.core.validators.RegexValidator(
+ "^#(?:[A-Fa-f0-9]{3}){1,2}$", "Invalid colour."
+ )
+ ],
+ verbose_name="tag colour",
+ ),
+ ),
+ (
+ "description",
+ models.TextField(blank=True, verbose_name="tag description"),
+ ),
],
options={
- 'verbose_name_plural': 'contest tags',
- 'verbose_name': 'contest tag',
+ "verbose_name_plural": "contest tags",
+ "verbose_name": "contest tag",
},
),
migrations.CreateModel(
- name='Judge',
+ name="Judge",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('name', models.CharField(help_text='Server name, hostname-style', max_length=50, unique=True)),
- ('created', models.DateTimeField(auto_now_add=True, verbose_name='time of creation')),
- ('auth_key', models.CharField(help_text='A key to authenticated this judge', max_length=100, verbose_name='authentication key')),
- ('is_blocked', models.BooleanField(default=False, help_text='Whether this judge should be blocked from connecting, even if its key is correct.', verbose_name='block judge')),
- ('online', models.BooleanField(default=False, verbose_name='judge online status')),
- ('start_time', models.DateTimeField(null=True, verbose_name='judge start time')),
- ('ping', models.FloatField(null=True, verbose_name='response time')),
- ('load', models.FloatField(help_text='Load for the last minute, divided by processors to be fair.', null=True, verbose_name='system load')),
- ('description', models.TextField(blank=True, verbose_name='description')),
- ('last_ip', models.GenericIPAddressField(blank=True, null=True, verbose_name='Last connected IP')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "name",
+ models.CharField(
+ help_text="Server name, hostname-style",
+ max_length=50,
+ unique=True,
+ ),
+ ),
+ (
+ "created",
+ models.DateTimeField(
+ auto_now_add=True, verbose_name="time of creation"
+ ),
+ ),
+ (
+ "auth_key",
+ models.CharField(
+ help_text="A key to authenticated this judge",
+ max_length=100,
+ verbose_name="authentication key",
+ ),
+ ),
+ (
+ "is_blocked",
+ models.BooleanField(
+ default=False,
+ help_text="Whether this judge should be blocked from connecting, even if its key is correct.",
+ verbose_name="block judge",
+ ),
+ ),
+ (
+ "online",
+ models.BooleanField(
+ default=False, verbose_name="judge online status"
+ ),
+ ),
+ (
+ "start_time",
+ models.DateTimeField(null=True, verbose_name="judge start time"),
+ ),
+ ("ping", models.FloatField(null=True, verbose_name="response time")),
+ (
+ "load",
+ models.FloatField(
+ help_text="Load for the last minute, divided by processors to be fair.",
+ null=True,
+ verbose_name="system load",
+ ),
+ ),
+ (
+ "description",
+ models.TextField(blank=True, verbose_name="description"),
+ ),
+ (
+ "last_ip",
+ models.GenericIPAddressField(
+ blank=True, null=True, verbose_name="Last connected IP"
+ ),
+ ),
],
options={
- 'ordering': ['name'],
- 'verbose_name_plural': 'judges',
- 'verbose_name': 'judge',
+ "ordering": ["name"],
+ "verbose_name_plural": "judges",
+ "verbose_name": "judge",
},
),
migrations.CreateModel(
- name='Language',
+ name="Language",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('key', models.CharField(help_text='The identifier for this language; the same as its executor id for judges.', max_length=6, unique=True, verbose_name='short identifier')),
- ('name', models.CharField(help_text='Longer name for the language, e.g. "Python 2" or "C++11".', max_length=20, verbose_name='long name')),
- ('short_name', models.CharField(blank=True, help_text='More readable, but short, name to display publicly; e.g. "PY2" or "C++11". If left blank, it will default to the short identifier.', max_length=10, null=True, verbose_name='short name')),
- ('common_name', models.CharField(help_text='Common name for the language. For example, the common name for C++03, C++11, and C++14 would be "C++"', max_length=10, verbose_name='common name')),
- ('ace', models.CharField(help_text='Language ID for Ace.js editor highlighting, appended to "mode-" to determine the Ace JavaScript file to use, e.g., "python".', max_length=20, verbose_name='ace mode name')),
- ('pygments', models.CharField(help_text='Language ID for Pygments highlighting in source windows.', max_length=20, verbose_name='pygments name')),
- ('template', models.TextField(blank=True, help_text='Code template to display in submission editor.', verbose_name='code template')),
- ('info', models.CharField(blank=True, help_text="Do not set this unless you know what you're doing! It will override the usually more specific, judge-provided runtime info!", max_length=50, verbose_name='runtime info override')),
- ('description', models.TextField(blank=True, help_text='Use field this to inform users of quirks with your environment, additional restrictions, etc.', verbose_name='language description')),
- ('extension', models.CharField(help_text='The extension of source files, e.g., "py" or "cpp".', max_length=10, verbose_name='extension')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "key",
+ models.CharField(
+ help_text="The identifier for this language; the same as its executor id for judges.",
+ max_length=6,
+ unique=True,
+ verbose_name="short identifier",
+ ),
+ ),
+ (
+ "name",
+ models.CharField(
+ help_text='Longer name for the language, e.g. "Python 2" or "C++11".',
+ max_length=20,
+ verbose_name="long name",
+ ),
+ ),
+ (
+ "short_name",
+ models.CharField(
+ blank=True,
+ help_text='More readable, but short, name to display publicly; e.g. "PY2" or "C++11". If left blank, it will default to the short identifier.',
+ max_length=10,
+ null=True,
+ verbose_name="short name",
+ ),
+ ),
+ (
+ "common_name",
+ models.CharField(
+ help_text='Common name for the language. For example, the common name for C++03, C++11, and C++14 would be "C++"',
+ max_length=10,
+ verbose_name="common name",
+ ),
+ ),
+ (
+ "ace",
+ models.CharField(
+ help_text='Language ID for Ace.js editor highlighting, appended to "mode-" to determine the Ace JavaScript file to use, e.g., "python".',
+ max_length=20,
+ verbose_name="ace mode name",
+ ),
+ ),
+ (
+ "pygments",
+ models.CharField(
+ help_text="Language ID for Pygments highlighting in source windows.",
+ max_length=20,
+ verbose_name="pygments name",
+ ),
+ ),
+ (
+ "template",
+ models.TextField(
+ blank=True,
+ help_text="Code template to display in submission editor.",
+ verbose_name="code template",
+ ),
+ ),
+ (
+ "info",
+ models.CharField(
+ blank=True,
+ help_text="Do not set this unless you know what you're doing! It will override the usually more specific, judge-provided runtime info!",
+ max_length=50,
+ verbose_name="runtime info override",
+ ),
+ ),
+ (
+ "description",
+ models.TextField(
+ blank=True,
+ help_text="Use field this to inform users of quirks with your environment, additional restrictions, etc.",
+ verbose_name="language description",
+ ),
+ ),
+ (
+ "extension",
+ models.CharField(
+ help_text='The extension of source files, e.g., "py" or "cpp".',
+ max_length=10,
+ verbose_name="extension",
+ ),
+ ),
],
options={
- 'ordering': ['key'],
- 'verbose_name_plural': 'languages',
- 'verbose_name': 'language',
+ "ordering": ["key"],
+ "verbose_name_plural": "languages",
+ "verbose_name": "language",
},
),
migrations.CreateModel(
- name='LanguageLimit',
+ name="LanguageLimit",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('time_limit', models.FloatField(verbose_name='time limit')),
- ('memory_limit', models.IntegerField(verbose_name='memory limit')),
- ('language', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Language', verbose_name='language')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("time_limit", models.FloatField(verbose_name="time limit")),
+ ("memory_limit", models.IntegerField(verbose_name="memory limit")),
+ (
+ "language",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="judge.Language",
+ verbose_name="language",
+ ),
+ ),
],
options={
- 'verbose_name_plural': 'language-specific resource limits',
- 'verbose_name': 'language-specific resource limit',
+ "verbose_name_plural": "language-specific resource limits",
+ "verbose_name": "language-specific resource limit",
},
),
migrations.CreateModel(
- name='License',
+ name="License",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('key', models.CharField(max_length=20, unique=True, validators=[django.core.validators.RegexValidator('^[-\\w.]+$', 'License key must be ^[-\\w.]+$')], verbose_name='key')),
- ('link', models.CharField(max_length=256, verbose_name='link')),
- ('name', models.CharField(max_length=256, verbose_name='full name')),
- ('display', models.CharField(blank=True, help_text='Displayed on pages under this license', max_length=256, verbose_name='short name')),
- ('icon', models.CharField(blank=True, help_text='URL to the icon', max_length=256, verbose_name='icon')),
- ('text', models.TextField(verbose_name='license text')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "key",
+ models.CharField(
+ max_length=20,
+ unique=True,
+ validators=[
+ django.core.validators.RegexValidator(
+ "^[-\\w.]+$", "License key must be ^[-\\w.]+$"
+ )
+ ],
+ verbose_name="key",
+ ),
+ ),
+ ("link", models.CharField(max_length=256, verbose_name="link")),
+ ("name", models.CharField(max_length=256, verbose_name="full name")),
+ (
+ "display",
+ models.CharField(
+ blank=True,
+ help_text="Displayed on pages under this license",
+ max_length=256,
+ verbose_name="short name",
+ ),
+ ),
+ (
+ "icon",
+ models.CharField(
+ blank=True,
+ help_text="URL to the icon",
+ max_length=256,
+ verbose_name="icon",
+ ),
+ ),
+ ("text", models.TextField(verbose_name="license text")),
],
options={
- 'verbose_name_plural': 'licenses',
- 'verbose_name': 'license',
+ "verbose_name_plural": "licenses",
+ "verbose_name": "license",
},
),
migrations.CreateModel(
- name='MiscConfig',
+ name="MiscConfig",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('key', models.CharField(db_index=True, max_length=30)),
- ('value', models.TextField(blank=True)),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("key", models.CharField(db_index=True, max_length=30)),
+ ("value", models.TextField(blank=True)),
],
options={
- 'verbose_name_plural': 'miscellaneous configuration',
- 'verbose_name': 'configuration item',
+ "verbose_name_plural": "miscellaneous configuration",
+ "verbose_name": "configuration item",
},
),
migrations.CreateModel(
- name='NavigationBar',
+ name="NavigationBar",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('order', models.PositiveIntegerField(db_index=True, verbose_name='order')),
- ('key', models.CharField(max_length=10, unique=True, verbose_name='identifier')),
- ('label', models.CharField(max_length=20, verbose_name='label')),
- ('path', models.CharField(max_length=255, verbose_name='link path')),
- ('regex', models.TextField(validators=[judge.models.interface.validate_regex], verbose_name='highlight regex')),
- ('lft', models.PositiveIntegerField(db_index=True, editable=False)),
- ('rght', models.PositiveIntegerField(db_index=True, editable=False)),
- ('tree_id', models.PositiveIntegerField(db_index=True, editable=False)),
- ('level', models.PositiveIntegerField(db_index=True, editable=False)),
- ('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='judge.NavigationBar', verbose_name='parent item')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "order",
+ models.PositiveIntegerField(db_index=True, verbose_name="order"),
+ ),
+ (
+ "key",
+ models.CharField(
+ max_length=10, unique=True, verbose_name="identifier"
+ ),
+ ),
+ ("label", models.CharField(max_length=20, verbose_name="label")),
+ ("path", models.CharField(max_length=255, verbose_name="link path")),
+ (
+ "regex",
+ models.TextField(
+ validators=[judge.models.interface.validate_regex],
+ verbose_name="highlight regex",
+ ),
+ ),
+ ("lft", models.PositiveIntegerField(db_index=True, editable=False)),
+ ("rght", models.PositiveIntegerField(db_index=True, editable=False)),
+ ("tree_id", models.PositiveIntegerField(db_index=True, editable=False)),
+ ("level", models.PositiveIntegerField(db_index=True, editable=False)),
+ (
+ "parent",
+ mptt.fields.TreeForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="children",
+ to="judge.NavigationBar",
+ verbose_name="parent item",
+ ),
+ ),
],
options={
- 'verbose_name_plural': 'navigation bar',
- 'verbose_name': 'navigation item',
+ "verbose_name_plural": "navigation bar",
+ "verbose_name": "navigation item",
},
),
migrations.CreateModel(
- name='Organization',
+ name="Organization",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('name', models.CharField(max_length=128, verbose_name='organization title')),
- ('slug', models.SlugField(help_text='Organization name shown in URL', max_length=128, verbose_name='organization slug')),
- ('short_name', models.CharField(help_text='Displayed beside user name during contests', max_length=20, verbose_name='short name')),
- ('about', models.TextField(verbose_name='organization description')),
- ('creation_date', models.DateTimeField(auto_now_add=True, verbose_name='creation date')),
- ('is_open', models.BooleanField(default=True, help_text='Allow joining organization', verbose_name='is open organization?')),
- ('slots', models.IntegerField(blank=True, help_text='Maximum amount of users in this organization, only applicable to private organizations', null=True, verbose_name='maximum size')),
- ('access_code', models.CharField(blank=True, help_text='Student access code', max_length=7, null=True, verbose_name='access code')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "name",
+ models.CharField(max_length=128, verbose_name="organization title"),
+ ),
+ (
+ "slug",
+ models.SlugField(
+ help_text="Organization name shown in URL",
+ max_length=128,
+ verbose_name="organization slug",
+ ),
+ ),
+ (
+ "short_name",
+ models.CharField(
+ help_text="Displayed beside user name during contests",
+ max_length=20,
+ verbose_name="short name",
+ ),
+ ),
+ ("about", models.TextField(verbose_name="organization description")),
+ (
+ "creation_date",
+ models.DateTimeField(
+ auto_now_add=True, verbose_name="creation date"
+ ),
+ ),
+ (
+ "is_open",
+ models.BooleanField(
+ default=True,
+ help_text="Allow joining organization",
+ verbose_name="is open organization?",
+ ),
+ ),
+ (
+ "slots",
+ models.IntegerField(
+ blank=True,
+ help_text="Maximum amount of users in this organization, only applicable to private organizations",
+ null=True,
+ verbose_name="maximum size",
+ ),
+ ),
+ (
+ "access_code",
+ models.CharField(
+ blank=True,
+ help_text="Student access code",
+ max_length=7,
+ null=True,
+ verbose_name="access code",
+ ),
+ ),
],
options={
- 'ordering': ['name'],
- 'verbose_name_plural': 'organizations',
- 'permissions': (('organization_admin', 'Administer organizations'), ('edit_all_organization', 'Edit all organizations')),
- 'verbose_name': 'organization',
+ "ordering": ["name"],
+ "verbose_name_plural": "organizations",
+ "permissions": (
+ ("organization_admin", "Administer organizations"),
+ ("edit_all_organization", "Edit all organizations"),
+ ),
+ "verbose_name": "organization",
},
),
migrations.CreateModel(
- name='OrganizationRequest',
+ name="OrganizationRequest",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('time', models.DateTimeField(auto_now_add=True, verbose_name='request time')),
- ('state', models.CharField(choices=[('P', 'Pending'), ('A', 'Approved'), ('R', 'Rejected')], max_length=1, verbose_name='state')),
- ('reason', models.TextField(verbose_name='reason')),
- ('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='requests', to='judge.Organization', verbose_name='organization')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "time",
+ models.DateTimeField(
+ auto_now_add=True, verbose_name="request time"
+ ),
+ ),
+ (
+ "state",
+ models.CharField(
+ choices=[
+ ("P", "Pending"),
+ ("A", "Approved"),
+ ("R", "Rejected"),
+ ],
+ max_length=1,
+ verbose_name="state",
+ ),
+ ),
+ ("reason", models.TextField(verbose_name="reason")),
+ (
+ "organization",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="requests",
+ to="judge.Organization",
+ verbose_name="organization",
+ ),
+ ),
],
options={
- 'verbose_name_plural': 'organization join requests',
- 'verbose_name': 'organization join request',
+ "verbose_name_plural": "organization join requests",
+ "verbose_name": "organization join request",
},
),
migrations.CreateModel(
- name='PrivateMessage',
+ name="PrivateMessage",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('title', models.CharField(max_length=50, verbose_name='message title')),
- ('content', models.TextField(verbose_name='message body')),
- ('timestamp', models.DateTimeField(auto_now_add=True, verbose_name='message timestamp')),
- ('read', models.BooleanField(default=False, verbose_name='read')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "title",
+ models.CharField(max_length=50, verbose_name="message title"),
+ ),
+ ("content", models.TextField(verbose_name="message body")),
+ (
+ "timestamp",
+ models.DateTimeField(
+ auto_now_add=True, verbose_name="message timestamp"
+ ),
+ ),
+ ("read", models.BooleanField(default=False, verbose_name="read")),
],
),
migrations.CreateModel(
- name='PrivateMessageThread',
+ name="PrivateMessageThread",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('messages', models.ManyToManyField(to='judge.PrivateMessage', verbose_name='messages in the thread')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "messages",
+ models.ManyToManyField(
+ to="judge.PrivateMessage", verbose_name="messages in the thread"
+ ),
+ ),
],
),
migrations.CreateModel(
- name='Problem',
+ name="Problem",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('code', models.CharField(max_length=20, unique=True, validators=[django.core.validators.RegexValidator('^[a-z0-9]+$', 'Problem code must be ^[a-z0-9]+$')], verbose_name='problem code')),
- ('name', models.CharField(db_index=True, max_length=100, verbose_name='problem name')),
- ('description', models.TextField(verbose_name='problem body')),
- ('time_limit', models.FloatField(help_text='The time limit for this problem, in seconds. Fractional seconds (e.g. 1.5) are supported.', verbose_name='time limit')),
- ('memory_limit', models.IntegerField(help_text='The memory limit for this problem, in kilobytes (e.g. 64mb = 65536 kilobytes).', verbose_name='memory limit')),
- ('short_circuit', models.BooleanField(default=False)),
- ('points', models.FloatField(verbose_name='points')),
- ('partial', models.BooleanField(default=False, verbose_name='allows partial points')),
- ('is_public', models.BooleanField(db_index=True, default=False, verbose_name='publicly visible')),
- ('is_manually_managed', models.BooleanField(db_index=True, default=False, help_text='Whether judges should be allowed to manage data or not', verbose_name='manually managed')),
- ('date', models.DateTimeField(blank=True, db_index=True, help_text="Doesn't have magic ability to auto-publish due to backward compatibility", null=True, verbose_name='date of publishing')),
- ('og_image', models.CharField(blank=True, max_length=150, verbose_name='OpenGraph image')),
- ('summary', models.TextField(blank=True, help_text='Plain-text, shown in meta description tag, e.g. for social media.', verbose_name='problem summary')),
- ('user_count', models.IntegerField(default=0, help_text='The number of users who solved the problem.', verbose_name='number of users')),
- ('ac_rate', models.FloatField(default=0, verbose_name='solve rate')),
- ('is_organization_private', models.BooleanField(default=False, verbose_name='private to organizations')),
- ('allowed_languages', models.ManyToManyField(to='judge.Language', verbose_name='allowed languages')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "code",
+ models.CharField(
+ max_length=20,
+ unique=True,
+ validators=[
+ django.core.validators.RegexValidator(
+ "^[a-z0-9]+$", "Problem code must be ^[a-z0-9]+$"
+ )
+ ],
+ verbose_name="problem code",
+ ),
+ ),
+ (
+ "name",
+ models.CharField(
+ db_index=True, max_length=100, verbose_name="problem name"
+ ),
+ ),
+ ("description", models.TextField(verbose_name="problem body")),
+ (
+ "time_limit",
+ models.FloatField(
+ help_text="The time limit for this problem, in seconds. Fractional seconds (e.g. 1.5) are supported.",
+ verbose_name="time limit",
+ ),
+ ),
+ (
+ "memory_limit",
+ models.IntegerField(
+ help_text="The memory limit for this problem, in kilobytes (e.g. 64mb = 65536 kilobytes).",
+ verbose_name="memory limit",
+ ),
+ ),
+ ("short_circuit", models.BooleanField(default=False)),
+ ("points", models.FloatField(verbose_name="points")),
+ (
+ "partial",
+ models.BooleanField(
+ default=False, verbose_name="allows partial points"
+ ),
+ ),
+ (
+ "is_public",
+ models.BooleanField(
+ db_index=True, default=False, verbose_name="publicly visible"
+ ),
+ ),
+ (
+ "is_manually_managed",
+ models.BooleanField(
+ db_index=True,
+ default=False,
+ help_text="Whether judges should be allowed to manage data or not",
+ verbose_name="manually managed",
+ ),
+ ),
+ (
+ "date",
+ models.DateTimeField(
+ blank=True,
+ db_index=True,
+ help_text="Doesn't have magic ability to auto-publish due to backward compatibility",
+ null=True,
+ verbose_name="date of publishing",
+ ),
+ ),
+ (
+ "og_image",
+ models.CharField(
+ blank=True, max_length=150, verbose_name="OpenGraph image"
+ ),
+ ),
+ (
+ "summary",
+ models.TextField(
+ blank=True,
+ help_text="Plain-text, shown in meta description tag, e.g. for social media.",
+ verbose_name="problem summary",
+ ),
+ ),
+ (
+ "user_count",
+ models.IntegerField(
+ default=0,
+ help_text="The number of users who solved the problem.",
+ verbose_name="number of users",
+ ),
+ ),
+ ("ac_rate", models.FloatField(default=0, verbose_name="solve rate")),
+ (
+ "is_organization_private",
+ models.BooleanField(
+ default=False, verbose_name="private to organizations"
+ ),
+ ),
+ (
+ "allowed_languages",
+ models.ManyToManyField(
+ to="judge.Language", verbose_name="allowed languages"
+ ),
+ ),
],
options={
- 'verbose_name_plural': 'problems',
- 'permissions': (('see_private_problem', 'See hidden problems'), ('edit_own_problem', 'Edit own problems'), ('edit_all_problem', 'Edit all problems'), ('edit_public_problem', 'Edit all public problems'), ('clone_problem', 'Clone problem'), ('change_public_visibility', 'Change is_public field'), ('change_manually_managed', 'Change is_manually_managed field'), ('see_organization_problem', 'See organization-private problems')),
- 'verbose_name': 'problem',
+ "verbose_name_plural": "problems",
+ "permissions": (
+ ("see_private_problem", "See hidden problems"),
+ ("edit_own_problem", "Edit own problems"),
+ ("edit_all_problem", "Edit all problems"),
+ ("edit_public_problem", "Edit all public problems"),
+ ("clone_problem", "Clone problem"),
+ ("change_public_visibility", "Change is_public field"),
+ ("change_manually_managed", "Change is_manually_managed field"),
+ ("see_organization_problem", "See organization-private problems"),
+ ),
+ "verbose_name": "problem",
},
),
migrations.CreateModel(
- name='ProblemClarification',
+ name="ProblemClarification",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('description', models.TextField(verbose_name='clarification body')),
- ('date', models.DateTimeField(auto_now_add=True, verbose_name='clarification timestamp')),
- ('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Problem', verbose_name='clarified problem')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("description", models.TextField(verbose_name="clarification body")),
+ (
+ "date",
+ models.DateTimeField(
+ auto_now_add=True, verbose_name="clarification timestamp"
+ ),
+ ),
+ (
+ "problem",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="judge.Problem",
+ verbose_name="clarified problem",
+ ),
+ ),
],
),
migrations.CreateModel(
- name='ProblemData',
+ name="ProblemData",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('zipfile', models.FileField(blank=True, null=True, storage=judge.utils.problem_data.ProblemDataStorage(), upload_to=judge.models.problem_data.problem_directory_file, verbose_name='data zip file')),
- ('generator', models.FileField(blank=True, null=True, storage=judge.utils.problem_data.ProblemDataStorage(), upload_to=judge.models.problem_data.problem_directory_file, verbose_name='generator file')),
- ('output_prefix', models.IntegerField(blank=True, null=True, verbose_name='output prefix length')),
- ('output_limit', models.IntegerField(blank=True, null=True, verbose_name='output limit length')),
- ('feedback', models.TextField(blank=True, verbose_name='init.yml generation feedback')),
- ('checker', models.CharField(blank=True, choices=[('standard', 'Standard'), ('floats', 'Floats'), ('floatsabs', 'Floats (absolute)'), ('floatsrel', 'Floats (relative)'), ('rstripped', 'Non-trailing spaces'), ('sorted', 'Unordered'), ('identical', 'Byte identical'), ('linecount', 'Line-by-line')], max_length=10, verbose_name='checker')),
- ('checker_args', models.TextField(blank=True, help_text='checker arguments as a JSON object', verbose_name='checker arguments')),
- ('problem', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='data_files', to='judge.Problem', verbose_name='problem')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "zipfile",
+ models.FileField(
+ blank=True,
+ null=True,
+ storage=judge.utils.problem_data.ProblemDataStorage(),
+ upload_to=judge.models.problem_data.problem_directory_file,
+ verbose_name="data zip file",
+ ),
+ ),
+ (
+ "generator",
+ models.FileField(
+ blank=True,
+ null=True,
+ storage=judge.utils.problem_data.ProblemDataStorage(),
+ upload_to=judge.models.problem_data.problem_directory_file,
+ verbose_name="generator file",
+ ),
+ ),
+ (
+ "output_prefix",
+ models.IntegerField(
+ blank=True, null=True, verbose_name="output prefix length"
+ ),
+ ),
+ (
+ "output_limit",
+ models.IntegerField(
+ blank=True, null=True, verbose_name="output limit length"
+ ),
+ ),
+ (
+ "feedback",
+ models.TextField(
+ blank=True, verbose_name="init.yml generation feedback"
+ ),
+ ),
+ (
+ "checker",
+ models.CharField(
+ blank=True,
+ choices=[
+ ("standard", "Standard"),
+ ("floats", "Floats"),
+ ("floatsabs", "Floats (absolute)"),
+ ("floatsrel", "Floats (relative)"),
+ ("rstripped", "Non-trailing spaces"),
+ ("sorted", "Unordered"),
+ ("identical", "Byte identical"),
+ ("linecount", "Line-by-line"),
+ ],
+ max_length=10,
+ verbose_name="checker",
+ ),
+ ),
+ (
+ "checker_args",
+ models.TextField(
+ blank=True,
+ help_text="checker arguments as a JSON object",
+ verbose_name="checker arguments",
+ ),
+ ),
+ (
+ "problem",
+ models.OneToOneField(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="data_files",
+ to="judge.Problem",
+ verbose_name="problem",
+ ),
+ ),
],
),
migrations.CreateModel(
- name='ProblemGroup',
+ name="ProblemGroup",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('name', models.CharField(max_length=20, unique=True, verbose_name='problem group ID')),
- ('full_name', models.CharField(max_length=100, verbose_name='problem group name')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "name",
+ models.CharField(
+ max_length=20, unique=True, verbose_name="problem group ID"
+ ),
+ ),
+ (
+ "full_name",
+ models.CharField(max_length=100, verbose_name="problem group name"),
+ ),
],
options={
- 'ordering': ['full_name'],
- 'verbose_name_plural': 'problem groups',
- 'verbose_name': 'problem group',
+ "ordering": ["full_name"],
+ "verbose_name_plural": "problem groups",
+ "verbose_name": "problem group",
},
),
migrations.CreateModel(
- name='ProblemTestCase',
+ name="ProblemTestCase",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('order', models.IntegerField(verbose_name='case position')),
- ('type', models.CharField(choices=[('C', 'Normal case'), ('S', 'Batch start'), ('E', 'Batch end')], default='C', max_length=1, verbose_name='case type')),
- ('input_file', models.CharField(blank=True, max_length=100, verbose_name='input file name')),
- ('output_file', models.CharField(blank=True, max_length=100, verbose_name='output file name')),
- ('generator_args', models.TextField(blank=True, verbose_name='generator arguments')),
- ('points', models.IntegerField(blank=True, null=True, verbose_name='point value')),
- ('is_pretest', models.BooleanField(verbose_name='case is pretest?')),
- ('output_prefix', models.IntegerField(blank=True, null=True, verbose_name='output prefix length')),
- ('output_limit', models.IntegerField(blank=True, null=True, verbose_name='output limit length')),
- ('checker', models.CharField(blank=True, choices=[('standard', 'Standard'), ('floats', 'Floats'), ('floatsabs', 'Floats (absolute)'), ('floatsrel', 'Floats (relative)'), ('rstripped', 'Non-trailing spaces'), ('sorted', 'Unordered'), ('identical', 'Byte identical'), ('linecount', 'Line-by-line')], max_length=10, verbose_name='checker')),
- ('checker_args', models.TextField(blank=True, help_text='checker arguments as a JSON object', verbose_name='checker arguments')),
- ('dataset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cases', to='judge.Problem', verbose_name='problem data set')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("order", models.IntegerField(verbose_name="case position")),
+ (
+ "type",
+ models.CharField(
+ choices=[
+ ("C", "Normal case"),
+ ("S", "Batch start"),
+ ("E", "Batch end"),
+ ],
+ default="C",
+ max_length=1,
+ verbose_name="case type",
+ ),
+ ),
+ (
+ "input_file",
+ models.CharField(
+ blank=True, max_length=100, verbose_name="input file name"
+ ),
+ ),
+ (
+ "output_file",
+ models.CharField(
+ blank=True, max_length=100, verbose_name="output file name"
+ ),
+ ),
+ (
+ "generator_args",
+ models.TextField(blank=True, verbose_name="generator arguments"),
+ ),
+ (
+ "points",
+ models.IntegerField(
+ blank=True, null=True, verbose_name="point value"
+ ),
+ ),
+ ("is_pretest", models.BooleanField(verbose_name="case is pretest?")),
+ (
+ "output_prefix",
+ models.IntegerField(
+ blank=True, null=True, verbose_name="output prefix length"
+ ),
+ ),
+ (
+ "output_limit",
+ models.IntegerField(
+ blank=True, null=True, verbose_name="output limit length"
+ ),
+ ),
+ (
+ "checker",
+ models.CharField(
+ blank=True,
+ choices=[
+ ("standard", "Standard"),
+ ("floats", "Floats"),
+ ("floatsabs", "Floats (absolute)"),
+ ("floatsrel", "Floats (relative)"),
+ ("rstripped", "Non-trailing spaces"),
+ ("sorted", "Unordered"),
+ ("identical", "Byte identical"),
+ ("linecount", "Line-by-line"),
+ ],
+ max_length=10,
+ verbose_name="checker",
+ ),
+ ),
+ (
+ "checker_args",
+ models.TextField(
+ blank=True,
+ help_text="checker arguments as a JSON object",
+ verbose_name="checker arguments",
+ ),
+ ),
+ (
+ "dataset",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="cases",
+ to="judge.Problem",
+ verbose_name="problem data set",
+ ),
+ ),
],
),
migrations.CreateModel(
- name='ProblemTranslation',
+ name="ProblemTranslation",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('language', models.CharField(choices=[('de', 'German'), ('en', 'English'), ('es', 'Spanish'), ('fr', 'French'), ('hr', 'Croatian'), ('hu', 'Hungarian'), ('ko', 'Korean'), ('ro', 'Romanian'), ('ru', 'Russian'), ('sr-latn', 'Serbian (Latin)'), ('tr', 'Turkish'), ('vi', 'Vietnamese'), ('zh-hans', 'Simplified Chinese')], max_length=7, verbose_name='language')),
- ('name', models.CharField(db_index=True, max_length=100, verbose_name='translated name')),
- ('description', models.TextField(verbose_name='translated description')),
- ('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='translations', to='judge.Problem', verbose_name='problem')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "language",
+ models.CharField(
+ choices=[
+ ("de", "German"),
+ ("en", "English"),
+ ("es", "Spanish"),
+ ("fr", "French"),
+ ("hr", "Croatian"),
+ ("hu", "Hungarian"),
+ ("ko", "Korean"),
+ ("ro", "Romanian"),
+ ("ru", "Russian"),
+ ("sr-latn", "Serbian (Latin)"),
+ ("tr", "Turkish"),
+ ("vi", "Vietnamese"),
+ ("zh-hans", "Simplified Chinese"),
+ ],
+ max_length=7,
+ verbose_name="language",
+ ),
+ ),
+ (
+ "name",
+ models.CharField(
+ db_index=True, max_length=100, verbose_name="translated name"
+ ),
+ ),
+ (
+ "description",
+ models.TextField(verbose_name="translated description"),
+ ),
+ (
+ "problem",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="translations",
+ to="judge.Problem",
+ verbose_name="problem",
+ ),
+ ),
],
options={
- 'verbose_name_plural': 'problem translations',
- 'verbose_name': 'problem translation',
+ "verbose_name_plural": "problem translations",
+ "verbose_name": "problem translation",
},
),
migrations.CreateModel(
- name='ProblemType',
+ name="ProblemType",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('name', models.CharField(max_length=20, unique=True, verbose_name='problem category ID')),
- ('full_name', models.CharField(max_length=100, verbose_name='problem category name')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "name",
+ models.CharField(
+ max_length=20, unique=True, verbose_name="problem category ID"
+ ),
+ ),
+ (
+ "full_name",
+ models.CharField(
+ max_length=100, verbose_name="problem category name"
+ ),
+ ),
],
options={
- 'ordering': ['full_name'],
- 'verbose_name_plural': 'problem types',
- 'verbose_name': 'problem type',
+ "ordering": ["full_name"],
+ "verbose_name_plural": "problem types",
+ "verbose_name": "problem type",
},
),
migrations.CreateModel(
- name='Profile',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('about', models.TextField(blank=True, null=True, verbose_name='self-description')),
- ('timezone', models.CharField(choices=[('Africa', [('Africa/Abidjan', 'Abidjan'), ('Africa/Accra', 'Accra'), ('Africa/Addis_Ababa', 'Addis_Ababa'), ('Africa/Algiers', 'Algiers'), ('Africa/Asmara', 'Asmara'), ('Africa/Asmera', 'Asmera'), ('Africa/Bamako', 'Bamako'), ('Africa/Bangui', 'Bangui'), ('Africa/Banjul', 'Banjul'), ('Africa/Bissau', 'Bissau'), ('Africa/Blantyre', 'Blantyre'), ('Africa/Brazzaville', 'Brazzaville'), ('Africa/Bujumbura', 'Bujumbura'), ('Africa/Cairo', 'Cairo'), ('Africa/Casablanca', 'Casablanca'), ('Africa/Ceuta', 'Ceuta'), ('Africa/Conakry', 'Conakry'), ('Africa/Dakar', 'Dakar'), ('Africa/Dar_es_Salaam', 'Dar_es_Salaam'), ('Africa/Djibouti', 'Djibouti'), ('Africa/Douala', 'Douala'), ('Africa/El_Aaiun', 'El_Aaiun'), ('Africa/Freetown', 'Freetown'), ('Africa/Gaborone', 'Gaborone'), ('Africa/Harare', 'Harare'), ('Africa/Johannesburg', 'Johannesburg'), ('Africa/Juba', 'Juba'), ('Africa/Kampala', 'Kampala'), ('Africa/Khartoum', 'Khartoum'), ('Africa/Kigali', 'Kigali'), ('Africa/Kinshasa', 'Kinshasa'), ('Africa/Lagos', 'Lagos'), ('Africa/Libreville', 'Libreville'), ('Africa/Lome', 'Lome'), ('Africa/Luanda', 'Luanda'), ('Africa/Lubumbashi', 'Lubumbashi'), ('Africa/Lusaka', 'Lusaka'), ('Africa/Malabo', 'Malabo'), ('Africa/Maputo', 'Maputo'), ('Africa/Maseru', 'Maseru'), ('Africa/Mbabane', 'Mbabane'), ('Africa/Mogadishu', 'Mogadishu'), ('Africa/Monrovia', 'Monrovia'), ('Africa/Nairobi', 'Nairobi'), ('Africa/Ndjamena', 'Ndjamena'), ('Africa/Niamey', 'Niamey'), ('Africa/Nouakchott', 'Nouakchott'), ('Africa/Ouagadougou', 'Ouagadougou'), ('Africa/Porto-Novo', 'Porto-Novo'), ('Africa/Sao_Tome', 'Sao_Tome'), ('Africa/Timbuktu', 'Timbuktu'), ('Africa/Tripoli', 'Tripoli'), ('Africa/Tunis', 'Tunis'), ('Africa/Windhoek', 'Windhoek')]), ('America', [('America/Adak', 'Adak'), ('America/Anchorage', 'Anchorage'), ('America/Anguilla', 'Anguilla'), ('America/Antigua', 'Antigua'), ('America/Araguaina', 'Araguaina'), ('America/Argentina/Buenos_Aires', 'Argentina/Buenos_Aires'), ('America/Argentina/Catamarca', 'Argentina/Catamarca'), ('America/Argentina/ComodRivadavia', 'Argentina/ComodRivadavia'), ('America/Argentina/Cordoba', 'Argentina/Cordoba'), ('America/Argentina/Jujuy', 'Argentina/Jujuy'), ('America/Argentina/La_Rioja', 'Argentina/La_Rioja'), ('America/Argentina/Mendoza', 'Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', 'Argentina/Rio_Gallegos'), ('America/Argentina/Salta', 'Argentina/Salta'), ('America/Argentina/San_Juan', 'Argentina/San_Juan'), ('America/Argentina/San_Luis', 'Argentina/San_Luis'), ('America/Argentina/Tucuman', 'Argentina/Tucuman'), ('America/Argentina/Ushuaia', 'Argentina/Ushuaia'), ('America/Aruba', 'Aruba'), ('America/Asuncion', 'Asuncion'), ('America/Atikokan', 'Atikokan'), ('America/Atka', 'Atka'), ('America/Bahia', 'Bahia'), ('America/Bahia_Banderas', 'Bahia_Banderas'), ('America/Barbados', 'Barbados'), ('America/Belem', 'Belem'), ('America/Belize', 'Belize'), ('America/Blanc-Sablon', 'Blanc-Sablon'), ('America/Boa_Vista', 'Boa_Vista'), ('America/Bogota', 'Bogota'), ('America/Boise', 'Boise'), ('America/Buenos_Aires', 'Buenos_Aires'), ('America/Cambridge_Bay', 'Cambridge_Bay'), ('America/Campo_Grande', 'Campo_Grande'), ('America/Cancun', 'Cancun'), ('America/Caracas', 'Caracas'), ('America/Catamarca', 'Catamarca'), ('America/Cayenne', 'Cayenne'), ('America/Cayman', 'Cayman'), ('America/Chicago', 'Chicago'), ('America/Chihuahua', 'Chihuahua'), ('America/Coral_Harbour', 'Coral_Harbour'), ('America/Cordoba', 'Cordoba'), ('America/Costa_Rica', 'Costa_Rica'), ('America/Creston', 'Creston'), ('America/Cuiaba', 'Cuiaba'), ('America/Curacao', 'Curacao'), ('America/Danmarkshavn', 'Danmarkshavn'), ('America/Dawson', 'Dawson'), ('America/Dawson_Creek', 'Dawson_Creek'), ('America/Denver', 'Denver'), ('America/Detroit', 'Detroit'), ('America/Dominica', 'Dominica'), ('America/Edmonton', 'Edmonton'), ('America/Eirunepe', 'Eirunepe'), ('America/El_Salvador', 'El_Salvador'), ('America/Ensenada', 'Ensenada'), ('America/Fort_Nelson', 'Fort_Nelson'), ('America/Fort_Wayne', 'Fort_Wayne'), ('America/Fortaleza', 'Fortaleza'), ('America/Glace_Bay', 'Glace_Bay'), ('America/Godthab', 'Godthab'), ('America/Goose_Bay', 'Goose_Bay'), ('America/Grand_Turk', 'Grand_Turk'), ('America/Grenada', 'Grenada'), ('America/Guadeloupe', 'Guadeloupe'), ('America/Guatemala', 'Guatemala'), ('America/Guayaquil', 'Guayaquil'), ('America/Guyana', 'Guyana'), ('America/Halifax', 'Halifax'), ('America/Havana', 'Havana'), ('America/Hermosillo', 'Hermosillo'), ('America/Indiana/Indianapolis', 'Indiana/Indianapolis'), ('America/Indiana/Knox', 'Indiana/Knox'), ('America/Indiana/Marengo', 'Indiana/Marengo'), ('America/Indiana/Petersburg', 'Indiana/Petersburg'), ('America/Indiana/Tell_City', 'Indiana/Tell_City'), ('America/Indiana/Vevay', 'Indiana/Vevay'), ('America/Indiana/Vincennes', 'Indiana/Vincennes'), ('America/Indiana/Winamac', 'Indiana/Winamac'), ('America/Indianapolis', 'Indianapolis'), ('America/Inuvik', 'Inuvik'), ('America/Iqaluit', 'Iqaluit'), ('America/Jamaica', 'Jamaica'), ('America/Jujuy', 'Jujuy'), ('America/Juneau', 'Juneau'), ('America/Kentucky/Louisville', 'Kentucky/Louisville'), ('America/Kentucky/Monticello', 'Kentucky/Monticello'), ('America/Knox_IN', 'Knox_IN'), ('America/Kralendijk', 'Kralendijk'), ('America/La_Paz', 'La_Paz'), ('America/Lima', 'Lima'), ('America/Los_Angeles', 'Los_Angeles'), ('America/Louisville', 'Louisville'), ('America/Lower_Princes', 'Lower_Princes'), ('America/Maceio', 'Maceio'), ('America/Managua', 'Managua'), ('America/Manaus', 'Manaus'), ('America/Marigot', 'Marigot'), ('America/Martinique', 'Martinique'), ('America/Matamoros', 'Matamoros'), ('America/Mazatlan', 'Mazatlan'), ('America/Mendoza', 'Mendoza'), ('America/Menominee', 'Menominee'), ('America/Merida', 'Merida'), ('America/Metlakatla', 'Metlakatla'), ('America/Mexico_City', 'Mexico_City'), ('America/Miquelon', 'Miquelon'), ('America/Moncton', 'Moncton'), ('America/Monterrey', 'Monterrey'), ('America/Montevideo', 'Montevideo'), ('America/Montreal', 'Montreal'), ('America/Montserrat', 'Montserrat'), ('America/Nassau', 'Nassau'), ('America/New_York', 'New_York'), ('America/Nipigon', 'Nipigon'), ('America/Nome', 'Nome'), ('America/Noronha', 'Noronha'), ('America/North_Dakota/Beulah', 'North_Dakota/Beulah'), ('America/North_Dakota/Center', 'North_Dakota/Center'), ('America/North_Dakota/New_Salem', 'North_Dakota/New_Salem'), ('America/Ojinaga', 'Ojinaga'), ('America/Panama', 'Panama'), ('America/Pangnirtung', 'Pangnirtung'), ('America/Paramaribo', 'Paramaribo'), ('America/Phoenix', 'Phoenix'), ('America/Port-au-Prince', 'Port-au-Prince'), ('America/Port_of_Spain', 'Port_of_Spain'), ('America/Porto_Acre', 'Porto_Acre'), ('America/Porto_Velho', 'Porto_Velho'), ('America/Puerto_Rico', 'Puerto_Rico'), ('America/Punta_Arenas', 'Punta_Arenas'), ('America/Rainy_River', 'Rainy_River'), ('America/Rankin_Inlet', 'Rankin_Inlet'), ('America/Recife', 'Recife'), ('America/Regina', 'Regina'), ('America/Resolute', 'Resolute'), ('America/Rio_Branco', 'Rio_Branco'), ('America/Rosario', 'Rosario'), ('America/Santa_Isabel', 'Santa_Isabel'), ('America/Santarem', 'Santarem'), ('America/Santiago', 'Santiago'), ('America/Santo_Domingo', 'Santo_Domingo'), ('America/Sao_Paulo', 'Sao_Paulo'), ('America/Scoresbysund', 'Scoresbysund'), ('America/Shiprock', 'Shiprock'), ('America/Sitka', 'Sitka'), ('America/St_Barthelemy', 'St_Barthelemy'), ('America/St_Johns', 'St_Johns'), ('America/St_Kitts', 'St_Kitts'), ('America/St_Lucia', 'St_Lucia'), ('America/St_Thomas', 'St_Thomas'), ('America/St_Vincent', 'St_Vincent'), ('America/Swift_Current', 'Swift_Current'), ('America/Tegucigalpa', 'Tegucigalpa'), ('America/Thule', 'Thule'), ('America/Thunder_Bay', 'Thunder_Bay'), ('America/Tijuana', 'Tijuana'), ('America/Toronto', 'Toronto'), ('America/Tortola', 'Tortola'), ('America/Vancouver', 'Vancouver'), ('America/Virgin', 'Virgin'), ('America/Whitehorse', 'Whitehorse'), ('America/Winnipeg', 'Winnipeg'), ('America/Yakutat', 'Yakutat'), ('America/Yellowknife', 'Yellowknife')]), ('Antarctica', [('Antarctica/Casey', 'Casey'), ('Antarctica/Davis', 'Davis'), ('Antarctica/DumontDUrville', 'DumontDUrville'), ('Antarctica/Macquarie', 'Macquarie'), ('Antarctica/Mawson', 'Mawson'), ('Antarctica/McMurdo', 'McMurdo'), ('Antarctica/Palmer', 'Palmer'), ('Antarctica/Rothera', 'Rothera'), ('Antarctica/South_Pole', 'South_Pole'), ('Antarctica/Syowa', 'Syowa'), ('Antarctica/Troll', 'Troll'), ('Antarctica/Vostok', 'Vostok')]), ('Arctic', [('Arctic/Longyearbyen', 'Longyearbyen')]), ('Asia', [('Asia/Aden', 'Aden'), ('Asia/Almaty', 'Almaty'), ('Asia/Amman', 'Amman'), ('Asia/Anadyr', 'Anadyr'), ('Asia/Aqtau', 'Aqtau'), ('Asia/Aqtobe', 'Aqtobe'), ('Asia/Ashgabat', 'Ashgabat'), ('Asia/Ashkhabad', 'Ashkhabad'), ('Asia/Atyrau', 'Atyrau'), ('Asia/Baghdad', 'Baghdad'), ('Asia/Bahrain', 'Bahrain'), ('Asia/Baku', 'Baku'), ('Asia/Bangkok', 'Bangkok'), ('Asia/Barnaul', 'Barnaul'), ('Asia/Beirut', 'Beirut'), ('Asia/Bishkek', 'Bishkek'), ('Asia/Brunei', 'Brunei'), ('Asia/Calcutta', 'Calcutta'), ('Asia/Chita', 'Chita'), ('Asia/Choibalsan', 'Choibalsan'), ('Asia/Chongqing', 'Chongqing'), ('Asia/Chungking', 'Chungking'), ('Asia/Colombo', 'Colombo'), ('Asia/Dacca', 'Dacca'), ('Asia/Damascus', 'Damascus'), ('Asia/Dhaka', 'Dhaka'), ('Asia/Dili', 'Dili'), ('Asia/Dubai', 'Dubai'), ('Asia/Dushanbe', 'Dushanbe'), ('Asia/Famagusta', 'Famagusta'), ('Asia/Gaza', 'Gaza'), ('Asia/Harbin', 'Harbin'), ('Asia/Hebron', 'Hebron'), ('Asia/Ho_Chi_Minh', 'Ho_Chi_Minh'), ('Asia/Hong_Kong', 'Hong_Kong'), ('Asia/Hovd', 'Hovd'), ('Asia/Irkutsk', 'Irkutsk'), ('Asia/Istanbul', 'Istanbul'), ('Asia/Jakarta', 'Jakarta'), ('Asia/Jayapura', 'Jayapura'), ('Asia/Jerusalem', 'Jerusalem'), ('Asia/Kabul', 'Kabul'), ('Asia/Kamchatka', 'Kamchatka'), ('Asia/Karachi', 'Karachi'), ('Asia/Kashgar', 'Kashgar'), ('Asia/Kathmandu', 'Kathmandu'), ('Asia/Katmandu', 'Katmandu'), ('Asia/Khandyga', 'Khandyga'), ('Asia/Kolkata', 'Kolkata'), ('Asia/Krasnoyarsk', 'Krasnoyarsk'), ('Asia/Kuala_Lumpur', 'Kuala_Lumpur'), ('Asia/Kuching', 'Kuching'), ('Asia/Kuwait', 'Kuwait'), ('Asia/Macao', 'Macao'), ('Asia/Macau', 'Macau'), ('Asia/Magadan', 'Magadan'), ('Asia/Makassar', 'Makassar'), ('Asia/Manila', 'Manila'), ('Asia/Muscat', 'Muscat'), ('Asia/Nicosia', 'Nicosia'), ('Asia/Novokuznetsk', 'Novokuznetsk'), ('Asia/Novosibirsk', 'Novosibirsk'), ('Asia/Omsk', 'Omsk'), ('Asia/Oral', 'Oral'), ('Asia/Phnom_Penh', 'Phnom_Penh'), ('Asia/Pontianak', 'Pontianak'), ('Asia/Pyongyang', 'Pyongyang'), ('Asia/Qatar', 'Qatar'), ('Asia/Qostanay', 'Qostanay'), ('Asia/Qyzylorda', 'Qyzylorda'), ('Asia/Rangoon', 'Rangoon'), ('Asia/Riyadh', 'Riyadh'), ('Asia/Saigon', 'Saigon'), ('Asia/Sakhalin', 'Sakhalin'), ('Asia/Samarkand', 'Samarkand'), ('Asia/Seoul', 'Seoul'), ('Asia/Shanghai', 'Shanghai'), ('Asia/Singapore', 'Singapore'), ('Asia/Srednekolymsk', 'Srednekolymsk'), ('Asia/Taipei', 'Taipei'), ('Asia/Tashkent', 'Tashkent'), ('Asia/Tbilisi', 'Tbilisi'), ('Asia/Tehran', 'Tehran'), ('Asia/Tel_Aviv', 'Tel_Aviv'), ('Asia/Thimbu', 'Thimbu'), ('Asia/Thimphu', 'Thimphu'), ('Asia/Tokyo', 'Tokyo'), ('Asia/Tomsk', 'Tomsk'), ('Asia/Ujung_Pandang', 'Ujung_Pandang'), ('Asia/Ulaanbaatar', 'Ulaanbaatar'), ('Asia/Ulan_Bator', 'Ulan_Bator'), ('Asia/Urumqi', 'Urumqi'), ('Asia/Ust-Nera', 'Ust-Nera'), ('Asia/Vientiane', 'Vientiane'), ('Asia/Vladivostok', 'Vladivostok'), ('Asia/Yakutsk', 'Yakutsk'), ('Asia/Yangon', 'Yangon'), ('Asia/Yekaterinburg', 'Yekaterinburg'), ('Asia/Yerevan', 'Yerevan')]), ('Atlantic', [('Atlantic/Azores', 'Azores'), ('Atlantic/Bermuda', 'Bermuda'), ('Atlantic/Canary', 'Canary'), ('Atlantic/Cape_Verde', 'Cape_Verde'), ('Atlantic/Faeroe', 'Faeroe'), ('Atlantic/Faroe', 'Faroe'), ('Atlantic/Jan_Mayen', 'Jan_Mayen'), ('Atlantic/Madeira', 'Madeira'), ('Atlantic/Reykjavik', 'Reykjavik'), ('Atlantic/South_Georgia', 'South_Georgia'), ('Atlantic/St_Helena', 'St_Helena'), ('Atlantic/Stanley', 'Stanley')]), ('Australia', [('Australia/ACT', 'ACT'), ('Australia/Adelaide', 'Adelaide'), ('Australia/Brisbane', 'Brisbane'), ('Australia/Broken_Hill', 'Broken_Hill'), ('Australia/Canberra', 'Canberra'), ('Australia/Currie', 'Currie'), ('Australia/Darwin', 'Darwin'), ('Australia/Eucla', 'Eucla'), ('Australia/Hobart', 'Hobart'), ('Australia/LHI', 'LHI'), ('Australia/Lindeman', 'Lindeman'), ('Australia/Lord_Howe', 'Lord_Howe'), ('Australia/Melbourne', 'Melbourne'), ('Australia/NSW', 'NSW'), ('Australia/North', 'North'), ('Australia/Perth', 'Perth'), ('Australia/Queensland', 'Queensland'), ('Australia/South', 'South'), ('Australia/Sydney', 'Sydney'), ('Australia/Tasmania', 'Tasmania'), ('Australia/Victoria', 'Victoria'), ('Australia/West', 'West'), ('Australia/Yancowinna', 'Yancowinna')]), ('Brazil', [('Brazil/Acre', 'Acre'), ('Brazil/DeNoronha', 'DeNoronha'), ('Brazil/East', 'East'), ('Brazil/West', 'West')]), ('Canada', [('Canada/Atlantic', 'Atlantic'), ('Canada/Central', 'Central'), ('Canada/Eastern', 'Eastern'), ('Canada/Mountain', 'Mountain'), ('Canada/Newfoundland', 'Newfoundland'), ('Canada/Pacific', 'Pacific'), ('Canada/Saskatchewan', 'Saskatchewan'), ('Canada/Yukon', 'Yukon')]), ('Chile', [('Chile/Continental', 'Continental'), ('Chile/EasterIsland', 'EasterIsland')]), ('Etc', [('Etc/Greenwich', 'Greenwich'), ('Etc/UCT', 'UCT'), ('Etc/UTC', 'UTC'), ('Etc/Universal', 'Universal'), ('Etc/Zulu', 'Zulu')]), ('Europe', [('Europe/Amsterdam', 'Amsterdam'), ('Europe/Andorra', 'Andorra'), ('Europe/Astrakhan', 'Astrakhan'), ('Europe/Athens', 'Athens'), ('Europe/Belfast', 'Belfast'), ('Europe/Belgrade', 'Belgrade'), ('Europe/Berlin', 'Berlin'), ('Europe/Bratislava', 'Bratislava'), ('Europe/Brussels', 'Brussels'), ('Europe/Bucharest', 'Bucharest'), ('Europe/Budapest', 'Budapest'), ('Europe/Busingen', 'Busingen'), ('Europe/Chisinau', 'Chisinau'), ('Europe/Copenhagen', 'Copenhagen'), ('Europe/Dublin', 'Dublin'), ('Europe/Gibraltar', 'Gibraltar'), ('Europe/Guernsey', 'Guernsey'), ('Europe/Helsinki', 'Helsinki'), ('Europe/Isle_of_Man', 'Isle_of_Man'), ('Europe/Istanbul', 'Istanbul'), ('Europe/Jersey', 'Jersey'), ('Europe/Kaliningrad', 'Kaliningrad'), ('Europe/Kiev', 'Kiev'), ('Europe/Kirov', 'Kirov'), ('Europe/Lisbon', 'Lisbon'), ('Europe/Ljubljana', 'Ljubljana'), ('Europe/London', 'London'), ('Europe/Luxembourg', 'Luxembourg'), ('Europe/Madrid', 'Madrid'), ('Europe/Malta', 'Malta'), ('Europe/Mariehamn', 'Mariehamn'), ('Europe/Minsk', 'Minsk'), ('Europe/Monaco', 'Monaco'), ('Europe/Moscow', 'Moscow'), ('Europe/Nicosia', 'Nicosia'), ('Europe/Oslo', 'Oslo'), ('Europe/Paris', 'Paris'), ('Europe/Podgorica', 'Podgorica'), ('Europe/Prague', 'Prague'), ('Europe/Riga', 'Riga'), ('Europe/Rome', 'Rome'), ('Europe/Samara', 'Samara'), ('Europe/San_Marino', 'San_Marino'), ('Europe/Sarajevo', 'Sarajevo'), ('Europe/Saratov', 'Saratov'), ('Europe/Simferopol', 'Simferopol'), ('Europe/Skopje', 'Skopje'), ('Europe/Sofia', 'Sofia'), ('Europe/Stockholm', 'Stockholm'), ('Europe/Tallinn', 'Tallinn'), ('Europe/Tirane', 'Tirane'), ('Europe/Tiraspol', 'Tiraspol'), ('Europe/Ulyanovsk', 'Ulyanovsk'), ('Europe/Uzhgorod', 'Uzhgorod'), ('Europe/Vaduz', 'Vaduz'), ('Europe/Vatican', 'Vatican'), ('Europe/Vienna', 'Vienna'), ('Europe/Vilnius', 'Vilnius'), ('Europe/Volgograd', 'Volgograd'), ('Europe/Warsaw', 'Warsaw'), ('Europe/Zagreb', 'Zagreb'), ('Europe/Zaporozhye', 'Zaporozhye'), ('Europe/Zurich', 'Zurich')]), ('Indian', [('Indian/Antananarivo', 'Antananarivo'), ('Indian/Chagos', 'Chagos'), ('Indian/Christmas', 'Christmas'), ('Indian/Cocos', 'Cocos'), ('Indian/Comoro', 'Comoro'), ('Indian/Kerguelen', 'Kerguelen'), ('Indian/Mahe', 'Mahe'), ('Indian/Maldives', 'Maldives'), ('Indian/Mauritius', 'Mauritius'), ('Indian/Mayotte', 'Mayotte'), ('Indian/Reunion', 'Reunion')]), ('Mexico', [('Mexico/BajaNorte', 'BajaNorte'), ('Mexico/BajaSur', 'BajaSur'), ('Mexico/General', 'General')]), ('Other', [('CET', 'CET'), ('CST6CDT', 'CST6CDT'), ('Cuba', 'Cuba'), ('EET', 'EET'), ('EST', 'EST'), ('EST5EDT', 'EST5EDT'), ('Egypt', 'Egypt'), ('Eire', 'Eire'), ('GB', 'GB'), ('GB-Eire', 'GB-Eire'), ('Greenwich', 'Greenwich'), ('HST', 'HST'), ('Hongkong', 'Hongkong'), ('Iceland', 'Iceland'), ('Iran', 'Iran'), ('Israel', 'Israel'), ('Jamaica', 'Jamaica'), ('Japan', 'Japan'), ('Kwajalein', 'Kwajalein'), ('Libya', 'Libya'), ('MET', 'MET'), ('MST', 'MST'), ('MST7MDT', 'MST7MDT'), ('NZ', 'NZ'), ('NZ-CHAT', 'NZ-CHAT'), ('Navajo', 'Navajo'), ('PRC', 'PRC'), ('PST8PDT', 'PST8PDT'), ('Poland', 'Poland'), ('Portugal', 'Portugal'), ('ROC', 'ROC'), ('ROK', 'ROK'), ('Singapore', 'Singapore'), ('Turkey', 'Turkey'), ('UCT', 'UCT'), ('UTC', 'UTC'), ('Universal', 'Universal'), ('W-SU', 'W-SU'), ('WET', 'WET'), ('Zulu', 'Zulu')]), ('Pacific', [('Pacific/Apia', 'Apia'), ('Pacific/Auckland', 'Auckland'), ('Pacific/Bougainville', 'Bougainville'), ('Pacific/Chatham', 'Chatham'), ('Pacific/Chuuk', 'Chuuk'), ('Pacific/Easter', 'Easter'), ('Pacific/Efate', 'Efate'), ('Pacific/Enderbury', 'Enderbury'), ('Pacific/Fakaofo', 'Fakaofo'), ('Pacific/Fiji', 'Fiji'), ('Pacific/Funafuti', 'Funafuti'), ('Pacific/Galapagos', 'Galapagos'), ('Pacific/Gambier', 'Gambier'), ('Pacific/Guadalcanal', 'Guadalcanal'), ('Pacific/Guam', 'Guam'), ('Pacific/Honolulu', 'Honolulu'), ('Pacific/Johnston', 'Johnston'), ('Pacific/Kiritimati', 'Kiritimati'), ('Pacific/Kosrae', 'Kosrae'), ('Pacific/Kwajalein', 'Kwajalein'), ('Pacific/Majuro', 'Majuro'), ('Pacific/Marquesas', 'Marquesas'), ('Pacific/Midway', 'Midway'), ('Pacific/Nauru', 'Nauru'), ('Pacific/Niue', 'Niue'), ('Pacific/Norfolk', 'Norfolk'), ('Pacific/Noumea', 'Noumea'), ('Pacific/Pago_Pago', 'Pago_Pago'), ('Pacific/Palau', 'Palau'), ('Pacific/Pitcairn', 'Pitcairn'), ('Pacific/Pohnpei', 'Pohnpei'), ('Pacific/Ponape', 'Ponape'), ('Pacific/Port_Moresby', 'Port_Moresby'), ('Pacific/Rarotonga', 'Rarotonga'), ('Pacific/Saipan', 'Saipan'), ('Pacific/Samoa', 'Samoa'), ('Pacific/Tahiti', 'Tahiti'), ('Pacific/Tarawa', 'Tarawa'), ('Pacific/Tongatapu', 'Tongatapu'), ('Pacific/Truk', 'Truk'), ('Pacific/Wake', 'Wake'), ('Pacific/Wallis', 'Wallis'), ('Pacific/Yap', 'Yap')]), ('US', [('US/Alaska', 'Alaska'), ('US/Aleutian', 'Aleutian'), ('US/Arizona', 'Arizona'), ('US/Central', 'Central'), ('US/East-Indiana', 'East-Indiana'), ('US/Eastern', 'Eastern'), ('US/Hawaii', 'Hawaii'), ('US/Indiana-Starke', 'Indiana-Starke'), ('US/Michigan', 'Michigan'), ('US/Mountain', 'Mountain'), ('US/Pacific', 'Pacific'), ('US/Samoa', 'Samoa')])], default='America/Toronto', max_length=50, verbose_name='location')),
- ('points', models.FloatField(db_index=True, default=0)),
- ('performance_points', models.FloatField(db_index=True, default=0)),
- ('problem_count', models.IntegerField(db_index=True, default=0)),
- ('ace_theme', models.CharField(choices=[('ambiance', 'Ambiance'), ('chaos', 'Chaos'), ('chrome', 'Chrome'), ('clouds', 'Clouds'), ('clouds_midnight', 'Clouds Midnight'), ('cobalt', 'Cobalt'), ('crimson_editor', 'Crimson Editor'), ('dawn', 'Dawn'), ('dreamweaver', 'Dreamweaver'), ('eclipse', 'Eclipse'), ('github', 'Github'), ('idle_fingers', 'Idle Fingers'), ('katzenmilch', 'Katzenmilch'), ('kr_theme', 'KR Theme'), ('kuroir', 'Kuroir'), ('merbivore', 'Merbivore'), ('merbivore_soft', 'Merbivore Soft'), ('mono_industrial', 'Mono Industrial'), ('monokai', 'Monokai'), ('pastel_on_dark', 'Pastel on Dark'), ('solarized_dark', 'Solarized Dark'), ('solarized_light', 'Solarized Light'), ('terminal', 'Terminal'), ('textmate', 'Textmate'), ('tomorrow', 'Tomorrow'), ('tomorrow_night', 'Tomorrow Night'), ('tomorrow_night_blue', 'Tomorrow Night Blue'), ('tomorrow_night_bright', 'Tomorrow Night Bright'), ('tomorrow_night_eighties', 'Tomorrow Night Eighties'), ('twilight', 'Twilight'), ('vibrant_ink', 'Vibrant Ink'), ('xcode', 'XCode')], default='github', max_length=30)),
- ('last_access', models.DateTimeField(default=django.utils.timezone.now, verbose_name='last access time')),
- ('ip', models.GenericIPAddressField(blank=True, null=True, verbose_name='last IP')),
- ('display_rank', models.CharField(choices=[('user', 'Normal User'), ('setter', 'Problem Setter'), ('admin', 'Admin')], default='user', max_length=10, verbose_name='display rank')),
- ('mute', models.BooleanField(default=False, help_text='Some users are at their best when silent.', verbose_name='comment mute')),
- ('is_unlisted', models.BooleanField(default=False, help_text='User will not be ranked.', verbose_name='unlisted user')),
- ('rating', models.IntegerField(default=None, null=True)),
- ('user_script', models.TextField(blank=True, default='', help_text='User-defined JavaScript for site customization.', max_length=65536, verbose_name='user script')),
- ('math_engine', models.CharField(choices=[('tex', 'Leave as LaTeX'), ('svg', 'SVG with PNG fallback'), ('mml', 'MathML only'), ('jax', 'MathJax with SVG/PNG fallback'), ('auto', 'Detect best quality')], default='auto', help_text='the rendering engine used to render math', max_length=4, verbose_name='math engine')),
- ('is_totp_enabled', models.BooleanField(default=False, help_text='check to enable TOTP-based two factor authentication', verbose_name='2FA enabled')),
- ('totp_key', judge.models.profile.EncryptedNullCharField(blank=True, help_text='32 character base32-encoded key for TOTP', max_length=32, null=True, validators=[django.core.validators.RegexValidator('^$|^[A-Z2-7]{32}$', 'TOTP key must be empty or base32')], verbose_name='TOTP key')),
- ('notes', models.TextField(blank=True, help_text='Notes for administrators regarding this user.', null=True, verbose_name='internal notes')),
- ('current_contest', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='judge.ContestParticipation', verbose_name='current contest')),
- ('language', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Language', verbose_name='preferred language')),
- ('organizations', sortedm2m.fields.SortedManyToManyField(blank=True, help_text=None, related_name='members', related_query_name='member', to='judge.Organization', verbose_name='organization')),
- ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user associated')),
+ name="Profile",
+ fields=[
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "about",
+ models.TextField(
+ blank=True, null=True, verbose_name="self-description"
+ ),
+ ),
+ (
+ "timezone",
+ models.CharField(
+ choices=[
+ (
+ "Africa",
+ [
+ ("Africa/Abidjan", "Abidjan"),
+ ("Africa/Accra", "Accra"),
+ ("Africa/Addis_Ababa", "Addis_Ababa"),
+ ("Africa/Algiers", "Algiers"),
+ ("Africa/Asmara", "Asmara"),
+ ("Africa/Asmera", "Asmera"),
+ ("Africa/Bamako", "Bamako"),
+ ("Africa/Bangui", "Bangui"),
+ ("Africa/Banjul", "Banjul"),
+ ("Africa/Bissau", "Bissau"),
+ ("Africa/Blantyre", "Blantyre"),
+ ("Africa/Brazzaville", "Brazzaville"),
+ ("Africa/Bujumbura", "Bujumbura"),
+ ("Africa/Cairo", "Cairo"),
+ ("Africa/Casablanca", "Casablanca"),
+ ("Africa/Ceuta", "Ceuta"),
+ ("Africa/Conakry", "Conakry"),
+ ("Africa/Dakar", "Dakar"),
+ ("Africa/Dar_es_Salaam", "Dar_es_Salaam"),
+ ("Africa/Djibouti", "Djibouti"),
+ ("Africa/Douala", "Douala"),
+ ("Africa/El_Aaiun", "El_Aaiun"),
+ ("Africa/Freetown", "Freetown"),
+ ("Africa/Gaborone", "Gaborone"),
+ ("Africa/Harare", "Harare"),
+ ("Africa/Johannesburg", "Johannesburg"),
+ ("Africa/Juba", "Juba"),
+ ("Africa/Kampala", "Kampala"),
+ ("Africa/Khartoum", "Khartoum"),
+ ("Africa/Kigali", "Kigali"),
+ ("Africa/Kinshasa", "Kinshasa"),
+ ("Africa/Lagos", "Lagos"),
+ ("Africa/Libreville", "Libreville"),
+ ("Africa/Lome", "Lome"),
+ ("Africa/Luanda", "Luanda"),
+ ("Africa/Lubumbashi", "Lubumbashi"),
+ ("Africa/Lusaka", "Lusaka"),
+ ("Africa/Malabo", "Malabo"),
+ ("Africa/Maputo", "Maputo"),
+ ("Africa/Maseru", "Maseru"),
+ ("Africa/Mbabane", "Mbabane"),
+ ("Africa/Mogadishu", "Mogadishu"),
+ ("Africa/Monrovia", "Monrovia"),
+ ("Africa/Nairobi", "Nairobi"),
+ ("Africa/Ndjamena", "Ndjamena"),
+ ("Africa/Niamey", "Niamey"),
+ ("Africa/Nouakchott", "Nouakchott"),
+ ("Africa/Ouagadougou", "Ouagadougou"),
+ ("Africa/Porto-Novo", "Porto-Novo"),
+ ("Africa/Sao_Tome", "Sao_Tome"),
+ ("Africa/Timbuktu", "Timbuktu"),
+ ("Africa/Tripoli", "Tripoli"),
+ ("Africa/Tunis", "Tunis"),
+ ("Africa/Windhoek", "Windhoek"),
+ ],
+ ),
+ (
+ "America",
+ [
+ ("America/Adak", "Adak"),
+ ("America/Anchorage", "Anchorage"),
+ ("America/Anguilla", "Anguilla"),
+ ("America/Antigua", "Antigua"),
+ ("America/Araguaina", "Araguaina"),
+ (
+ "America/Argentina/Buenos_Aires",
+ "Argentina/Buenos_Aires",
+ ),
+ (
+ "America/Argentina/Catamarca",
+ "Argentina/Catamarca",
+ ),
+ (
+ "America/Argentina/ComodRivadavia",
+ "Argentina/ComodRivadavia",
+ ),
+ ("America/Argentina/Cordoba", "Argentina/Cordoba"),
+ ("America/Argentina/Jujuy", "Argentina/Jujuy"),
+ (
+ "America/Argentina/La_Rioja",
+ "Argentina/La_Rioja",
+ ),
+ ("America/Argentina/Mendoza", "Argentina/Mendoza"),
+ (
+ "America/Argentina/Rio_Gallegos",
+ "Argentina/Rio_Gallegos",
+ ),
+ ("America/Argentina/Salta", "Argentina/Salta"),
+ (
+ "America/Argentina/San_Juan",
+ "Argentina/San_Juan",
+ ),
+ (
+ "America/Argentina/San_Luis",
+ "Argentina/San_Luis",
+ ),
+ ("America/Argentina/Tucuman", "Argentina/Tucuman"),
+ ("America/Argentina/Ushuaia", "Argentina/Ushuaia"),
+ ("America/Aruba", "Aruba"),
+ ("America/Asuncion", "Asuncion"),
+ ("America/Atikokan", "Atikokan"),
+ ("America/Atka", "Atka"),
+ ("America/Bahia", "Bahia"),
+ ("America/Bahia_Banderas", "Bahia_Banderas"),
+ ("America/Barbados", "Barbados"),
+ ("America/Belem", "Belem"),
+ ("America/Belize", "Belize"),
+ ("America/Blanc-Sablon", "Blanc-Sablon"),
+ ("America/Boa_Vista", "Boa_Vista"),
+ ("America/Bogota", "Bogota"),
+ ("America/Boise", "Boise"),
+ ("America/Buenos_Aires", "Buenos_Aires"),
+ ("America/Cambridge_Bay", "Cambridge_Bay"),
+ ("America/Campo_Grande", "Campo_Grande"),
+ ("America/Cancun", "Cancun"),
+ ("America/Caracas", "Caracas"),
+ ("America/Catamarca", "Catamarca"),
+ ("America/Cayenne", "Cayenne"),
+ ("America/Cayman", "Cayman"),
+ ("America/Chicago", "Chicago"),
+ ("America/Chihuahua", "Chihuahua"),
+ ("America/Coral_Harbour", "Coral_Harbour"),
+ ("America/Cordoba", "Cordoba"),
+ ("America/Costa_Rica", "Costa_Rica"),
+ ("America/Creston", "Creston"),
+ ("America/Cuiaba", "Cuiaba"),
+ ("America/Curacao", "Curacao"),
+ ("America/Danmarkshavn", "Danmarkshavn"),
+ ("America/Dawson", "Dawson"),
+ ("America/Dawson_Creek", "Dawson_Creek"),
+ ("America/Denver", "Denver"),
+ ("America/Detroit", "Detroit"),
+ ("America/Dominica", "Dominica"),
+ ("America/Edmonton", "Edmonton"),
+ ("America/Eirunepe", "Eirunepe"),
+ ("America/El_Salvador", "El_Salvador"),
+ ("America/Ensenada", "Ensenada"),
+ ("America/Fort_Nelson", "Fort_Nelson"),
+ ("America/Fort_Wayne", "Fort_Wayne"),
+ ("America/Fortaleza", "Fortaleza"),
+ ("America/Glace_Bay", "Glace_Bay"),
+ ("America/Godthab", "Godthab"),
+ ("America/Goose_Bay", "Goose_Bay"),
+ ("America/Grand_Turk", "Grand_Turk"),
+ ("America/Grenada", "Grenada"),
+ ("America/Guadeloupe", "Guadeloupe"),
+ ("America/Guatemala", "Guatemala"),
+ ("America/Guayaquil", "Guayaquil"),
+ ("America/Guyana", "Guyana"),
+ ("America/Halifax", "Halifax"),
+ ("America/Havana", "Havana"),
+ ("America/Hermosillo", "Hermosillo"),
+ (
+ "America/Indiana/Indianapolis",
+ "Indiana/Indianapolis",
+ ),
+ ("America/Indiana/Knox", "Indiana/Knox"),
+ ("America/Indiana/Marengo", "Indiana/Marengo"),
+ (
+ "America/Indiana/Petersburg",
+ "Indiana/Petersburg",
+ ),
+ ("America/Indiana/Tell_City", "Indiana/Tell_City"),
+ ("America/Indiana/Vevay", "Indiana/Vevay"),
+ ("America/Indiana/Vincennes", "Indiana/Vincennes"),
+ ("America/Indiana/Winamac", "Indiana/Winamac"),
+ ("America/Indianapolis", "Indianapolis"),
+ ("America/Inuvik", "Inuvik"),
+ ("America/Iqaluit", "Iqaluit"),
+ ("America/Jamaica", "Jamaica"),
+ ("America/Jujuy", "Jujuy"),
+ ("America/Juneau", "Juneau"),
+ (
+ "America/Kentucky/Louisville",
+ "Kentucky/Louisville",
+ ),
+ (
+ "America/Kentucky/Monticello",
+ "Kentucky/Monticello",
+ ),
+ ("America/Knox_IN", "Knox_IN"),
+ ("America/Kralendijk", "Kralendijk"),
+ ("America/La_Paz", "La_Paz"),
+ ("America/Lima", "Lima"),
+ ("America/Los_Angeles", "Los_Angeles"),
+ ("America/Louisville", "Louisville"),
+ ("America/Lower_Princes", "Lower_Princes"),
+ ("America/Maceio", "Maceio"),
+ ("America/Managua", "Managua"),
+ ("America/Manaus", "Manaus"),
+ ("America/Marigot", "Marigot"),
+ ("America/Martinique", "Martinique"),
+ ("America/Matamoros", "Matamoros"),
+ ("America/Mazatlan", "Mazatlan"),
+ ("America/Mendoza", "Mendoza"),
+ ("America/Menominee", "Menominee"),
+ ("America/Merida", "Merida"),
+ ("America/Metlakatla", "Metlakatla"),
+ ("America/Mexico_City", "Mexico_City"),
+ ("America/Miquelon", "Miquelon"),
+ ("America/Moncton", "Moncton"),
+ ("America/Monterrey", "Monterrey"),
+ ("America/Montevideo", "Montevideo"),
+ ("America/Montreal", "Montreal"),
+ ("America/Montserrat", "Montserrat"),
+ ("America/Nassau", "Nassau"),
+ ("America/New_York", "New_York"),
+ ("America/Nipigon", "Nipigon"),
+ ("America/Nome", "Nome"),
+ ("America/Noronha", "Noronha"),
+ (
+ "America/North_Dakota/Beulah",
+ "North_Dakota/Beulah",
+ ),
+ (
+ "America/North_Dakota/Center",
+ "North_Dakota/Center",
+ ),
+ (
+ "America/North_Dakota/New_Salem",
+ "North_Dakota/New_Salem",
+ ),
+ ("America/Ojinaga", "Ojinaga"),
+ ("America/Panama", "Panama"),
+ ("America/Pangnirtung", "Pangnirtung"),
+ ("America/Paramaribo", "Paramaribo"),
+ ("America/Phoenix", "Phoenix"),
+ ("America/Port-au-Prince", "Port-au-Prince"),
+ ("America/Port_of_Spain", "Port_of_Spain"),
+ ("America/Porto_Acre", "Porto_Acre"),
+ ("America/Porto_Velho", "Porto_Velho"),
+ ("America/Puerto_Rico", "Puerto_Rico"),
+ ("America/Punta_Arenas", "Punta_Arenas"),
+ ("America/Rainy_River", "Rainy_River"),
+ ("America/Rankin_Inlet", "Rankin_Inlet"),
+ ("America/Recife", "Recife"),
+ ("America/Regina", "Regina"),
+ ("America/Resolute", "Resolute"),
+ ("America/Rio_Branco", "Rio_Branco"),
+ ("America/Rosario", "Rosario"),
+ ("America/Santa_Isabel", "Santa_Isabel"),
+ ("America/Santarem", "Santarem"),
+ ("America/Santiago", "Santiago"),
+ ("America/Santo_Domingo", "Santo_Domingo"),
+ ("America/Sao_Paulo", "Sao_Paulo"),
+ ("America/Scoresbysund", "Scoresbysund"),
+ ("America/Shiprock", "Shiprock"),
+ ("America/Sitka", "Sitka"),
+ ("America/St_Barthelemy", "St_Barthelemy"),
+ ("America/St_Johns", "St_Johns"),
+ ("America/St_Kitts", "St_Kitts"),
+ ("America/St_Lucia", "St_Lucia"),
+ ("America/St_Thomas", "St_Thomas"),
+ ("America/St_Vincent", "St_Vincent"),
+ ("America/Swift_Current", "Swift_Current"),
+ ("America/Tegucigalpa", "Tegucigalpa"),
+ ("America/Thule", "Thule"),
+ ("America/Thunder_Bay", "Thunder_Bay"),
+ ("America/Tijuana", "Tijuana"),
+ ("America/Toronto", "Toronto"),
+ ("America/Tortola", "Tortola"),
+ ("America/Vancouver", "Vancouver"),
+ ("America/Virgin", "Virgin"),
+ ("America/Whitehorse", "Whitehorse"),
+ ("America/Winnipeg", "Winnipeg"),
+ ("America/Yakutat", "Yakutat"),
+ ("America/Yellowknife", "Yellowknife"),
+ ],
+ ),
+ (
+ "Antarctica",
+ [
+ ("Antarctica/Casey", "Casey"),
+ ("Antarctica/Davis", "Davis"),
+ ("Antarctica/DumontDUrville", "DumontDUrville"),
+ ("Antarctica/Macquarie", "Macquarie"),
+ ("Antarctica/Mawson", "Mawson"),
+ ("Antarctica/McMurdo", "McMurdo"),
+ ("Antarctica/Palmer", "Palmer"),
+ ("Antarctica/Rothera", "Rothera"),
+ ("Antarctica/South_Pole", "South_Pole"),
+ ("Antarctica/Syowa", "Syowa"),
+ ("Antarctica/Troll", "Troll"),
+ ("Antarctica/Vostok", "Vostok"),
+ ],
+ ),
+ ("Arctic", [("Arctic/Longyearbyen", "Longyearbyen")]),
+ (
+ "Asia",
+ [
+ ("Asia/Aden", "Aden"),
+ ("Asia/Almaty", "Almaty"),
+ ("Asia/Amman", "Amman"),
+ ("Asia/Anadyr", "Anadyr"),
+ ("Asia/Aqtau", "Aqtau"),
+ ("Asia/Aqtobe", "Aqtobe"),
+ ("Asia/Ashgabat", "Ashgabat"),
+ ("Asia/Ashkhabad", "Ashkhabad"),
+ ("Asia/Atyrau", "Atyrau"),
+ ("Asia/Baghdad", "Baghdad"),
+ ("Asia/Bahrain", "Bahrain"),
+ ("Asia/Baku", "Baku"),
+ ("Asia/Bangkok", "Bangkok"),
+ ("Asia/Barnaul", "Barnaul"),
+ ("Asia/Beirut", "Beirut"),
+ ("Asia/Bishkek", "Bishkek"),
+ ("Asia/Brunei", "Brunei"),
+ ("Asia/Calcutta", "Calcutta"),
+ ("Asia/Chita", "Chita"),
+ ("Asia/Choibalsan", "Choibalsan"),
+ ("Asia/Chongqing", "Chongqing"),
+ ("Asia/Chungking", "Chungking"),
+ ("Asia/Colombo", "Colombo"),
+ ("Asia/Dacca", "Dacca"),
+ ("Asia/Damascus", "Damascus"),
+ ("Asia/Dhaka", "Dhaka"),
+ ("Asia/Dili", "Dili"),
+ ("Asia/Dubai", "Dubai"),
+ ("Asia/Dushanbe", "Dushanbe"),
+ ("Asia/Famagusta", "Famagusta"),
+ ("Asia/Gaza", "Gaza"),
+ ("Asia/Harbin", "Harbin"),
+ ("Asia/Hebron", "Hebron"),
+ ("Asia/Ho_Chi_Minh", "Ho_Chi_Minh"),
+ ("Asia/Hong_Kong", "Hong_Kong"),
+ ("Asia/Hovd", "Hovd"),
+ ("Asia/Irkutsk", "Irkutsk"),
+ ("Asia/Istanbul", "Istanbul"),
+ ("Asia/Jakarta", "Jakarta"),
+ ("Asia/Jayapura", "Jayapura"),
+ ("Asia/Jerusalem", "Jerusalem"),
+ ("Asia/Kabul", "Kabul"),
+ ("Asia/Kamchatka", "Kamchatka"),
+ ("Asia/Karachi", "Karachi"),
+ ("Asia/Kashgar", "Kashgar"),
+ ("Asia/Kathmandu", "Kathmandu"),
+ ("Asia/Katmandu", "Katmandu"),
+ ("Asia/Khandyga", "Khandyga"),
+ ("Asia/Kolkata", "Kolkata"),
+ ("Asia/Krasnoyarsk", "Krasnoyarsk"),
+ ("Asia/Kuala_Lumpur", "Kuala_Lumpur"),
+ ("Asia/Kuching", "Kuching"),
+ ("Asia/Kuwait", "Kuwait"),
+ ("Asia/Macao", "Macao"),
+ ("Asia/Macau", "Macau"),
+ ("Asia/Magadan", "Magadan"),
+ ("Asia/Makassar", "Makassar"),
+ ("Asia/Manila", "Manila"),
+ ("Asia/Muscat", "Muscat"),
+ ("Asia/Nicosia", "Nicosia"),
+ ("Asia/Novokuznetsk", "Novokuznetsk"),
+ ("Asia/Novosibirsk", "Novosibirsk"),
+ ("Asia/Omsk", "Omsk"),
+ ("Asia/Oral", "Oral"),
+ ("Asia/Phnom_Penh", "Phnom_Penh"),
+ ("Asia/Pontianak", "Pontianak"),
+ ("Asia/Pyongyang", "Pyongyang"),
+ ("Asia/Qatar", "Qatar"),
+ ("Asia/Qostanay", "Qostanay"),
+ ("Asia/Qyzylorda", "Qyzylorda"),
+ ("Asia/Rangoon", "Rangoon"),
+ ("Asia/Riyadh", "Riyadh"),
+ ("Asia/Saigon", "Saigon"),
+ ("Asia/Sakhalin", "Sakhalin"),
+ ("Asia/Samarkand", "Samarkand"),
+ ("Asia/Seoul", "Seoul"),
+ ("Asia/Shanghai", "Shanghai"),
+ ("Asia/Singapore", "Singapore"),
+ ("Asia/Srednekolymsk", "Srednekolymsk"),
+ ("Asia/Taipei", "Taipei"),
+ ("Asia/Tashkent", "Tashkent"),
+ ("Asia/Tbilisi", "Tbilisi"),
+ ("Asia/Tehran", "Tehran"),
+ ("Asia/Tel_Aviv", "Tel_Aviv"),
+ ("Asia/Thimbu", "Thimbu"),
+ ("Asia/Thimphu", "Thimphu"),
+ ("Asia/Tokyo", "Tokyo"),
+ ("Asia/Tomsk", "Tomsk"),
+ ("Asia/Ujung_Pandang", "Ujung_Pandang"),
+ ("Asia/Ulaanbaatar", "Ulaanbaatar"),
+ ("Asia/Ulan_Bator", "Ulan_Bator"),
+ ("Asia/Urumqi", "Urumqi"),
+ ("Asia/Ust-Nera", "Ust-Nera"),
+ ("Asia/Vientiane", "Vientiane"),
+ ("Asia/Vladivostok", "Vladivostok"),
+ ("Asia/Yakutsk", "Yakutsk"),
+ ("Asia/Yangon", "Yangon"),
+ ("Asia/Yekaterinburg", "Yekaterinburg"),
+ ("Asia/Yerevan", "Yerevan"),
+ ],
+ ),
+ (
+ "Atlantic",
+ [
+ ("Atlantic/Azores", "Azores"),
+ ("Atlantic/Bermuda", "Bermuda"),
+ ("Atlantic/Canary", "Canary"),
+ ("Atlantic/Cape_Verde", "Cape_Verde"),
+ ("Atlantic/Faeroe", "Faeroe"),
+ ("Atlantic/Faroe", "Faroe"),
+ ("Atlantic/Jan_Mayen", "Jan_Mayen"),
+ ("Atlantic/Madeira", "Madeira"),
+ ("Atlantic/Reykjavik", "Reykjavik"),
+ ("Atlantic/South_Georgia", "South_Georgia"),
+ ("Atlantic/St_Helena", "St_Helena"),
+ ("Atlantic/Stanley", "Stanley"),
+ ],
+ ),
+ (
+ "Australia",
+ [
+ ("Australia/ACT", "ACT"),
+ ("Australia/Adelaide", "Adelaide"),
+ ("Australia/Brisbane", "Brisbane"),
+ ("Australia/Broken_Hill", "Broken_Hill"),
+ ("Australia/Canberra", "Canberra"),
+ ("Australia/Currie", "Currie"),
+ ("Australia/Darwin", "Darwin"),
+ ("Australia/Eucla", "Eucla"),
+ ("Australia/Hobart", "Hobart"),
+ ("Australia/LHI", "LHI"),
+ ("Australia/Lindeman", "Lindeman"),
+ ("Australia/Lord_Howe", "Lord_Howe"),
+ ("Australia/Melbourne", "Melbourne"),
+ ("Australia/NSW", "NSW"),
+ ("Australia/North", "North"),
+ ("Australia/Perth", "Perth"),
+ ("Australia/Queensland", "Queensland"),
+ ("Australia/South", "South"),
+ ("Australia/Sydney", "Sydney"),
+ ("Australia/Tasmania", "Tasmania"),
+ ("Australia/Victoria", "Victoria"),
+ ("Australia/West", "West"),
+ ("Australia/Yancowinna", "Yancowinna"),
+ ],
+ ),
+ (
+ "Brazil",
+ [
+ ("Brazil/Acre", "Acre"),
+ ("Brazil/DeNoronha", "DeNoronha"),
+ ("Brazil/East", "East"),
+ ("Brazil/West", "West"),
+ ],
+ ),
+ (
+ "Canada",
+ [
+ ("Canada/Atlantic", "Atlantic"),
+ ("Canada/Central", "Central"),
+ ("Canada/Eastern", "Eastern"),
+ ("Canada/Mountain", "Mountain"),
+ ("Canada/Newfoundland", "Newfoundland"),
+ ("Canada/Pacific", "Pacific"),
+ ("Canada/Saskatchewan", "Saskatchewan"),
+ ("Canada/Yukon", "Yukon"),
+ ],
+ ),
+ (
+ "Chile",
+ [
+ ("Chile/Continental", "Continental"),
+ ("Chile/EasterIsland", "EasterIsland"),
+ ],
+ ),
+ (
+ "Etc",
+ [
+ ("Etc/Greenwich", "Greenwich"),
+ ("Etc/UCT", "UCT"),
+ ("Etc/UTC", "UTC"),
+ ("Etc/Universal", "Universal"),
+ ("Etc/Zulu", "Zulu"),
+ ],
+ ),
+ (
+ "Europe",
+ [
+ ("Europe/Amsterdam", "Amsterdam"),
+ ("Europe/Andorra", "Andorra"),
+ ("Europe/Astrakhan", "Astrakhan"),
+ ("Europe/Athens", "Athens"),
+ ("Europe/Belfast", "Belfast"),
+ ("Europe/Belgrade", "Belgrade"),
+ ("Europe/Berlin", "Berlin"),
+ ("Europe/Bratislava", "Bratislava"),
+ ("Europe/Brussels", "Brussels"),
+ ("Europe/Bucharest", "Bucharest"),
+ ("Europe/Budapest", "Budapest"),
+ ("Europe/Busingen", "Busingen"),
+ ("Europe/Chisinau", "Chisinau"),
+ ("Europe/Copenhagen", "Copenhagen"),
+ ("Europe/Dublin", "Dublin"),
+ ("Europe/Gibraltar", "Gibraltar"),
+ ("Europe/Guernsey", "Guernsey"),
+ ("Europe/Helsinki", "Helsinki"),
+ ("Europe/Isle_of_Man", "Isle_of_Man"),
+ ("Europe/Istanbul", "Istanbul"),
+ ("Europe/Jersey", "Jersey"),
+ ("Europe/Kaliningrad", "Kaliningrad"),
+ ("Europe/Kiev", "Kiev"),
+ ("Europe/Kirov", "Kirov"),
+ ("Europe/Lisbon", "Lisbon"),
+ ("Europe/Ljubljana", "Ljubljana"),
+ ("Europe/London", "London"),
+ ("Europe/Luxembourg", "Luxembourg"),
+ ("Europe/Madrid", "Madrid"),
+ ("Europe/Malta", "Malta"),
+ ("Europe/Mariehamn", "Mariehamn"),
+ ("Europe/Minsk", "Minsk"),
+ ("Europe/Monaco", "Monaco"),
+ ("Europe/Moscow", "Moscow"),
+ ("Europe/Nicosia", "Nicosia"),
+ ("Europe/Oslo", "Oslo"),
+ ("Europe/Paris", "Paris"),
+ ("Europe/Podgorica", "Podgorica"),
+ ("Europe/Prague", "Prague"),
+ ("Europe/Riga", "Riga"),
+ ("Europe/Rome", "Rome"),
+ ("Europe/Samara", "Samara"),
+ ("Europe/San_Marino", "San_Marino"),
+ ("Europe/Sarajevo", "Sarajevo"),
+ ("Europe/Saratov", "Saratov"),
+ ("Europe/Simferopol", "Simferopol"),
+ ("Europe/Skopje", "Skopje"),
+ ("Europe/Sofia", "Sofia"),
+ ("Europe/Stockholm", "Stockholm"),
+ ("Europe/Tallinn", "Tallinn"),
+ ("Europe/Tirane", "Tirane"),
+ ("Europe/Tiraspol", "Tiraspol"),
+ ("Europe/Ulyanovsk", "Ulyanovsk"),
+ ("Europe/Uzhgorod", "Uzhgorod"),
+ ("Europe/Vaduz", "Vaduz"),
+ ("Europe/Vatican", "Vatican"),
+ ("Europe/Vienna", "Vienna"),
+ ("Europe/Vilnius", "Vilnius"),
+ ("Europe/Volgograd", "Volgograd"),
+ ("Europe/Warsaw", "Warsaw"),
+ ("Europe/Zagreb", "Zagreb"),
+ ("Europe/Zaporozhye", "Zaporozhye"),
+ ("Europe/Zurich", "Zurich"),
+ ],
+ ),
+ (
+ "Indian",
+ [
+ ("Indian/Antananarivo", "Antananarivo"),
+ ("Indian/Chagos", "Chagos"),
+ ("Indian/Christmas", "Christmas"),
+ ("Indian/Cocos", "Cocos"),
+ ("Indian/Comoro", "Comoro"),
+ ("Indian/Kerguelen", "Kerguelen"),
+ ("Indian/Mahe", "Mahe"),
+ ("Indian/Maldives", "Maldives"),
+ ("Indian/Mauritius", "Mauritius"),
+ ("Indian/Mayotte", "Mayotte"),
+ ("Indian/Reunion", "Reunion"),
+ ],
+ ),
+ (
+ "Mexico",
+ [
+ ("Mexico/BajaNorte", "BajaNorte"),
+ ("Mexico/BajaSur", "BajaSur"),
+ ("Mexico/General", "General"),
+ ],
+ ),
+ (
+ "Other",
+ [
+ ("CET", "CET"),
+ ("CST6CDT", "CST6CDT"),
+ ("Cuba", "Cuba"),
+ ("EET", "EET"),
+ ("EST", "EST"),
+ ("EST5EDT", "EST5EDT"),
+ ("Egypt", "Egypt"),
+ ("Eire", "Eire"),
+ ("GB", "GB"),
+ ("GB-Eire", "GB-Eire"),
+ ("Greenwich", "Greenwich"),
+ ("HST", "HST"),
+ ("Hongkong", "Hongkong"),
+ ("Iceland", "Iceland"),
+ ("Iran", "Iran"),
+ ("Israel", "Israel"),
+ ("Jamaica", "Jamaica"),
+ ("Japan", "Japan"),
+ ("Kwajalein", "Kwajalein"),
+ ("Libya", "Libya"),
+ ("MET", "MET"),
+ ("MST", "MST"),
+ ("MST7MDT", "MST7MDT"),
+ ("NZ", "NZ"),
+ ("NZ-CHAT", "NZ-CHAT"),
+ ("Navajo", "Navajo"),
+ ("PRC", "PRC"),
+ ("PST8PDT", "PST8PDT"),
+ ("Poland", "Poland"),
+ ("Portugal", "Portugal"),
+ ("ROC", "ROC"),
+ ("ROK", "ROK"),
+ ("Singapore", "Singapore"),
+ ("Turkey", "Turkey"),
+ ("UCT", "UCT"),
+ ("UTC", "UTC"),
+ ("Universal", "Universal"),
+ ("W-SU", "W-SU"),
+ ("WET", "WET"),
+ ("Zulu", "Zulu"),
+ ],
+ ),
+ (
+ "Pacific",
+ [
+ ("Pacific/Apia", "Apia"),
+ ("Pacific/Auckland", "Auckland"),
+ ("Pacific/Bougainville", "Bougainville"),
+ ("Pacific/Chatham", "Chatham"),
+ ("Pacific/Chuuk", "Chuuk"),
+ ("Pacific/Easter", "Easter"),
+ ("Pacific/Efate", "Efate"),
+ ("Pacific/Enderbury", "Enderbury"),
+ ("Pacific/Fakaofo", "Fakaofo"),
+ ("Pacific/Fiji", "Fiji"),
+ ("Pacific/Funafuti", "Funafuti"),
+ ("Pacific/Galapagos", "Galapagos"),
+ ("Pacific/Gambier", "Gambier"),
+ ("Pacific/Guadalcanal", "Guadalcanal"),
+ ("Pacific/Guam", "Guam"),
+ ("Pacific/Honolulu", "Honolulu"),
+ ("Pacific/Johnston", "Johnston"),
+ ("Pacific/Kiritimati", "Kiritimati"),
+ ("Pacific/Kosrae", "Kosrae"),
+ ("Pacific/Kwajalein", "Kwajalein"),
+ ("Pacific/Majuro", "Majuro"),
+ ("Pacific/Marquesas", "Marquesas"),
+ ("Pacific/Midway", "Midway"),
+ ("Pacific/Nauru", "Nauru"),
+ ("Pacific/Niue", "Niue"),
+ ("Pacific/Norfolk", "Norfolk"),
+ ("Pacific/Noumea", "Noumea"),
+ ("Pacific/Pago_Pago", "Pago_Pago"),
+ ("Pacific/Palau", "Palau"),
+ ("Pacific/Pitcairn", "Pitcairn"),
+ ("Pacific/Pohnpei", "Pohnpei"),
+ ("Pacific/Ponape", "Ponape"),
+ ("Pacific/Port_Moresby", "Port_Moresby"),
+ ("Pacific/Rarotonga", "Rarotonga"),
+ ("Pacific/Saipan", "Saipan"),
+ ("Pacific/Samoa", "Samoa"),
+ ("Pacific/Tahiti", "Tahiti"),
+ ("Pacific/Tarawa", "Tarawa"),
+ ("Pacific/Tongatapu", "Tongatapu"),
+ ("Pacific/Truk", "Truk"),
+ ("Pacific/Wake", "Wake"),
+ ("Pacific/Wallis", "Wallis"),
+ ("Pacific/Yap", "Yap"),
+ ],
+ ),
+ (
+ "US",
+ [
+ ("US/Alaska", "Alaska"),
+ ("US/Aleutian", "Aleutian"),
+ ("US/Arizona", "Arizona"),
+ ("US/Central", "Central"),
+ ("US/East-Indiana", "East-Indiana"),
+ ("US/Eastern", "Eastern"),
+ ("US/Hawaii", "Hawaii"),
+ ("US/Indiana-Starke", "Indiana-Starke"),
+ ("US/Michigan", "Michigan"),
+ ("US/Mountain", "Mountain"),
+ ("US/Pacific", "Pacific"),
+ ("US/Samoa", "Samoa"),
+ ],
+ ),
+ ],
+ default="America/Toronto",
+ max_length=50,
+ verbose_name="location",
+ ),
+ ),
+ ("points", models.FloatField(db_index=True, default=0)),
+ ("performance_points", models.FloatField(db_index=True, default=0)),
+ ("problem_count", models.IntegerField(db_index=True, default=0)),
+ (
+ "ace_theme",
+ models.CharField(
+ choices=[
+ ("ambiance", "Ambiance"),
+ ("chaos", "Chaos"),
+ ("chrome", "Chrome"),
+ ("clouds", "Clouds"),
+ ("clouds_midnight", "Clouds Midnight"),
+ ("cobalt", "Cobalt"),
+ ("crimson_editor", "Crimson Editor"),
+ ("dawn", "Dawn"),
+ ("dreamweaver", "Dreamweaver"),
+ ("eclipse", "Eclipse"),
+ ("github", "Github"),
+ ("idle_fingers", "Idle Fingers"),
+ ("katzenmilch", "Katzenmilch"),
+ ("kr_theme", "KR Theme"),
+ ("kuroir", "Kuroir"),
+ ("merbivore", "Merbivore"),
+ ("merbivore_soft", "Merbivore Soft"),
+ ("mono_industrial", "Mono Industrial"),
+ ("monokai", "Monokai"),
+ ("pastel_on_dark", "Pastel on Dark"),
+ ("solarized_dark", "Solarized Dark"),
+ ("solarized_light", "Solarized Light"),
+ ("terminal", "Terminal"),
+ ("textmate", "Textmate"),
+ ("tomorrow", "Tomorrow"),
+ ("tomorrow_night", "Tomorrow Night"),
+ ("tomorrow_night_blue", "Tomorrow Night Blue"),
+ ("tomorrow_night_bright", "Tomorrow Night Bright"),
+ ("tomorrow_night_eighties", "Tomorrow Night Eighties"),
+ ("twilight", "Twilight"),
+ ("vibrant_ink", "Vibrant Ink"),
+ ("xcode", "XCode"),
+ ],
+ default="github",
+ max_length=30,
+ ),
+ ),
+ (
+ "last_access",
+ models.DateTimeField(
+ default=django.utils.timezone.now,
+ verbose_name="last access time",
+ ),
+ ),
+ (
+ "ip",
+ models.GenericIPAddressField(
+ blank=True, null=True, verbose_name="last IP"
+ ),
+ ),
+ (
+ "display_rank",
+ models.CharField(
+ choices=[
+ ("user", "Normal User"),
+ ("setter", "Problem Setter"),
+ ("admin", "Admin"),
+ ],
+ default="user",
+ max_length=10,
+ verbose_name="display rank",
+ ),
+ ),
+ (
+ "mute",
+ models.BooleanField(
+ default=False,
+ help_text="Some users are at their best when silent.",
+ verbose_name="comment mute",
+ ),
+ ),
+ (
+ "is_unlisted",
+ models.BooleanField(
+ default=False,
+ help_text="User will not be ranked.",
+ verbose_name="unlisted user",
+ ),
+ ),
+ ("rating", models.IntegerField(default=None, null=True)),
+ (
+ "user_script",
+ models.TextField(
+ blank=True,
+ default="",
+ help_text="User-defined JavaScript for site customization.",
+ max_length=65536,
+ verbose_name="user script",
+ ),
+ ),
+ (
+ "math_engine",
+ models.CharField(
+ choices=[
+ ("tex", "Leave as LaTeX"),
+ ("svg", "SVG with PNG fallback"),
+ ("mml", "MathML only"),
+ ("jax", "MathJax with SVG/PNG fallback"),
+ ("auto", "Detect best quality"),
+ ],
+ default="auto",
+ help_text="the rendering engine used to render math",
+ max_length=4,
+ verbose_name="math engine",
+ ),
+ ),
+ (
+ "is_totp_enabled",
+ models.BooleanField(
+ default=False,
+ help_text="check to enable TOTP-based two factor authentication",
+ verbose_name="2FA enabled",
+ ),
+ ),
+ (
+ "totp_key",
+ judge.models.profile.EncryptedNullCharField(
+ blank=True,
+ help_text="32 character base32-encoded key for TOTP",
+ max_length=32,
+ null=True,
+ validators=[
+ django.core.validators.RegexValidator(
+ "^$|^[A-Z2-7]{32}$", "TOTP key must be empty or base32"
+ )
+ ],
+ verbose_name="TOTP key",
+ ),
+ ),
+ (
+ "notes",
+ models.TextField(
+ blank=True,
+ help_text="Notes for administrators regarding this user.",
+ null=True,
+ verbose_name="internal notes",
+ ),
+ ),
+ (
+ "current_contest",
+ models.OneToOneField(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="+",
+ to="judge.ContestParticipation",
+ verbose_name="current contest",
+ ),
+ ),
+ (
+ "language",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="judge.Language",
+ verbose_name="preferred language",
+ ),
+ ),
+ (
+ "organizations",
+ sortedm2m.fields.SortedManyToManyField(
+ blank=True,
+ help_text=None,
+ related_name="members",
+ related_query_name="member",
+ to="judge.Organization",
+ verbose_name="organization",
+ ),
+ ),
+ (
+ "user",
+ models.OneToOneField(
+ on_delete=django.db.models.deletion.CASCADE,
+ to=settings.AUTH_USER_MODEL,
+ verbose_name="user associated",
+ ),
+ ),
],
options={
- 'verbose_name_plural': 'user profiles',
- 'permissions': (('test_site', 'Shows in-progress development stuff'), ('totp', 'Edit TOTP settings')),
- 'verbose_name': 'user profile',
+ "verbose_name_plural": "user profiles",
+ "permissions": (
+ ("test_site", "Shows in-progress development stuff"),
+ ("totp", "Edit TOTP settings"),
+ ),
+ "verbose_name": "user profile",
},
),
migrations.CreateModel(
- name='Rating',
+ name="Rating",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('rank', models.IntegerField(verbose_name='rank')),
- ('rating', models.IntegerField(verbose_name='rating')),
- ('volatility', models.IntegerField(verbose_name='volatility')),
- ('last_rated', models.DateTimeField(db_index=True, verbose_name='last rated')),
- ('contest', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ratings', to='judge.Contest', verbose_name='contest')),
- ('participation', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='rating', to='judge.ContestParticipation', verbose_name='participation')),
- ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ratings', to='judge.Profile', verbose_name='user')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("rank", models.IntegerField(verbose_name="rank")),
+ ("rating", models.IntegerField(verbose_name="rating")),
+ ("volatility", models.IntegerField(verbose_name="volatility")),
+ (
+ "last_rated",
+ models.DateTimeField(db_index=True, verbose_name="last rated"),
+ ),
+ (
+ "contest",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="ratings",
+ to="judge.Contest",
+ verbose_name="contest",
+ ),
+ ),
+ (
+ "participation",
+ models.OneToOneField(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="rating",
+ to="judge.ContestParticipation",
+ verbose_name="participation",
+ ),
+ ),
+ (
+ "user",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="ratings",
+ to="judge.Profile",
+ verbose_name="user",
+ ),
+ ),
],
options={
- 'verbose_name_plural': 'contest ratings',
- 'verbose_name': 'contest rating',
+ "verbose_name_plural": "contest ratings",
+ "verbose_name": "contest rating",
},
),
migrations.CreateModel(
- name='RuntimeVersion',
+ name="RuntimeVersion",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('name', models.CharField(max_length=64, verbose_name='runtime name')),
- ('version', models.CharField(blank=True, max_length=64, verbose_name='runtime version')),
- ('priority', models.IntegerField(default=0, verbose_name='order in which to display this runtime')),
- ('judge', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Judge', verbose_name='judge on which this runtime exists')),
- ('language', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Language', verbose_name='language to which this runtime belongs')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("name", models.CharField(max_length=64, verbose_name="runtime name")),
+ (
+ "version",
+ models.CharField(
+ blank=True, max_length=64, verbose_name="runtime version"
+ ),
+ ),
+ (
+ "priority",
+ models.IntegerField(
+ default=0, verbose_name="order in which to display this runtime"
+ ),
+ ),
+ (
+ "judge",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="judge.Judge",
+ verbose_name="judge on which this runtime exists",
+ ),
+ ),
+ (
+ "language",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="judge.Language",
+ verbose_name="language to which this runtime belongs",
+ ),
+ ),
],
),
migrations.CreateModel(
- name='Solution',
+ name="Solution",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('is_public', models.BooleanField(default=False, verbose_name='public visibility')),
- ('publish_on', models.DateTimeField(verbose_name='publish date')),
- ('content', models.TextField(verbose_name='editorial content')),
- ('authors', models.ManyToManyField(blank=True, to='judge.Profile', verbose_name='authors')),
- ('problem', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='solution', to='judge.Problem', verbose_name='associated problem')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "is_public",
+ models.BooleanField(
+ default=False, verbose_name="public visibility"
+ ),
+ ),
+ ("publish_on", models.DateTimeField(verbose_name="publish date")),
+ ("content", models.TextField(verbose_name="editorial content")),
+ (
+ "authors",
+ models.ManyToManyField(
+ blank=True, to="judge.Profile", verbose_name="authors"
+ ),
+ ),
+ (
+ "problem",
+ models.OneToOneField(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="solution",
+ to="judge.Problem",
+ verbose_name="associated problem",
+ ),
+ ),
],
options={
- 'verbose_name_plural': 'solutions',
- 'permissions': (('see_private_solution', 'See hidden solutions'),),
- 'verbose_name': 'solution',
+ "verbose_name_plural": "solutions",
+ "permissions": (("see_private_solution", "See hidden solutions"),),
+ "verbose_name": "solution",
},
),
migrations.CreateModel(
- name='Submission',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('date', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='submission time')),
- ('time', models.FloatField(db_index=True, null=True, verbose_name='execution time')),
- ('memory', models.FloatField(null=True, verbose_name='memory usage')),
- ('points', models.FloatField(db_index=True, null=True, verbose_name='points granted')),
- ('source', models.TextField(max_length=65536, verbose_name='source code')),
- ('status', models.CharField(choices=[('QU', 'Queued'), ('P', 'Processing'), ('G', 'Grading'), ('D', 'Completed'), ('IE', 'Internal Error'), ('CE', 'Compile Error'), ('AB', 'Aborted')], db_index=True, default='QU', max_length=2, verbose_name='status')),
- ('result', models.CharField(blank=True, choices=[('AC', 'Accepted'), ('WA', 'Wrong Answer'), ('TLE', 'Time Limit Exceeded'), ('MLE', 'Memory Limit Exceeded'), ('OLE', 'Output Limit Exceeded'), ('IR', 'Invalid Return'), ('RTE', 'Runtime Error'), ('CE', 'Compile Error'), ('IE', 'Internal Error'), ('SC', 'Short circuit'), ('AB', 'Aborted')], db_index=True, default=None, max_length=3, null=True, verbose_name='result')),
- ('error', models.TextField(blank=True, null=True, verbose_name='compile errors')),
- ('current_testcase', models.IntegerField(default=0)),
- ('batch', models.BooleanField(default=False, verbose_name='batched cases')),
- ('case_points', models.FloatField(default=0, verbose_name='test case points')),
- ('case_total', models.FloatField(default=0, verbose_name='test case total points')),
- ('was_rejudged', models.BooleanField(default=False, verbose_name='was rejudged by admin')),
- ('is_pretested', models.BooleanField(default=False, verbose_name='was ran on pretests only')),
- ('judged_on', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='judge.Judge', verbose_name='judged on')),
- ('language', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Language', verbose_name='submission language')),
- ('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Problem')),
- ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Profile')),
+ name="Submission",
+ fields=[
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "date",
+ models.DateTimeField(
+ auto_now_add=True, db_index=True, verbose_name="submission time"
+ ),
+ ),
+ (
+ "time",
+ models.FloatField(
+ db_index=True, null=True, verbose_name="execution time"
+ ),
+ ),
+ ("memory", models.FloatField(null=True, verbose_name="memory usage")),
+ (
+ "points",
+ models.FloatField(
+ db_index=True, null=True, verbose_name="points granted"
+ ),
+ ),
+ (
+ "source",
+ models.TextField(max_length=65536, verbose_name="source code"),
+ ),
+ (
+ "status",
+ models.CharField(
+ choices=[
+ ("QU", "Queued"),
+ ("P", "Processing"),
+ ("G", "Grading"),
+ ("D", "Completed"),
+ ("IE", "Internal Error"),
+ ("CE", "Compile Error"),
+ ("AB", "Aborted"),
+ ],
+ db_index=True,
+ default="QU",
+ max_length=2,
+ verbose_name="status",
+ ),
+ ),
+ (
+ "result",
+ models.CharField(
+ blank=True,
+ choices=[
+ ("AC", "Accepted"),
+ ("WA", "Wrong Answer"),
+ ("TLE", "Time Limit Exceeded"),
+ ("MLE", "Memory Limit Exceeded"),
+ ("OLE", "Output Limit Exceeded"),
+ ("IR", "Invalid Return"),
+ ("RTE", "Runtime Error"),
+ ("CE", "Compile Error"),
+ ("IE", "Internal Error"),
+ ("SC", "Short circuit"),
+ ("AB", "Aborted"),
+ ],
+ db_index=True,
+ default=None,
+ max_length=3,
+ null=True,
+ verbose_name="result",
+ ),
+ ),
+ (
+ "error",
+ models.TextField(
+ blank=True, null=True, verbose_name="compile errors"
+ ),
+ ),
+ ("current_testcase", models.IntegerField(default=0)),
+ (
+ "batch",
+ models.BooleanField(default=False, verbose_name="batched cases"),
+ ),
+ (
+ "case_points",
+ models.FloatField(default=0, verbose_name="test case points"),
+ ),
+ (
+ "case_total",
+ models.FloatField(default=0, verbose_name="test case total points"),
+ ),
+ (
+ "was_rejudged",
+ models.BooleanField(
+ default=False, verbose_name="was rejudged by admin"
+ ),
+ ),
+ (
+ "is_pretested",
+ models.BooleanField(
+ default=False, verbose_name="was ran on pretests only"
+ ),
+ ),
+ (
+ "judged_on",
+ models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to="judge.Judge",
+ verbose_name="judged on",
+ ),
+ ),
+ (
+ "language",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="judge.Language",
+ verbose_name="submission language",
+ ),
+ ),
+ (
+ "problem",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE, to="judge.Problem"
+ ),
+ ),
+ (
+ "user",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE, to="judge.Profile"
+ ),
+ ),
],
options={
- 'verbose_name_plural': 'submissions',
- 'permissions': (('abort_any_submission', 'Abort any submission'), ('rejudge_submission', 'Rejudge the submission'), ('rejudge_submission_lot', 'Rejudge a lot of submissions'), ('spam_submission', 'Submit without limit'), ('view_all_submission', 'View all submission'), ('resubmit_other', "Resubmit others' submission")),
- 'verbose_name': 'submission',
+ "verbose_name_plural": "submissions",
+ "permissions": (
+ ("abort_any_submission", "Abort any submission"),
+ ("rejudge_submission", "Rejudge the submission"),
+ ("rejudge_submission_lot", "Rejudge a lot of submissions"),
+ ("spam_submission", "Submit without limit"),
+ ("view_all_submission", "View all submission"),
+ ("resubmit_other", "Resubmit others' submission"),
+ ),
+ "verbose_name": "submission",
},
),
migrations.CreateModel(
- name='SubmissionTestCase',
+ name="SubmissionTestCase",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('case', models.IntegerField(verbose_name='test case ID')),
- ('status', models.CharField(choices=[('AC', 'Accepted'), ('WA', 'Wrong Answer'), ('TLE', 'Time Limit Exceeded'), ('MLE', 'Memory Limit Exceeded'), ('OLE', 'Output Limit Exceeded'), ('IR', 'Invalid Return'), ('RTE', 'Runtime Error'), ('CE', 'Compile Error'), ('IE', 'Internal Error'), ('SC', 'Short circuit'), ('AB', 'Aborted')], max_length=3, verbose_name='status flag')),
- ('time', models.FloatField(null=True, verbose_name='execution time')),
- ('memory', models.FloatField(null=True, verbose_name='memory usage')),
- ('points', models.FloatField(null=True, verbose_name='points granted')),
- ('total', models.FloatField(null=True, verbose_name='points possible')),
- ('batch', models.IntegerField(null=True, verbose_name='batch number')),
- ('feedback', models.CharField(blank=True, max_length=50, verbose_name='judging feedback')),
- ('extended_feedback', models.TextField(blank=True, verbose_name='extended judging feedback')),
- ('output', models.TextField(blank=True, verbose_name='program output')),
- ('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='test_cases', to='judge.Submission', verbose_name='associated submission')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("case", models.IntegerField(verbose_name="test case ID")),
+ (
+ "status",
+ models.CharField(
+ choices=[
+ ("AC", "Accepted"),
+ ("WA", "Wrong Answer"),
+ ("TLE", "Time Limit Exceeded"),
+ ("MLE", "Memory Limit Exceeded"),
+ ("OLE", "Output Limit Exceeded"),
+ ("IR", "Invalid Return"),
+ ("RTE", "Runtime Error"),
+ ("CE", "Compile Error"),
+ ("IE", "Internal Error"),
+ ("SC", "Short circuit"),
+ ("AB", "Aborted"),
+ ],
+ max_length=3,
+ verbose_name="status flag",
+ ),
+ ),
+ ("time", models.FloatField(null=True, verbose_name="execution time")),
+ ("memory", models.FloatField(null=True, verbose_name="memory usage")),
+ ("points", models.FloatField(null=True, verbose_name="points granted")),
+ ("total", models.FloatField(null=True, verbose_name="points possible")),
+ ("batch", models.IntegerField(null=True, verbose_name="batch number")),
+ (
+ "feedback",
+ models.CharField(
+ blank=True, max_length=50, verbose_name="judging feedback"
+ ),
+ ),
+ (
+ "extended_feedback",
+ models.TextField(
+ blank=True, verbose_name="extended judging feedback"
+ ),
+ ),
+ ("output", models.TextField(blank=True, verbose_name="program output")),
+ (
+ "submission",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="test_cases",
+ to="judge.Submission",
+ verbose_name="associated submission",
+ ),
+ ),
],
options={
- 'verbose_name_plural': 'submission test cases',
- 'verbose_name': 'submission test case',
+ "verbose_name_plural": "submission test cases",
+ "verbose_name": "submission test case",
},
),
migrations.CreateModel(
- name='Ticket',
+ name="Ticket",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('title', models.CharField(max_length=100, verbose_name='ticket title')),
- ('time', models.DateTimeField(auto_now_add=True, verbose_name='creation time')),
- ('notes', models.TextField(blank=True, help_text='Staff notes for this issue to aid in processing.', verbose_name='quick notes')),
- ('object_id', models.PositiveIntegerField(verbose_name='linked item ID')),
- ('is_open', models.BooleanField(default=True, verbose_name='is ticket open?')),
- ('assignees', models.ManyToManyField(related_name='assigned_tickets', to='judge.Profile', verbose_name='assignees')),
- ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType', verbose_name='linked item type')),
- ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tickets', to='judge.Profile', verbose_name='ticket creator')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "title",
+ models.CharField(max_length=100, verbose_name="ticket title"),
+ ),
+ (
+ "time",
+ models.DateTimeField(
+ auto_now_add=True, verbose_name="creation time"
+ ),
+ ),
+ (
+ "notes",
+ models.TextField(
+ blank=True,
+ help_text="Staff notes for this issue to aid in processing.",
+ verbose_name="quick notes",
+ ),
+ ),
+ (
+ "object_id",
+ models.PositiveIntegerField(verbose_name="linked item ID"),
+ ),
+ (
+ "is_open",
+ models.BooleanField(default=True, verbose_name="is ticket open?"),
+ ),
+ (
+ "assignees",
+ models.ManyToManyField(
+ related_name="assigned_tickets",
+ to="judge.Profile",
+ verbose_name="assignees",
+ ),
+ ),
+ (
+ "content_type",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="contenttypes.ContentType",
+ verbose_name="linked item type",
+ ),
+ ),
+ (
+ "user",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="tickets",
+ to="judge.Profile",
+ verbose_name="ticket creator",
+ ),
+ ),
],
),
migrations.CreateModel(
- name='TicketMessage',
+ name="TicketMessage",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('body', models.TextField(verbose_name='message body')),
- ('time', models.DateTimeField(auto_now_add=True, verbose_name='message time')),
- ('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', related_query_name='message', to='judge.Ticket', verbose_name='ticket')),
- ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ticket_messages', to='judge.Profile', verbose_name='poster')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("body", models.TextField(verbose_name="message body")),
+ (
+ "time",
+ models.DateTimeField(
+ auto_now_add=True, verbose_name="message time"
+ ),
+ ),
+ (
+ "ticket",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="messages",
+ related_query_name="message",
+ to="judge.Ticket",
+ verbose_name="ticket",
+ ),
+ ),
+ (
+ "user",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="ticket_messages",
+ to="judge.Profile",
+ verbose_name="poster",
+ ),
+ ),
],
),
migrations.AddField(
- model_name='problem',
- name='authors',
- field=models.ManyToManyField(blank=True, related_name='authored_problems', to='judge.Profile', verbose_name='creators'),
+ model_name="problem",
+ name="authors",
+ field=models.ManyToManyField(
+ blank=True,
+ related_name="authored_problems",
+ to="judge.Profile",
+ verbose_name="creators",
+ ),
),
migrations.AddField(
- model_name='problem',
- name='banned_users',
- field=models.ManyToManyField(blank=True, help_text='Bans the selected users from submitting to this problem.', to='judge.Profile', verbose_name='personae non gratae'),
+ model_name="problem",
+ name="banned_users",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="Bans the selected users from submitting to this problem.",
+ to="judge.Profile",
+ verbose_name="personae non gratae",
+ ),
),
migrations.AddField(
- model_name='problem',
- name='curators',
- field=models.ManyToManyField(blank=True, help_text='These users will be able to edit a problem, but not be publicly shown as an author.', related_name='curated_problems', to='judge.Profile', verbose_name='curators'),
+ model_name="problem",
+ name="curators",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="These users will be able to edit a problem, but not be publicly shown as an author.",
+ related_name="curated_problems",
+ to="judge.Profile",
+ verbose_name="curators",
+ ),
),
migrations.AddField(
- model_name='problem',
- name='group',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.ProblemGroup', verbose_name='problem group'),
+ model_name="problem",
+ name="group",
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="judge.ProblemGroup",
+ verbose_name="problem group",
+ ),
),
migrations.AddField(
- model_name='problem',
- name='license',
- field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='judge.License'),
+ model_name="problem",
+ name="license",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to="judge.License",
+ ),
),
migrations.AddField(
- model_name='problem',
- name='organizations',
- field=models.ManyToManyField(blank=True, help_text='If private, only these organizations may see the problem.', to='judge.Organization', verbose_name='organizations'),
+ model_name="problem",
+ name="organizations",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="If private, only these organizations may see the problem.",
+ to="judge.Organization",
+ verbose_name="organizations",
+ ),
),
migrations.AddField(
- model_name='problem',
- name='testers',
- field=models.ManyToManyField(blank=True, help_text='These users will be able to view a private problem, but not edit it.', related_name='tested_problems', to='judge.Profile', verbose_name='testers'),
+ model_name="problem",
+ name="testers",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="These users will be able to view a private problem, but not edit it.",
+ related_name="tested_problems",
+ to="judge.Profile",
+ verbose_name="testers",
+ ),
),
migrations.AddField(
- model_name='problem',
- name='types',
- field=models.ManyToManyField(to='judge.ProblemType', verbose_name='problem types'),
+ model_name="problem",
+ name="types",
+ field=models.ManyToManyField(
+ to="judge.ProblemType", verbose_name="problem types"
+ ),
),
migrations.AddField(
- model_name='privatemessage',
- name='sender',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_messages', to='judge.Profile', verbose_name='sender'),
+ model_name="privatemessage",
+ name="sender",
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="sent_messages",
+ to="judge.Profile",
+ verbose_name="sender",
+ ),
),
migrations.AddField(
- model_name='privatemessage',
- name='target',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_messages', to='judge.Profile', verbose_name='target'),
+ model_name="privatemessage",
+ name="target",
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="received_messages",
+ to="judge.Profile",
+ verbose_name="target",
+ ),
),
migrations.AddField(
- model_name='organizationrequest',
- name='user',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='requests', to='judge.Profile', verbose_name='user'),
+ model_name="organizationrequest",
+ name="user",
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="requests",
+ to="judge.Profile",
+ verbose_name="user",
+ ),
),
migrations.AddField(
- model_name='organization',
- name='admins',
- field=models.ManyToManyField(help_text='Those who can edit this organization', related_name='admin_of', to='judge.Profile', verbose_name='administrators'),
+ model_name="organization",
+ name="admins",
+ field=models.ManyToManyField(
+ help_text="Those who can edit this organization",
+ related_name="admin_of",
+ to="judge.Profile",
+ verbose_name="administrators",
+ ),
),
migrations.AddField(
- model_name='organization',
- name='registrant',
- field=models.ForeignKey(help_text='User who registered this organization', on_delete=django.db.models.deletion.CASCADE, related_name='registrant+', to='judge.Profile', verbose_name='registrant'),
+ model_name="organization",
+ name="registrant",
+ field=models.ForeignKey(
+ help_text="User who registered this organization",
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="registrant+",
+ to="judge.Profile",
+ verbose_name="registrant",
+ ),
),
migrations.AddField(
- model_name='languagelimit',
- name='problem',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='language_limits', to='judge.Problem', verbose_name='problem'),
+ model_name="languagelimit",
+ name="problem",
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="language_limits",
+ to="judge.Problem",
+ verbose_name="problem",
+ ),
),
migrations.AddField(
- model_name='judge',
- name='problems',
- field=models.ManyToManyField(related_name='judges', to='judge.Problem', verbose_name='problems'),
+ model_name="judge",
+ name="problems",
+ field=models.ManyToManyField(
+ related_name="judges", to="judge.Problem", verbose_name="problems"
+ ),
),
migrations.AddField(
- model_name='judge',
- name='runtimes',
- field=models.ManyToManyField(related_name='judges', to='judge.Language', verbose_name='judges'),
+ model_name="judge",
+ name="runtimes",
+ field=models.ManyToManyField(
+ related_name="judges", to="judge.Language", verbose_name="judges"
+ ),
),
migrations.AddField(
- model_name='contestsubmission',
- name='submission',
- field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='contest', to='judge.Submission', verbose_name='submission'),
+ model_name="contestsubmission",
+ name="submission",
+ field=models.OneToOneField(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="contest",
+ to="judge.Submission",
+ verbose_name="submission",
+ ),
),
migrations.AddField(
- model_name='contestproblem',
- name='problem',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contests', to='judge.Problem', verbose_name='problem'),
+ model_name="contestproblem",
+ name="problem",
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="contests",
+ to="judge.Problem",
+ verbose_name="problem",
+ ),
),
migrations.AddField(
- model_name='contestparticipation',
- name='user',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contest_history', to='judge.Profile', verbose_name='user'),
+ model_name="contestparticipation",
+ name="user",
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="contest_history",
+ to="judge.Profile",
+ verbose_name="user",
+ ),
),
migrations.AddField(
- model_name='contest',
- name='banned_users',
- field=models.ManyToManyField(blank=True, help_text='Bans the selected users from joining this contest.', to='judge.Profile', verbose_name='personae non gratae'),
+ model_name="contest",
+ name="banned_users",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="Bans the selected users from joining this contest.",
+ to="judge.Profile",
+ verbose_name="personae non gratae",
+ ),
),
migrations.AddField(
- model_name='contest',
- name='organizations',
- field=models.ManyToManyField(blank=True, help_text='If private, only these organizations may see the contest', to='judge.Organization', verbose_name='organizations'),
+ model_name="contest",
+ name="organizations",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="If private, only these organizations may see the contest",
+ to="judge.Organization",
+ verbose_name="organizations",
+ ),
),
migrations.AddField(
- model_name='contest',
- name='organizers',
- field=models.ManyToManyField(help_text='These people will be able to edit the contest.', related_name='_contest_organizers_+', to='judge.Profile'),
+ model_name="contest",
+ name="organizers",
+ field=models.ManyToManyField(
+ help_text="These people will be able to edit the contest.",
+ related_name="_contest_organizers_+",
+ to="judge.Profile",
+ ),
),
migrations.AddField(
- model_name='contest',
- name='problems',
- field=models.ManyToManyField(through='judge.ContestProblem', to='judge.Problem', verbose_name='problems'),
+ model_name="contest",
+ name="problems",
+ field=models.ManyToManyField(
+ through="judge.ContestProblem",
+ to="judge.Problem",
+ verbose_name="problems",
+ ),
),
migrations.AddField(
- model_name='contest',
- name='rate_exclude',
- field=models.ManyToManyField(blank=True, related_name='_contest_rate_exclude_+', to='judge.Profile', verbose_name='exclude from ratings'),
+ model_name="contest",
+ name="rate_exclude",
+ field=models.ManyToManyField(
+ blank=True,
+ related_name="_contest_rate_exclude_+",
+ to="judge.Profile",
+ verbose_name="exclude from ratings",
+ ),
),
migrations.AddField(
- model_name='contest',
- name='tags',
- field=models.ManyToManyField(blank=True, related_name='contests', to='judge.ContestTag', verbose_name='contest tags'),
+ model_name="contest",
+ name="tags",
+ field=models.ManyToManyField(
+ blank=True,
+ related_name="contests",
+ to="judge.ContestTag",
+ verbose_name="contest tags",
+ ),
),
migrations.AddField(
- model_name='commentvote',
- name='voter',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='voted_comments', to='judge.Profile'),
+ model_name="commentvote",
+ name="voter",
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="voted_comments",
+ to="judge.Profile",
+ ),
),
migrations.AddField(
- model_name='comment',
- name='author',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Profile', verbose_name='commenter'),
+ model_name="comment",
+ name="author",
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="judge.Profile",
+ verbose_name="commenter",
+ ),
),
migrations.AddField(
- model_name='comment',
- name='parent',
- field=mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='replies', to='judge.Comment', verbose_name='parent'),
+ model_name="comment",
+ name="parent",
+ field=mptt.fields.TreeForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="replies",
+ to="judge.Comment",
+ verbose_name="parent",
+ ),
),
migrations.AddField(
- model_name='blogpost',
- name='authors',
- field=models.ManyToManyField(blank=True, to='judge.Profile', verbose_name='authors'),
+ model_name="blogpost",
+ name="authors",
+ field=models.ManyToManyField(
+ blank=True, to="judge.Profile", verbose_name="authors"
+ ),
),
migrations.AlterUniqueTogether(
- name='rating',
- unique_together=set([('user', 'contest')]),
+ name="rating",
+ unique_together=set([("user", "contest")]),
),
migrations.AlterUniqueTogether(
- name='problemtranslation',
- unique_together=set([('problem', 'language')]),
+ name="problemtranslation",
+ unique_together=set([("problem", "language")]),
),
migrations.AlterUniqueTogether(
- name='languagelimit',
- unique_together=set([('problem', 'language')]),
+ name="languagelimit",
+ unique_together=set([("problem", "language")]),
),
migrations.AlterUniqueTogether(
- name='contestproblem',
- unique_together=set([('problem', 'contest')]),
+ name="contestproblem",
+ unique_together=set([("problem", "contest")]),
),
migrations.AlterUniqueTogether(
- name='contestparticipation',
- unique_together=set([('contest', 'user', 'virtual')]),
+ name="contestparticipation",
+ unique_together=set([("contest", "user", "virtual")]),
),
migrations.AlterUniqueTogether(
- name='commentvote',
- unique_together=set([('voter', 'comment')]),
+ name="commentvote",
+ unique_together=set([("voter", "comment")]),
),
]
diff --git a/judge/migrations/0085_submission_source.py b/judge/migrations/0085_submission_source.py
index 5bfa72acb8..7c1fd1be3b 100644
--- a/judge/migrations/0085_submission_source.py
+++ b/judge/migrations/0085_submission_source.py
@@ -5,35 +5,62 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0001_squashed_0084_contest_formats'),
+ ("judge", "0001_squashed_0084_contest_formats"),
]
operations = [
migrations.CreateModel(
- name='SubmissionSource',
+ name="SubmissionSource",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('source', models.TextField(max_length=65536, verbose_name='source code')),
- ('submission', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='link', to='judge.Submission', verbose_name='associated submission')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "source",
+ models.TextField(max_length=65536, verbose_name="source code"),
+ ),
+ (
+ "submission",
+ models.OneToOneField(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="link",
+ to="judge.Submission",
+ verbose_name="associated submission",
+ ),
+ ),
],
),
migrations.RunSQL(
- ["""INSERT INTO judge_submissionsource (source, submission_id)
- SELECT source, id AS 'submission_id' FROM judge_submission;"""],
- ["""UPDATE judge_submission sub
+ [
+ """INSERT INTO judge_submissionsource (source, submission_id)
+ SELECT source, id AS 'submission_id' FROM judge_submission;"""
+ ],
+ [
+ """UPDATE judge_submission sub
INNER JOIN judge_submissionsource src ON sub.id = src.submission_id
- SET sub.source = src.source;"""],
+ SET sub.source = src.source;"""
+ ],
elidable=True,
),
migrations.RemoveField(
- model_name='submission',
- name='source',
+ model_name="submission",
+ name="source",
),
migrations.AlterField(
- model_name='submissionsource',
- name='submission',
- field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='source', to='judge.Submission', verbose_name='associated submission'),
+ model_name="submissionsource",
+ name="submission",
+ field=models.OneToOneField(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="source",
+ to="judge.Submission",
+ verbose_name="associated submission",
+ ),
),
]
diff --git a/judge/migrations/0086_rating_ceiling.py b/judge/migrations/0086_rating_ceiling.py
index a544a21972..da24e8f66a 100644
--- a/judge/migrations/0086_rating_ceiling.py
+++ b/judge/migrations/0086_rating_ceiling.py
@@ -4,20 +4,29 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0085_submission_source'),
+ ("judge", "0085_submission_source"),
]
operations = [
migrations.AddField(
- model_name='contest',
- name='rating_ceiling',
- field=models.IntegerField(blank=True, help_text='Rating ceiling for contest', null=True, verbose_name='rating ceiling'),
+ model_name="contest",
+ name="rating_ceiling",
+ field=models.IntegerField(
+ blank=True,
+ help_text="Rating ceiling for contest",
+ null=True,
+ verbose_name="rating ceiling",
+ ),
),
migrations.AddField(
- model_name='contest',
- name='rating_floor',
- field=models.IntegerField(blank=True, help_text='Rating floor for contest', null=True, verbose_name='rating floor'),
+ model_name="contest",
+ name="rating_floor",
+ field=models.IntegerField(
+ blank=True,
+ help_text="Rating floor for contest",
+ null=True,
+ verbose_name="rating floor",
+ ),
),
]
diff --git a/judge/migrations/0087_problem_resource_limits.py b/judge/migrations/0087_problem_resource_limits.py
index e6da153fcb..8e8b5ee66c 100644
--- a/judge/migrations/0087_problem_resource_limits.py
+++ b/judge/migrations/0087_problem_resource_limits.py
@@ -5,20 +5,29 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0086_rating_ceiling'),
+ ("judge", "0086_rating_ceiling"),
]
operations = [
migrations.AlterField(
- model_name='problem',
- name='memory_limit',
- field=models.PositiveIntegerField(help_text='The memory limit for this problem, in kilobytes (e.g. 64mb = 65536 kilobytes).', verbose_name='memory limit'),
+ model_name="problem",
+ name="memory_limit",
+ field=models.PositiveIntegerField(
+ help_text="The memory limit for this problem, in kilobytes (e.g. 64mb = 65536 kilobytes).",
+ verbose_name="memory limit",
+ ),
),
migrations.AlterField(
- model_name='problem',
- name='time_limit',
- field=models.FloatField(help_text='The time limit for this problem, in seconds. Fractional seconds (e.g. 1.5) are supported.', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(2000)], verbose_name='time limit'),
+ model_name="problem",
+ name="time_limit",
+ field=models.FloatField(
+ help_text="The time limit for this problem, in seconds. Fractional seconds (e.g. 1.5) are supported.",
+ validators=[
+ django.core.validators.MinValueValidator(0),
+ django.core.validators.MaxValueValidator(2000),
+ ],
+ verbose_name="time limit",
+ ),
),
]
diff --git a/judge/migrations/0088_private_contests.py b/judge/migrations/0088_private_contests.py
index b3505b554d..22f7009fd6 100644
--- a/judge/migrations/0088_private_contests.py
+++ b/judge/migrations/0088_private_contests.py
@@ -4,34 +4,54 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0087_problem_resource_limits'),
+ ("judge", "0087_problem_resource_limits"),
]
operations = [
migrations.AlterModelOptions(
- name='contest',
- options={'permissions': (('see_private_contest', 'See private contests'), ('edit_own_contest', 'Edit own contests'), ('edit_all_contest', 'Edit all contests'), ('contest_rating', 'Rate contests'), ('contest_access_code', 'Contest access codes'), ('create_private_contest', 'Create private contests')), 'verbose_name': 'contest', 'verbose_name_plural': 'contests'},
+ name="contest",
+ options={
+ "permissions": (
+ ("see_private_contest", "See private contests"),
+ ("edit_own_contest", "Edit own contests"),
+ ("edit_all_contest", "Edit all contests"),
+ ("contest_rating", "Rate contests"),
+ ("contest_access_code", "Contest access codes"),
+ ("create_private_contest", "Create private contests"),
+ ),
+ "verbose_name": "contest",
+ "verbose_name_plural": "contests",
+ },
),
migrations.RenameField(
- model_name='contest',
- old_name='is_public',
- new_name='is_visible',
+ model_name="contest",
+ old_name="is_public",
+ new_name="is_visible",
),
migrations.AddField(
- model_name='contest',
- name='is_organization_private',
- field=models.BooleanField(default=False, verbose_name='private to organizations'),
+ model_name="contest",
+ name="is_organization_private",
+ field=models.BooleanField(
+ default=False, verbose_name="private to organizations"
+ ),
),
migrations.AddField(
- model_name='contest',
- name='private_contestants',
- field=models.ManyToManyField(blank=True, help_text='If private, only these users may see the contest', related_name='_contest_private_contestants_+', to='judge.Profile', verbose_name='private contestants'),
+ model_name="contest",
+ name="private_contestants",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="If private, only these users may see the contest",
+ related_name="_contest_private_contestants_+",
+ to="judge.Profile",
+ verbose_name="private contestants",
+ ),
),
migrations.AlterField(
- model_name='contest',
- name='is_private',
- field=models.BooleanField(default=False, verbose_name='private to specific users'),
+ model_name="contest",
+ name="is_private",
+ field=models.BooleanField(
+ default=False, verbose_name="private to specific users"
+ ),
),
]
diff --git a/judge/migrations/0089_submission_to_contest.py b/judge/migrations/0089_submission_to_contest.py
index 73fe6cb50a..3c07a7e79b 100644
--- a/judge/migrations/0089_submission_to_contest.py
+++ b/judge/migrations/0089_submission_to_contest.py
@@ -5,23 +5,32 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0088_private_contests'),
+ ("judge", "0088_private_contests"),
]
operations = [
migrations.AddField(
- model_name='submission',
- name='contest_object',
- field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='judge.Contest', verbose_name='contest'),
+ model_name="submission",
+ name="contest_object",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="+",
+ to="judge.Contest",
+ verbose_name="contest",
+ ),
),
- migrations.RunSQL("""
+ migrations.RunSQL(
+ """
UPDATE `judge_submission`
INNER JOIN `judge_contestsubmission`
ON (`judge_submission`.`id` = `judge_contestsubmission`.`submission_id`)
INNER JOIN `judge_contestparticipation`
ON (`judge_contestsubmission`.`participation_id` = `judge_contestparticipation`.`id`)
SET `judge_submission`.`contest_object_id` = `judge_contestparticipation`.`contest_id`
- """, migrations.RunSQL.noop),
+ """,
+ migrations.RunSQL.noop,
+ ),
]
diff --git a/judge/migrations/0090_fix_contest_visibility.py b/judge/migrations/0090_fix_contest_visibility.py
index 03c6b74e7b..0634489662 100644
--- a/judge/migrations/0090_fix_contest_visibility.py
+++ b/judge/migrations/0090_fix_contest_visibility.py
@@ -2,18 +2,20 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0089_submission_to_contest'),
+ ("judge", "0089_submission_to_contest"),
]
operations = [
- migrations.RunSQL("""
+ migrations.RunSQL(
+ """
UPDATE `judge_contest`
SET `judge_contest`.`is_private` = 0, `judge_contest`.`is_organization_private` = 1
WHERE `judge_contest`.`is_private` = 1
- """, """
+ """,
+ """
UPDATE `judge_contest`
SET `judge_contest`.`is_private` = `judge_contest`.`is_organization_private`
- """),
+ """,
+ ),
]
diff --git a/judge/migrations/0091_compiler_message_ansi2html.py b/judge/migrations/0091_compiler_message_ansi2html.py
index 7240f95212..b02ab5683f 100644
--- a/judge/migrations/0091_compiler_message_ansi2html.py
+++ b/judge/migrations/0091_compiler_message_ansi2html.py
@@ -5,17 +5,16 @@
def strip_error_html(apps, schema_editor):
- Submission = apps.get_model('judge', 'Submission')
+ Submission = apps.get_model("judge", "Submission")
for sub in Submission.objects.filter(error__isnull=False).iterator():
if sub.error:
sub.error = clean_html(lh.fromstring(sub.error)).text_content()
- sub.save(update_fields=['error'])
+ sub.save(update_fields=["error"])
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0090_fix_contest_visibility'),
+ ("judge", "0090_fix_contest_visibility"),
]
operations = [
diff --git a/judge/migrations/0092_contest_clone.py b/judge/migrations/0092_contest_clone.py
index 0235513a68..94cff02f04 100644
--- a/judge/migrations/0092_contest_clone.py
+++ b/judge/migrations/0092_contest_clone.py
@@ -4,14 +4,25 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0091_compiler_message_ansi2html'),
+ ("judge", "0091_compiler_message_ansi2html"),
]
operations = [
migrations.AlterModelOptions(
- name='contest',
- options={'permissions': (('see_private_contest', 'See private contests'), ('edit_own_contest', 'Edit own contests'), ('edit_all_contest', 'Edit all contests'), ('clone_contest', 'Clone contest'), ('contest_rating', 'Rate contests'), ('contest_access_code', 'Contest access codes'), ('create_private_contest', 'Create private contests')), 'verbose_name': 'contest', 'verbose_name_plural': 'contests'},
+ name="contest",
+ options={
+ "permissions": (
+ ("see_private_contest", "See private contests"),
+ ("edit_own_contest", "Edit own contests"),
+ ("edit_all_contest", "Edit all contests"),
+ ("clone_contest", "Clone contest"),
+ ("contest_rating", "Rate contests"),
+ ("contest_access_code", "Contest access codes"),
+ ("create_private_contest", "Create private contests"),
+ ),
+ "verbose_name": "contest",
+ "verbose_name_plural": "contests",
+ },
),
]
diff --git a/judge/migrations/0093_contest_moss.py b/judge/migrations/0093_contest_moss.py
index 10e2799561..b884878d13 100644
--- a/judge/migrations/0093_contest_moss.py
+++ b/judge/migrations/0093_contest_moss.py
@@ -5,41 +5,71 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0092_contest_clone'),
+ ("judge", "0092_contest_clone"),
]
operations = [
migrations.CreateModel(
- name='ContestMoss',
+ name="ContestMoss",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('language', models.CharField(max_length=10)),
- ('submission_count', models.PositiveIntegerField(default=0)),
- ('url', models.URLField(blank=True, null=True)),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("language", models.CharField(max_length=10)),
+ ("submission_count", models.PositiveIntegerField(default=0)),
+ ("url", models.URLField(blank=True, null=True)),
],
options={
- 'verbose_name': 'contest moss result',
- 'verbose_name_plural': 'contest moss results',
+ "verbose_name": "contest moss result",
+ "verbose_name_plural": "contest moss results",
},
),
migrations.AlterModelOptions(
- name='contest',
- options={'permissions': (('see_private_contest', 'See private contests'), ('edit_own_contest', 'Edit own contests'), ('edit_all_contest', 'Edit all contests'), ('clone_contest', 'Clone contest'), ('moss_contest', 'MOSS contest'), ('contest_rating', 'Rate contests'), ('contest_access_code', 'Contest access codes'), ('create_private_contest', 'Create private contests')), 'verbose_name': 'contest', 'verbose_name_plural': 'contests'},
+ name="contest",
+ options={
+ "permissions": (
+ ("see_private_contest", "See private contests"),
+ ("edit_own_contest", "Edit own contests"),
+ ("edit_all_contest", "Edit all contests"),
+ ("clone_contest", "Clone contest"),
+ ("moss_contest", "MOSS contest"),
+ ("contest_rating", "Rate contests"),
+ ("contest_access_code", "Contest access codes"),
+ ("create_private_contest", "Create private contests"),
+ ),
+ "verbose_name": "contest",
+ "verbose_name_plural": "contests",
+ },
),
migrations.AddField(
- model_name='contestmoss',
- name='contest',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='moss', to='judge.Contest', verbose_name='contest'),
+ model_name="contestmoss",
+ name="contest",
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="moss",
+ to="judge.Contest",
+ verbose_name="contest",
+ ),
),
migrations.AddField(
- model_name='contestmoss',
- name='problem',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='moss', to='judge.Problem', verbose_name='problem'),
+ model_name="contestmoss",
+ name="problem",
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="moss",
+ to="judge.Problem",
+ verbose_name="problem",
+ ),
),
migrations.AlterUniqueTogether(
- name='contestmoss',
- unique_together={('contest', 'problem', 'language')},
+ name="contestmoss",
+ unique_together={("contest", "problem", "language")},
),
]
diff --git a/judge/migrations/0094_submissiontestcase_unique_together.py b/judge/migrations/0094_submissiontestcase_unique_together.py
index 83413296c1..c56cdedbee 100644
--- a/judge/migrations/0094_submissiontestcase_unique_together.py
+++ b/judge/migrations/0094_submissiontestcase_unique_together.py
@@ -3,12 +3,12 @@
class Migration(migrations.Migration):
dependencies = [
- ('judge', '0093_contest_moss'),
+ ("judge", "0093_contest_moss"),
]
operations = [
migrations.AlterUniqueTogether(
- name='submissiontestcase',
- unique_together={('submission', 'case')},
+ name="submissiontestcase",
+ unique_together={("submission", "case")},
),
]
diff --git a/judge/migrations/0095_organization_logo_override.py b/judge/migrations/0095_organization_logo_override.py
index b1d86eb24b..09e9aed34b 100644
--- a/judge/migrations/0095_organization_logo_override.py
+++ b/judge/migrations/0095_organization_logo_override.py
@@ -4,15 +4,20 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0094_submissiontestcase_unique_together'),
+ ("judge", "0094_submissiontestcase_unique_together"),
]
operations = [
migrations.AddField(
- model_name='organization',
- name='logo_override_image',
- field=models.CharField(blank=True, default='', help_text='This image will replace the default site logo for users viewing the organization.', max_length=150, verbose_name='Logo override image'),
+ model_name="organization",
+ name="logo_override_image",
+ field=models.CharField(
+ blank=True,
+ default="",
+ help_text="This image will replace the default site logo for users viewing the organization.",
+ max_length=150,
+ verbose_name="Logo override image",
+ ),
),
]
diff --git a/judge/migrations/0096_profile_language_set_default.py b/judge/migrations/0096_profile_language_set_default.py
index a0a9453de6..c5d14b3357 100644
--- a/judge/migrations/0096_profile_language_set_default.py
+++ b/judge/migrations/0096_profile_language_set_default.py
@@ -7,21 +7,25 @@
def create_python3(apps, schema_editor):
- Language = apps.get_model('judge', 'Language')
- Language.objects.get_or_create(key='PY3', defaults={'name': 'Python 3'})[0]
+ Language = apps.get_model("judge", "Language")
+ Language.objects.get_or_create(key="PY3", defaults={"name": "Python 3"})[0]
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0095_organization_logo_override'),
+ ("judge", "0095_organization_logo_override"),
]
operations = [
migrations.RunPython(create_python3, reverse_code=migrations.RunPython.noop),
migrations.AlterField(
- model_name='profile',
- name='language',
- field=models.ForeignKey(default=judge.models.runtime.Language.get_default_language_pk, on_delete=django.db.models.deletion.SET_DEFAULT, to='judge.Language', verbose_name='preferred language'),
+ model_name="profile",
+ name="language",
+ field=models.ForeignKey(
+ default=judge.models.runtime.Language.get_default_language_pk,
+ on_delete=django.db.models.deletion.SET_DEFAULT,
+ to="judge.Language",
+ verbose_name="preferred language",
+ ),
),
]
diff --git a/judge/migrations/0097_participation_is_disqualified.py b/judge/migrations/0097_participation_is_disqualified.py
index 537f83f0be..835e971159 100644
--- a/judge/migrations/0097_participation_is_disqualified.py
+++ b/judge/migrations/0097_participation_is_disqualified.py
@@ -4,20 +4,27 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0096_profile_language_set_default'),
+ ("judge", "0096_profile_language_set_default"),
]
operations = [
migrations.AddField(
- model_name='contestparticipation',
- name='is_disqualified',
- field=models.BooleanField(default=False, help_text='Whether this participation is disqualified.', verbose_name='is disqualified'),
+ model_name="contestparticipation",
+ name="is_disqualified",
+ field=models.BooleanField(
+ default=False,
+ help_text="Whether this participation is disqualified.",
+ verbose_name="is disqualified",
+ ),
),
migrations.AlterField(
- model_name='contestparticipation',
- name='virtual',
- field=models.IntegerField(default=0, help_text='0 means non-virtual, otherwise the n-th virtual participation.', verbose_name='virtual participation id'),
+ model_name="contestparticipation",
+ name="virtual",
+ field=models.IntegerField(
+ default=0,
+ help_text="0 means non-virtual, otherwise the n-th virtual participation.",
+ verbose_name="virtual participation id",
+ ),
),
]
diff --git a/judge/migrations/0098_view_contest_scoreboard.py b/judge/migrations/0098_view_contest_scoreboard.py
index ae000b1217..82f2f194d3 100644
--- a/judge/migrations/0098_view_contest_scoreboard.py
+++ b/judge/migrations/0098_view_contest_scoreboard.py
@@ -4,15 +4,20 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0097_participation_is_disqualified'),
+ ("judge", "0097_participation_is_disqualified"),
]
operations = [
migrations.AddField(
- model_name='contest',
- name='view_contest_scoreboard',
- field=models.ManyToManyField(blank=True, help_text='These users will be able to view the scoreboard.', related_name='view_contest_scoreboard', to='judge.Profile', verbose_name='view contest scoreboard'),
+ model_name="contest",
+ name="view_contest_scoreboard",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="These users will be able to view the scoreboard.",
+ related_name="view_contest_scoreboard",
+ to="judge.Profile",
+ verbose_name="view contest scoreboard",
+ ),
),
]
diff --git a/judge/migrations/0099_contest_problem_label.py b/judge/migrations/0099_contest_problem_label.py
index 4bd5db7a4a..6247a92e33 100644
--- a/judge/migrations/0099_contest_problem_label.py
+++ b/judge/migrations/0099_contest_problem_label.py
@@ -4,19 +4,36 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0098_view_contest_scoreboard'),
+ ("judge", "0098_view_contest_scoreboard"),
]
operations = [
migrations.AddField(
- model_name='contest',
- name='problem_label_script',
- field=models.TextField(blank=True, help_text='A custom Lua function to generate problem labels. Requires a single function with an integer parameter, the zero-indexed contest problem index, and returns a string, the label.', verbose_name='contest problem label script'),
+ model_name="contest",
+ name="problem_label_script",
+ field=models.TextField(
+ blank=True,
+ help_text="A custom Lua function to generate problem labels. Requires a single function with an integer parameter, the zero-indexed contest problem index, and returns a string, the label.",
+ verbose_name="contest problem label script",
+ ),
),
migrations.AlterModelOptions(
- name='contest',
- options={'permissions': (('see_private_contest', 'See private contests'), ('edit_own_contest', 'Edit own contests'), ('edit_all_contest', 'Edit all contests'), ('clone_contest', 'Clone contest'), ('moss_contest', 'MOSS contest'), ('contest_rating', 'Rate contests'), ('contest_access_code', 'Contest access codes'), ('create_private_contest', 'Create private contests'), ('contest_problem_label', 'Edit contest problem label script')), 'verbose_name': 'contest', 'verbose_name_plural': 'contests'},
+ name="contest",
+ options={
+ "permissions": (
+ ("see_private_contest", "See private contests"),
+ ("edit_own_contest", "Edit own contests"),
+ ("edit_all_contest", "Edit all contests"),
+ ("clone_contest", "Clone contest"),
+ ("moss_contest", "MOSS contest"),
+ ("contest_rating", "Rate contests"),
+ ("contest_access_code", "Contest access codes"),
+ ("create_private_contest", "Create private contests"),
+ ("contest_problem_label", "Edit contest problem label script"),
+ ),
+ "verbose_name": "contest",
+ "verbose_name_plural": "contests",
+ },
),
]
diff --git a/judge/migrations/0100_contest_visiblity_permission.py b/judge/migrations/0100_contest_visiblity_permission.py
index 784ed60b91..e56f67c8a3 100644
--- a/judge/migrations/0100_contest_visiblity_permission.py
+++ b/judge/migrations/0100_contest_visiblity_permission.py
@@ -4,14 +4,28 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0099_contest_problem_label'),
+ ("judge", "0099_contest_problem_label"),
]
operations = [
migrations.AlterModelOptions(
- name='contest',
- options={'permissions': (('see_private_contest', 'See private contests'), ('edit_own_contest', 'Edit own contests'), ('edit_all_contest', 'Edit all contests'), ('clone_contest', 'Clone contest'), ('moss_contest', 'MOSS contest'), ('contest_rating', 'Rate contests'), ('contest_access_code', 'Contest access codes'), ('create_private_contest', 'Create private contests'), ('contest_problem_label', 'Edit contest problem label script'), ('change_contest_visibility', 'Change contest visibility')), 'verbose_name': 'contest', 'verbose_name_plural': 'contests'},
+ name="contest",
+ options={
+ "permissions": (
+ ("see_private_contest", "See private contests"),
+ ("edit_own_contest", "Edit own contests"),
+ ("edit_all_contest", "Edit all contests"),
+ ("clone_contest", "Clone contest"),
+ ("moss_contest", "MOSS contest"),
+ ("contest_rating", "Rate contests"),
+ ("contest_access_code", "Contest access codes"),
+ ("create_private_contest", "Create private contests"),
+ ("contest_problem_label", "Edit contest problem label script"),
+ ("change_contest_visibility", "Change contest visibility"),
+ ),
+ "verbose_name": "contest",
+ "verbose_name_plural": "contests",
+ },
),
]
diff --git a/judge/migrations/0101_submission_judged_date.py b/judge/migrations/0101_submission_judged_date.py
index cac63688fc..5670436ff2 100644
--- a/judge/migrations/0101_submission_judged_date.py
+++ b/judge/migrations/0101_submission_judged_date.py
@@ -4,15 +4,16 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0100_contest_visiblity_permission'),
+ ("judge", "0100_contest_visiblity_permission"),
]
operations = [
migrations.AddField(
- model_name='submission',
- name='judged_date',
- field=models.DateTimeField(default=None, null=True, verbose_name='submission judge time'),
+ model_name="submission",
+ name="judged_date",
+ field=models.DateTimeField(
+ default=None, null=True, verbose_name="submission judge time"
+ ),
),
]
diff --git a/judge/migrations/0102_api_token.py b/judge/migrations/0102_api_token.py
index 1e4c2da9cd..26dc63bba6 100644
--- a/judge/migrations/0102_api_token.py
+++ b/judge/migrations/0102_api_token.py
@@ -5,15 +5,24 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0101_submission_judged_date'),
+ ("judge", "0101_submission_judged_date"),
]
operations = [
migrations.AddField(
- model_name='profile',
- name='api_token',
- field=models.CharField(help_text='64 character hex-encoded API access token', max_length=64, null=True, validators=[django.core.validators.RegexValidator('^[a-f0-9]{64}$', 'API token must be None or hexadecimal')], verbose_name='API token'),
+ model_name="profile",
+ name="api_token",
+ field=models.CharField(
+ help_text="64 character hex-encoded API access token",
+ max_length=64,
+ null=True,
+ validators=[
+ django.core.validators.RegexValidator(
+ "^[a-f0-9]{64}$", "API token must be None or hexadecimal"
+ )
+ ],
+ verbose_name="API token",
+ ),
),
]
diff --git a/judge/migrations/0103_contest_participation_tiebreak_field.py b/judge/migrations/0103_contest_participation_tiebreak_field.py
index d11bce272a..5b89fc6dff 100644
--- a/judge/migrations/0103_contest_participation_tiebreak_field.py
+++ b/judge/migrations/0103_contest_participation_tiebreak_field.py
@@ -4,15 +4,14 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0102_api_token'),
+ ("judge", "0102_api_token"),
]
operations = [
migrations.AddField(
- model_name='contestparticipation',
- name='tiebreaker',
- field=models.FloatField(default=0.0, verbose_name='tie-breaking field'),
+ model_name="contestparticipation",
+ name="tiebreaker",
+ field=models.FloatField(default=0.0, verbose_name="tie-breaking field"),
),
]
diff --git a/judge/migrations/0104_contestproblem_maxsubs.py b/judge/migrations/0104_contestproblem_maxsubs.py
index 776d7bc026..ddcf8d30df 100644
--- a/judge/migrations/0104_contestproblem_maxsubs.py
+++ b/judge/migrations/0104_contestproblem_maxsubs.py
@@ -6,26 +6,35 @@
def zero_to_none(apps, schema_editor):
- ContestProblem = apps.get_model('judge', 'ContestProblem')
+ ContestProblem = apps.get_model("judge", "ContestProblem")
ContestProblem.objects.filter(max_submissions=0).update(max_submissions=None)
def none_to_zero(apps, schema_editor):
- ContestProblem = apps.get_model('judge', 'ContestProblem')
+ ContestProblem = apps.get_model("judge", "ContestProblem")
ContestProblem.objects.filter(max_submissions=None).update(max_submissions=0)
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0103_contest_participation_tiebreak_field'),
+ ("judge", "0103_contest_participation_tiebreak_field"),
]
operations = [
migrations.AlterField(
- model_name='contestproblem',
- name='max_submissions',
- field=models.IntegerField(blank=True, default=None, help_text='Maximum number of submissions for this problem, or leave blank for no limit.', null=True, validators=[judge.models.contest.MinValueOrNoneValidator(1, "Why include a problem you can't submit to?")]),
+ model_name="contestproblem",
+ name="max_submissions",
+ field=models.IntegerField(
+ blank=True,
+ default=None,
+ help_text="Maximum number of submissions for this problem, or leave blank for no limit.",
+ null=True,
+ validators=[
+ judge.models.contest.MinValueOrNoneValidator(
+ 1, "Why include a problem you can't submit to?"
+ )
+ ],
+ ),
),
migrations.RunPython(zero_to_none, none_to_zero, atomic=True),
]
diff --git a/judge/migrations/0105_webauthn.py b/judge/migrations/0105_webauthn.py
index 4bf2289188..52b3bc1f3e 100644
--- a/judge/migrations/0105_webauthn.py
+++ b/judge/migrations/0105_webauthn.py
@@ -5,26 +5,50 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0104_contestproblem_maxsubs'),
+ ("judge", "0104_contestproblem_maxsubs"),
]
operations = [
migrations.AddField(
- model_name='profile',
- name='is_webauthn_enabled',
- field=models.BooleanField(default=False, help_text='check to enable WebAuthn-based two-factor authentication', verbose_name='WebAuthn 2FA enabled'),
+ model_name="profile",
+ name="is_webauthn_enabled",
+ field=models.BooleanField(
+ default=False,
+ help_text="check to enable WebAuthn-based two-factor authentication",
+ verbose_name="WebAuthn 2FA enabled",
+ ),
),
migrations.CreateModel(
- name='WebAuthnCredential',
+ name="WebAuthnCredential",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('name', models.CharField(max_length=100, verbose_name='device name')),
- ('cred_id', models.CharField(max_length=255, unique=True, verbose_name='credential ID')),
- ('public_key', models.TextField(verbose_name='public key')),
- ('counter', models.BigIntegerField(verbose_name='sign counter')),
- ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='webauthn_credentials', to='judge.Profile', verbose_name='user')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("name", models.CharField(max_length=100, verbose_name="device name")),
+ (
+ "cred_id",
+ models.CharField(
+ max_length=255, unique=True, verbose_name="credential ID"
+ ),
+ ),
+ ("public_key", models.TextField(verbose_name="public key")),
+ ("counter", models.BigIntegerField(verbose_name="sign counter")),
+ (
+ "user",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="webauthn_credentials",
+ to="judge.Profile",
+ verbose_name="user",
+ ),
+ ),
],
),
]
diff --git a/judge/migrations/0106_user_data_download.py b/judge/migrations/0106_user_data_download.py
index 1a13e431bb..ddbbece380 100644
--- a/judge/migrations/0106_user_data_download.py
+++ b/judge/migrations/0106_user_data_download.py
@@ -4,15 +4,16 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0105_webauthn'),
+ ("judge", "0105_webauthn"),
]
operations = [
migrations.AddField(
- model_name='profile',
- name='data_last_downloaded',
- field=models.DateTimeField(blank=True, null=True, verbose_name='last data download time'),
+ model_name="profile",
+ name="data_last_downloaded",
+ field=models.DateTimeField(
+ blank=True, null=True, verbose_name="last data download time"
+ ),
),
]
diff --git a/judge/migrations/0107_submission_lock.py b/judge/migrations/0107_submission_lock.py
index 1d5f796b89..69d628f650 100644
--- a/judge/migrations/0107_submission_lock.py
+++ b/judge/migrations/0107_submission_lock.py
@@ -5,37 +5,72 @@
def updatecontestsubmissions(apps, schema_editor):
- Contest = apps.get_model('judge', 'Contest')
+ Contest = apps.get_model("judge", "Contest")
Contest.objects.filter(end_time__lt=timezone.now()).update(is_locked=True)
- Submission = apps.get_model('judge', 'Submission')
- Submission.objects.filter(contest_object__is_locked=True, contest__participation__virtual=0).update(is_locked=True)
+ Submission = apps.get_model("judge", "Submission")
+ Submission.objects.filter(
+ contest_object__is_locked=True, contest__participation__virtual=0
+ ).update(is_locked=True)
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0106_user_data_download'),
+ ("judge", "0106_user_data_download"),
]
operations = [
migrations.AlterModelOptions(
- name='contest',
- options={'permissions': (('see_private_contest', 'See private contests'), ('edit_own_contest', 'Edit own contests'), ('edit_all_contest', 'Edit all contests'), ('clone_contest', 'Clone contest'), ('moss_contest', 'MOSS contest'), ('contest_rating', 'Rate contests'), ('contest_access_code', 'Contest access codes'), ('create_private_contest', 'Create private contests'), ('change_contest_visibility', 'Change contest visibility'), ('contest_problem_label', 'Edit contest problem label script'), ('lock_contest', 'Change lock status of contest')), 'verbose_name': 'contest', 'verbose_name_plural': 'contests'},
+ name="contest",
+ options={
+ "permissions": (
+ ("see_private_contest", "See private contests"),
+ ("edit_own_contest", "Edit own contests"),
+ ("edit_all_contest", "Edit all contests"),
+ ("clone_contest", "Clone contest"),
+ ("moss_contest", "MOSS contest"),
+ ("contest_rating", "Rate contests"),
+ ("contest_access_code", "Contest access codes"),
+ ("create_private_contest", "Create private contests"),
+ ("change_contest_visibility", "Change contest visibility"),
+ ("contest_problem_label", "Edit contest problem label script"),
+ ("lock_contest", "Change lock status of contest"),
+ ),
+ "verbose_name": "contest",
+ "verbose_name_plural": "contests",
+ },
),
migrations.AlterModelOptions(
- name='submission',
- options={'permissions': (('abort_any_submission', 'Abort any submission'), ('rejudge_submission', 'Rejudge the submission'), ('rejudge_submission_lot', 'Rejudge a lot of submissions'), ('spam_submission', 'Submit without limit'), ('view_all_submission', 'View all submission'), ('resubmit_other', "Resubmit others' submission"), ('lock_submission', 'Change lock status of submission')), 'verbose_name': 'submission', 'verbose_name_plural': 'submissions'},
+ name="submission",
+ options={
+ "permissions": (
+ ("abort_any_submission", "Abort any submission"),
+ ("rejudge_submission", "Rejudge the submission"),
+ ("rejudge_submission_lot", "Rejudge a lot of submissions"),
+ ("spam_submission", "Submit without limit"),
+ ("view_all_submission", "View all submission"),
+ ("resubmit_other", "Resubmit others' submission"),
+ ("lock_submission", "Change lock status of submission"),
+ ),
+ "verbose_name": "submission",
+ "verbose_name_plural": "submissions",
+ },
),
migrations.AddField(
- model_name='contest',
- name='is_locked',
- field=models.BooleanField(default=False, help_text='Prevent submissions from this contest from being rejudged.', verbose_name='contest lock'),
+ model_name="contest",
+ name="is_locked",
+ field=models.BooleanField(
+ default=False,
+ help_text="Prevent submissions from this contest from being rejudged.",
+ verbose_name="contest lock",
+ ),
),
migrations.AddField(
- model_name='submission',
- name='is_locked',
- field=models.BooleanField(default=False, verbose_name='lock submission'),
+ model_name="submission",
+ name="is_locked",
+ field=models.BooleanField(default=False, verbose_name="lock submission"),
+ ),
+ migrations.RunPython(
+ updatecontestsubmissions, reverse_code=migrations.RunPython.noop
),
- migrations.RunPython(updatecontestsubmissions, reverse_code=migrations.RunPython.noop),
]
diff --git a/judge/migrations/0108_bleach_problems.py b/judge/migrations/0108_bleach_problems.py
index 7e0e2ce61b..11e063c751 100644
--- a/judge/migrations/0108_bleach_problems.py
+++ b/judge/migrations/0108_bleach_problems.py
@@ -4,19 +4,34 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0107_submission_lock'),
+ ("judge", "0107_submission_lock"),
]
operations = [
migrations.AlterModelOptions(
- name='problem',
- options={'permissions': (('see_private_problem', 'See hidden problems'), ('edit_own_problem', 'Edit own problems'), ('edit_all_problem', 'Edit all problems'), ('edit_public_problem', 'Edit all public problems'), ('problem_full_markup', 'Edit problems with full markup'), ('clone_problem', 'Clone problem'), ('change_public_visibility', 'Change is_public field'), ('change_manually_managed', 'Change is_manually_managed field'), ('see_organization_problem', 'See organization-private problems')), 'verbose_name': 'problem', 'verbose_name_plural': 'problems'},
+ name="problem",
+ options={
+ "permissions": (
+ ("see_private_problem", "See hidden problems"),
+ ("edit_own_problem", "Edit own problems"),
+ ("edit_all_problem", "Edit all problems"),
+ ("edit_public_problem", "Edit all public problems"),
+ ("problem_full_markup", "Edit problems with full markup"),
+ ("clone_problem", "Clone problem"),
+ ("change_public_visibility", "Change is_public field"),
+ ("change_manually_managed", "Change is_manually_managed field"),
+ ("see_organization_problem", "See organization-private problems"),
+ ),
+ "verbose_name": "problem",
+ "verbose_name_plural": "problems",
+ },
),
migrations.AddField(
- model_name='problem',
- name='is_full_markup',
- field=models.BooleanField(default=False, verbose_name='allow full markdown access'),
+ model_name="problem",
+ name="is_full_markup",
+ field=models.BooleanField(
+ default=False, verbose_name="allow full markdown access"
+ ),
),
]
diff --git a/judge/migrations/0109_scratch_codes.py b/judge/migrations/0109_scratch_codes.py
index 4c44d9e446..cd566ef4e3 100644
--- a/judge/migrations/0109_scratch_codes.py
+++ b/judge/migrations/0109_scratch_codes.py
@@ -8,15 +8,26 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0108_bleach_problems'),
+ ("judge", "0108_bleach_problems"),
]
operations = [
migrations.AddField(
- model_name='profile',
- name='scratch_codes',
- field=judge.models.profile.EncryptedNullCharField(blank=True, help_text='JSON array of 16 character base32-encoded codes for scratch codes', max_length=255, null=True, validators=[django.core.validators.RegexValidator(r'^(\[\])?$|^\[("[A-Z0-9]{16}", *)*"[A-Z0-9]{16}"\]$', 'Scratch codes must be empty or a JSON array of 16-character base32 codes')], verbose_name='scratch codes'),
+ model_name="profile",
+ name="scratch_codes",
+ field=judge.models.profile.EncryptedNullCharField(
+ blank=True,
+ help_text="JSON array of 16 character base32-encoded codes for scratch codes",
+ max_length=255,
+ null=True,
+ validators=[
+ django.core.validators.RegexValidator(
+ r'^(\[\])?$|^\[("[A-Z0-9]{16}", *)*"[A-Z0-9]{16}"\]$',
+ "Scratch codes must be empty or a JSON array of 16-character base32 codes",
+ )
+ ],
+ verbose_name="scratch codes",
+ ),
),
]
diff --git a/judge/migrations/0110_default_output_prefix_override.py b/judge/migrations/0110_default_output_prefix_override.py
index 0172b33015..8dde38abf3 100644
--- a/judge/migrations/0110_default_output_prefix_override.py
+++ b/judge/migrations/0110_default_output_prefix_override.py
@@ -4,15 +4,19 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0109_scratch_codes'),
+ ("judge", "0109_scratch_codes"),
]
operations = [
migrations.AlterField(
- model_name='contestproblem',
- name='output_prefix_override',
- field=models.IntegerField(blank=True, default=0, null=True, verbose_name='output prefix length override'),
+ model_name="contestproblem",
+ name="output_prefix_override",
+ field=models.IntegerField(
+ blank=True,
+ default=0,
+ null=True,
+ verbose_name="output prefix length override",
+ ),
),
]
diff --git a/judge/migrations/0111_blank_assignees_ticket.py b/judge/migrations/0111_blank_assignees_ticket.py
index 028f5d9e88..f9f630e3e3 100644
--- a/judge/migrations/0111_blank_assignees_ticket.py
+++ b/judge/migrations/0111_blank_assignees_ticket.py
@@ -4,15 +4,19 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0110_default_output_prefix_override'),
+ ("judge", "0110_default_output_prefix_override"),
]
operations = [
migrations.AlterField(
- model_name='ticket',
- name='assignees',
- field=models.ManyToManyField(blank=True, related_name='assigned_tickets', to='judge.Profile', verbose_name='assignees'),
+ model_name="ticket",
+ name="assignees",
+ field=models.ManyToManyField(
+ blank=True,
+ related_name="assigned_tickets",
+ to="judge.Profile",
+ verbose_name="assignees",
+ ),
),
]
diff --git a/judge/migrations/0112_language_extensions.py b/judge/migrations/0112_language_extensions.py
index ad8d3b1778..2fb7130975 100644
--- a/judge/migrations/0112_language_extensions.py
+++ b/judge/migrations/0112_language_extensions.py
@@ -2,70 +2,70 @@
def update_language_extensions(apps, schema_editor):
- Language = apps.get_model('judge', 'Language')
+ Language = apps.get_model("judge", "Language")
extension_mapping = {
- 'ADA': 'adb',
- 'AWK': 'awk',
- 'BASH': 'sh',
- 'BF': 'c',
- 'C': 'c',
- 'C11': 'c',
- 'CBL': 'cbl',
- 'CLANG': 'c',
- 'CLANGX': 'cpp',
- 'COFFEE': 'coffee',
- 'CPP03': 'cpp',
- 'CPP11': 'cpp',
- 'CPP14': 'cpp',
- 'CPP17': 'cpp',
- 'D': 'd',
- 'DART': 'dart',
- 'F95': 'f95',
- 'FORTH': 'fs',
- 'GAS32': 'asm',
- 'GAS64': 'asm',
- 'GASARM': 'asm',
- 'GO': 'go',
- 'GROOVY': 'groovy',
- 'HASK': 'hs',
- 'ICK': 'i',
- 'JAVA10': 'java',
- 'JAVA11': 'java',
- 'JAVA8': 'java',
- 'JAVA9': 'java',
- 'KOTLIN': 'kt',
- 'LUA': 'lua',
- 'MONOCS': 'cs',
- 'MONOFS': 'fs',
- 'MONOVB': 'vb',
- 'NASM': 'asm',
- 'NASM64': 'asm',
- 'OBJC': 'm',
- 'OCAML': 'ml',
- 'PAS': 'pas',
- 'PERL': 'pl',
- 'PHP': 'php',
- 'PIKE': 'pike',
- 'PRO': 'pl',
- 'PY2': 'py',
- 'PY3': 'py',
- 'PYPY': 'py',
- 'PYPY3': 'py',
- 'RKT': 'rkt',
- 'RUBY18': 'rb',
- 'RUBY2': 'rb',
- 'RUST': 'rs',
- 'SBCL': 'cl',
- 'SCALA': 'scala',
- 'SCM': 'scm',
- 'SED': 'sed',
- 'SWIFT': 'swift',
- 'TCL': 'tcl',
- 'TEXT': 'txt',
- 'TUR': 't',
- 'V8JS': 'js',
- 'ZIG': 'zig',
+ "ADA": "adb",
+ "AWK": "awk",
+ "BASH": "sh",
+ "BF": "c",
+ "C": "c",
+ "C11": "c",
+ "CBL": "cbl",
+ "CLANG": "c",
+ "CLANGX": "cpp",
+ "COFFEE": "coffee",
+ "CPP03": "cpp",
+ "CPP11": "cpp",
+ "CPP14": "cpp",
+ "CPP17": "cpp",
+ "D": "d",
+ "DART": "dart",
+ "F95": "f95",
+ "FORTH": "fs",
+ "GAS32": "asm",
+ "GAS64": "asm",
+ "GASARM": "asm",
+ "GO": "go",
+ "GROOVY": "groovy",
+ "HASK": "hs",
+ "ICK": "i",
+ "JAVA10": "java",
+ "JAVA11": "java",
+ "JAVA8": "java",
+ "JAVA9": "java",
+ "KOTLIN": "kt",
+ "LUA": "lua",
+ "MONOCS": "cs",
+ "MONOFS": "fs",
+ "MONOVB": "vb",
+ "NASM": "asm",
+ "NASM64": "asm",
+ "OBJC": "m",
+ "OCAML": "ml",
+ "PAS": "pas",
+ "PERL": "pl",
+ "PHP": "php",
+ "PIKE": "pike",
+ "PRO": "pl",
+ "PY2": "py",
+ "PY3": "py",
+ "PYPY": "py",
+ "PYPY3": "py",
+ "RKT": "rkt",
+ "RUBY18": "rb",
+ "RUBY2": "rb",
+ "RUST": "rs",
+ "SBCL": "cl",
+ "SCALA": "scala",
+ "SCM": "scm",
+ "SED": "sed",
+ "SWIFT": "swift",
+ "TCL": "tcl",
+ "TEXT": "txt",
+ "TUR": "t",
+ "V8JS": "js",
+ "ZIG": "zig",
}
languages = Language.objects.all()
@@ -73,20 +73,24 @@ def update_language_extensions(apps, schema_editor):
try:
extension = extension_mapping[language.key]
except KeyError:
- print('Warning: no extension found for %s. Setting extension to language key.' % language.key)
+ print(
+ "Warning: no extension found for %s. Setting extension to language key."
+ % language.key
+ )
extension = language.key.lower()
language.extension = extension
- Language.objects.bulk_update(languages, ['extension'])
+ Language.objects.bulk_update(languages, ["extension"])
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0111_blank_assignees_ticket'),
+ ("judge", "0111_blank_assignees_ticket"),
]
operations = [
- migrations.RunPython(update_language_extensions, reverse_code=migrations.RunPython.noop),
+ migrations.RunPython(
+ update_language_extensions, reverse_code=migrations.RunPython.noop
+ ),
]
diff --git a/judge/migrations/0113_contest_decimal_points.py b/judge/migrations/0113_contest_decimal_points.py
index 9031157dd6..fd5acc1631 100644
--- a/judge/migrations/0113_contest_decimal_points.py
+++ b/judge/migrations/0113_contest_decimal_points.py
@@ -4,20 +4,27 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0112_language_extensions'),
+ ("judge", "0112_language_extensions"),
]
operations = [
migrations.AddField(
- model_name='contest',
- name='points_precision',
- field=models.IntegerField(default=3, help_text='Number of digits to round points to.', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(10)], verbose_name='precision points'),
+ model_name="contest",
+ name="points_precision",
+ field=models.IntegerField(
+ default=3,
+ help_text="Number of digits to round points to.",
+ validators=[
+ django.core.validators.MinValueValidator(0),
+ django.core.validators.MaxValueValidator(10),
+ ],
+ verbose_name="precision points",
+ ),
),
migrations.AlterField(
- model_name='contestparticipation',
- name='score',
- field=models.FloatField(db_index=True, default=0, verbose_name='score'),
+ model_name="contestparticipation",
+ name="score",
+ field=models.FloatField(db_index=True, default=0, verbose_name="score"),
),
]
diff --git a/judge/migrations/0114_remove_org_registrant.py b/judge/migrations/0114_remove_org_registrant.py
index e494942d43..835ac3fc65 100644
--- a/judge/migrations/0114_remove_org_registrant.py
+++ b/judge/migrations/0114_remove_org_registrant.py
@@ -4,7 +4,7 @@
def make_admin_registrant(apps, schema_editor):
- Organization = apps.get_model('judge', 'Organization')
+ Organization = apps.get_model("judge", "Organization")
db_alias = schema_editor.connection.alias
for org in Organization.objects.using(db_alias).all():
org.registrant = org.admins.first()
@@ -12,15 +12,14 @@ def make_admin_registrant(apps, schema_editor):
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0113_contest_decimal_points'),
+ ("judge", "0113_contest_decimal_points"),
]
operations = [
migrations.RunPython(migrations.RunPython.noop, make_admin_registrant),
migrations.RemoveField(
- model_name='organization',
- name='registrant',
+ model_name="organization",
+ name="registrant",
),
]
diff --git a/judge/migrations/0115_contest_scoreboard_visibility.py b/judge/migrations/0115_contest_scoreboard_visibility.py
index fb3582a136..8bb0babd17 100644
--- a/judge/migrations/0115_contest_scoreboard_visibility.py
+++ b/judge/migrations/0115_contest_scoreboard_visibility.py
@@ -4,30 +4,43 @@
def hide_scoreboard_eq_true(apps, schema_editor):
- Contest = apps.get_model('judge', 'Contest')
- Contest.objects.filter(hide_scoreboard=True).update(scoreboard_visibility='C')
+ Contest = apps.get_model("judge", "Contest")
+ Contest.objects.filter(hide_scoreboard=True).update(scoreboard_visibility="C")
def scoreboard_visibility_eq_contest(apps, schema_editor):
- Contest = apps.get_model('judge', 'Contest')
- Contest.objects.filter(scoreboard_visibility__in=('C', 'P')).update(hide_scoreboard=True)
+ Contest = apps.get_model("judge", "Contest")
+ Contest.objects.filter(scoreboard_visibility__in=("C", "P")).update(
+ hide_scoreboard=True
+ )
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0114_remove_org_registrant'),
+ ("judge", "0114_remove_org_registrant"),
]
operations = [
migrations.AddField(
- model_name='contest',
- name='scoreboard_visibility',
- field=models.CharField(choices=[('V', 'Visible'), ('C', 'Hidden for duration of contest'), ('P', 'Hidden for duration of participation')], default='V', help_text='Scoreboard visibility through the duration of the contest', max_length=1, verbose_name='scoreboard visibility'),
+ model_name="contest",
+ name="scoreboard_visibility",
+ field=models.CharField(
+ choices=[
+ ("V", "Visible"),
+ ("C", "Hidden for duration of contest"),
+ ("P", "Hidden for duration of participation"),
+ ],
+ default="V",
+ help_text="Scoreboard visibility through the duration of the contest",
+ max_length=1,
+ verbose_name="scoreboard visibility",
+ ),
+ ),
+ migrations.RunPython(
+ hide_scoreboard_eq_true, scoreboard_visibility_eq_contest, atomic=True
),
- migrations.RunPython(hide_scoreboard_eq_true, scoreboard_visibility_eq_contest, atomic=True),
migrations.RemoveField(
- model_name='contest',
- name='hide_scoreboard',
+ model_name="contest",
+ name="hide_scoreboard",
),
]
diff --git a/judge/migrations/0116_contest_curator_and_tester.py b/judge/migrations/0116_contest_curator_and_tester.py
index 389e7dafee..0a866ef05f 100644
--- a/judge/migrations/0116_contest_curator_and_tester.py
+++ b/judge/migrations/0116_contest_curator_and_tester.py
@@ -4,30 +4,43 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0115_contest_scoreboard_visibility'),
+ ("judge", "0115_contest_scoreboard_visibility"),
]
operations = [
migrations.AlterField(
- model_name='contest',
- name='organizers',
- field=models.ManyToManyField(help_text='These users will be able to edit the contest.', related_name='_contest_authors_+', to='judge.Profile'),
+ model_name="contest",
+ name="organizers",
+ field=models.ManyToManyField(
+ help_text="These users will be able to edit the contest.",
+ related_name="_contest_authors_+",
+ to="judge.Profile",
+ ),
),
migrations.RenameField(
- model_name='contest',
- old_name='organizers',
- new_name='authors',
+ model_name="contest",
+ old_name="organizers",
+ new_name="authors",
),
migrations.AddField(
- model_name='contest',
- name='curators',
- field=models.ManyToManyField(blank=True, help_text='These users will be able to edit the contest, but will not be listed as authors.', related_name='_contest_curators_+', to='judge.Profile'),
+ model_name="contest",
+ name="curators",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="These users will be able to edit the contest, but will not be listed as authors.",
+ related_name="_contest_curators_+",
+ to="judge.Profile",
+ ),
),
migrations.AddField(
- model_name='contest',
- name='testers',
- field=models.ManyToManyField(blank=True, help_text='These users will be able to view the contest, but not edit it.', related_name='_contest_testers_+', to='judge.Profile'),
+ model_name="contest",
+ name="testers",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="These users will be able to view the contest, but not edit it.",
+ related_name="_contest_testers_+",
+ to="judge.Profile",
+ ),
),
]
diff --git a/judge/migrations/0117_remove_private_messages.py b/judge/migrations/0117_remove_private_messages.py
index 67456e40f3..a25aa554ec 100644
--- a/judge/migrations/0117_remove_private_messages.py
+++ b/judge/migrations/0117_remove_private_messages.py
@@ -4,20 +4,19 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0116_contest_curator_and_tester'),
+ ("judge", "0116_contest_curator_and_tester"),
]
operations = [
migrations.RemoveField(
- model_name='privatemessagethread',
- name='messages',
+ model_name="privatemessagethread",
+ name="messages",
),
migrations.DeleteModel(
- name='PrivateMessage',
+ name="PrivateMessage",
),
migrations.DeleteModel(
- name='PrivateMessageThread',
+ name="PrivateMessageThread",
),
]
diff --git a/judge/migrations/0118_convert_to_dates.py b/judge/migrations/0118_convert_to_dates.py
index 94bdc3d92e..0578c5b00c 100644
--- a/judge/migrations/0118_convert_to_dates.py
+++ b/judge/migrations/0118_convert_to_dates.py
@@ -6,17 +6,17 @@
def convert_to_datetime(apps, schema_editor):
- Contest = apps.get_model('judge', 'Contest')
- Submission = apps.get_model('judge', 'Submission')
+ Contest = apps.get_model("judge", "Contest")
+ Submission = apps.get_model("judge", "Submission")
- Submission.objects.filter(was_rejudged=True).update(rejudged_date=F('judged_date'))
+ Submission.objects.filter(was_rejudged=True).update(rejudged_date=F("judged_date"))
Submission.objects.filter(is_locked=True).update(locked_after=timezone.now())
- Contest.objects.filter(is_locked=True).update(locked_after=F('end_time'))
+ Contest.objects.filter(is_locked=True).update(locked_after=F("end_time"))
def convert_to_boolean(apps, schema_editor):
- Contest = apps.get_model('judge', 'Contest')
- Submission = apps.get_model('judge', 'Submission')
+ Contest = apps.get_model("judge", "Contest")
+ Submission = apps.get_model("judge", "Submission")
now = timezone.now()
@@ -26,38 +26,46 @@ def convert_to_boolean(apps, schema_editor):
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0117_remove_private_messages'),
+ ("judge", "0117_remove_private_messages"),
]
operations = [
migrations.AddField(
- model_name='contest',
- name='locked_after',
- field=models.DateTimeField(blank=True, help_text='Prevent submissions from this contest from being rejudged after this date.', null=True, verbose_name='contest lock'),
+ model_name="contest",
+ name="locked_after",
+ field=models.DateTimeField(
+ blank=True,
+ help_text="Prevent submissions from this contest from being rejudged after this date.",
+ null=True,
+ verbose_name="contest lock",
+ ),
),
migrations.AddField(
- model_name='submission',
- name='locked_after',
- field=models.DateTimeField(blank=True, null=True, verbose_name='submission lock'),
+ model_name="submission",
+ name="locked_after",
+ field=models.DateTimeField(
+ blank=True, null=True, verbose_name="submission lock"
+ ),
),
migrations.AddField(
- model_name='submission',
- name='rejudged_date',
- field=models.DateTimeField(blank=True, null=True, verbose_name='last rejudge date by admin'),
+ model_name="submission",
+ name="rejudged_date",
+ field=models.DateTimeField(
+ blank=True, null=True, verbose_name="last rejudge date by admin"
+ ),
),
migrations.RunPython(convert_to_datetime, convert_to_boolean, atomic=True),
migrations.RemoveField(
- model_name='contest',
- name='is_locked',
+ model_name="contest",
+ name="is_locked",
),
migrations.RemoveField(
- model_name='submission',
- name='is_locked',
+ model_name="submission",
+ name="is_locked",
),
migrations.RemoveField(
- model_name='submission',
- name='was_rejudged',
+ model_name="submission",
+ name="was_rejudged",
),
]
diff --git a/judge/migrations/0119_hide_problem_authors.py b/judge/migrations/0119_hide_problem_authors.py
index 31c2808040..9ee01da855 100644
--- a/judge/migrations/0119_hide_problem_authors.py
+++ b/judge/migrations/0119_hide_problem_authors.py
@@ -4,15 +4,18 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0118_convert_to_dates'),
+ ("judge", "0118_convert_to_dates"),
]
operations = [
migrations.AddField(
- model_name='contest',
- name='hide_problem_authors',
- field=models.BooleanField(default=False, help_text='Whether problem authors should be hidden by default.', verbose_name='hide problem authors'),
+ model_name="contest",
+ name="hide_problem_authors",
+ field=models.BooleanField(
+ default=False,
+ help_text="Whether problem authors should be hidden by default.",
+ verbose_name="hide problem authors",
+ ),
),
]
diff --git a/judge/migrations/0120_totp_no_reuse.py b/judge/migrations/0120_totp_no_reuse.py
index df2d4ba028..2b51de0961 100644
--- a/judge/migrations/0120_totp_no_reuse.py
+++ b/judge/migrations/0120_totp_no_reuse.py
@@ -4,15 +4,14 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0119_hide_problem_authors'),
+ ("judge", "0119_hide_problem_authors"),
]
operations = [
migrations.AddField(
- model_name='profile',
- name='last_totp_timecode',
- field=models.IntegerField(default=0, verbose_name='last TOTP timecode'),
+ model_name="profile",
+ name="last_totp_timecode",
+ field=models.IntegerField(default=0, verbose_name="last TOTP timecode"),
),
]
diff --git a/judge/migrations/0121_per_problem_sub_access_control.py b/judge/migrations/0121_per_problem_sub_access_control.py
index d57e6850fc..db63db1fdf 100644
--- a/judge/migrations/0121_per_problem_sub_access_control.py
+++ b/judge/migrations/0121_per_problem_sub_access_control.py
@@ -4,15 +4,24 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0120_totp_no_reuse'),
+ ("judge", "0120_totp_no_reuse"),
]
operations = [
migrations.AddField(
- model_name='problem',
- name='submission_source_visibility_mode',
- field=models.CharField(choices=[('F', 'Follow global setting'), ('A', 'Always visible'), ('S', 'Visible if problem solved'), ('O', 'Only own submissions')], default='F', max_length=1, verbose_name='submission source visibility'),
+ model_name="problem",
+ name="submission_source_visibility_mode",
+ field=models.CharField(
+ choices=[
+ ("F", "Follow global setting"),
+ ("A", "Always visible"),
+ ("S", "Visible if problem solved"),
+ ("O", "Only own submissions"),
+ ],
+ default="F",
+ max_length=1,
+ verbose_name="submission source visibility",
+ ),
),
]
diff --git a/judge/migrations/0122_username_display_override.py b/judge/migrations/0122_username_display_override.py
index c8650a4cfa..fcc7cc28a5 100644
--- a/judge/migrations/0122_username_display_override.py
+++ b/judge/migrations/0122_username_display_override.py
@@ -4,15 +4,19 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0121_per_problem_sub_access_control'),
+ ("judge", "0121_per_problem_sub_access_control"),
]
operations = [
migrations.AddField(
- model_name='profile',
- name='username_display_override',
- field=models.CharField(blank=True, max_length=100, verbose_name='display name override', help_text='name displayed in place of username'),
+ model_name="profile",
+ name="username_display_override",
+ field=models.CharField(
+ blank=True,
+ max_length=100,
+ verbose_name="display name override",
+ help_text="name displayed in place of username",
+ ),
),
]
diff --git a/judge/migrations/0123_contest_rating_elo_mmr.py b/judge/migrations/0123_contest_rating_elo_mmr.py
index e5957e63f1..fb4c6297cf 100644
--- a/judge/migrations/0123_contest_rating_elo_mmr.py
+++ b/judge/migrations/0123_contest_rating_elo_mmr.py
@@ -7,7 +7,7 @@
from django.utils import timezone
-def tie_ranker(iterable, key=attrgetter('points')):
+def tie_ranker(iterable, key=attrgetter("points")):
rank = 0
delta = 1
last = None
@@ -53,7 +53,9 @@ def WP(RA, RB, VA, VB):
return (math.erf((RB - RA) / math.sqrt(2 * (VA * VA + VB * VB))) + 1) / 2.0
-def recalculate_ratings(old_rating, old_volatility, actual_rank, times_rated, is_disqualified):
+def recalculate_ratings(
+ old_rating, old_volatility, actual_rank, times_rated, is_disqualified
+):
# actual_rank: 1 is first place, N is last place
# if there are ties, use the average of places (if places 2, 3, 4, 5 tie, use 3.5 for all of them)
@@ -74,7 +76,9 @@ def recalculate_ratings(old_rating, old_volatility, actual_rank, times_rated, is
for i in range(N):
ERank = 0.5
for j in range(N):
- ERank += WP(old_rating[i], old_rating[j], old_volatility[i], old_volatility[j])
+ ERank += WP(
+ old_rating[i], old_rating[j], old_volatility[i], old_volatility[j]
+ )
EPerf = -normal_CDF_inverse((ERank - 0.5) / N)
APerf = -normal_CDF_inverse((actual_rank[i] - 0.5) / N)
@@ -98,8 +102,10 @@ def recalculate_ratings(old_rating, old_volatility, actual_rank, times_rated, is
if times_rated[i] == 0:
new_volatility[i] = 385
else:
- new_volatility[i] = math.sqrt(((new_rating[i] - old_rating[i]) ** 2) / Weight +
- (old_volatility[i] ** 2) / (Weight + 1))
+ new_volatility[i] = math.sqrt(
+ ((new_rating[i] - old_rating[i]) ** 2) / Weight
+ + (old_volatility[i] ** 2) / (Weight + 1)
+ )
if is_disqualified[i]:
# DQed users can manipulate TopCoder ratings to get higher volatility in order to increase their rating
@@ -112,23 +118,49 @@ def recalculate_ratings(old_rating, old_volatility, actual_rank, times_rated, is
# inflate a little if we have to so people who placed first don't lose rating
best_rank = min(actual_rank)
for i in range(N):
- if abs(actual_rank[i] - best_rank) <= 1e-3 and new_rating[i] < old_rating[i] + 1:
+ if (
+ abs(actual_rank[i] - best_rank) <= 1e-3
+ and new_rating[i] < old_rating[i] + 1
+ ):
new_rating[i] = old_rating[i] + 1
- return list(map(int, map(round, new_rating))), list(map(int, map(round, new_volatility)))
+ return list(map(int, map(round, new_rating))), list(
+ map(int, map(round, new_volatility))
+ )
def tc_rate_contest(contest, Rating, Profile):
- rating_subquery = Rating.objects.filter(user=OuterRef('user'))
- rating_sorted = rating_subquery.order_by('-contest__end_time')
- users = contest.users.order_by('is_disqualified', '-score', 'cumtime', 'tiebreaker') \
- .annotate(submissions=Count('submission'),
- last_rating=Coalesce(Subquery(rating_sorted.values('rating')[:1]), 1200),
- volatility=Coalesce(Subquery(rating_sorted.values('volatility')[:1]), 535),
- times=Coalesce(Subquery(rating_subquery.order_by().values('user_id')
- .annotate(count=Count('id')).values('count')), 0)) \
- .exclude(user_id__in=contest.rate_exclude.all()) \
- .filter(virtual=0).values('id', 'user_id', 'score', 'cumtime', 'tiebreaker', 'is_disqualified',
- 'last_rating', 'volatility', 'times')
+ rating_subquery = Rating.objects.filter(user=OuterRef("user"))
+ rating_sorted = rating_subquery.order_by("-contest__end_time")
+ users = (
+ contest.users.order_by("is_disqualified", "-score", "cumtime", "tiebreaker")
+ .annotate(
+ submissions=Count("submission"),
+ last_rating=Coalesce(Subquery(rating_sorted.values("rating")[:1]), 1200),
+ volatility=Coalesce(Subquery(rating_sorted.values("volatility")[:1]), 535),
+ times=Coalesce(
+ Subquery(
+ rating_subquery.order_by()
+ .values("user_id")
+ .annotate(count=Count("id"))
+ .values("count")
+ ),
+ 0,
+ ),
+ )
+ .exclude(user_id__in=contest.rate_exclude.all())
+ .filter(virtual=0)
+ .values(
+ "id",
+ "user_id",
+ "score",
+ "cumtime",
+ "tiebreaker",
+ "is_disqualified",
+ "last_rating",
+ "volatility",
+ "times",
+ )
+ )
if not contest.rate_all:
users = users.filter(submissions__gt=0)
if contest.rating_floor is not None:
@@ -137,72 +169,93 @@ def tc_rate_contest(contest, Rating, Profile):
users = users.exclude(last_rating__gt=contest.rating_ceiling)
users = list(users)
- participation_ids = list(map(itemgetter('id'), users))
- user_ids = list(map(itemgetter('user_id'), users))
- is_disqualified = list(map(itemgetter('is_disqualified'), users))
- ranking = list(tie_ranker(users, key=itemgetter('score', 'cumtime', 'tiebreaker')))
- old_rating = list(map(itemgetter('last_rating'), users))
- old_volatility = list(map(itemgetter('volatility'), users))
- times_ranked = list(map(itemgetter('times'), users))
- rating, volatility = recalculate_ratings(old_rating, old_volatility, ranking, times_ranked, is_disqualified)
+ participation_ids = list(map(itemgetter("id"), users))
+ user_ids = list(map(itemgetter("user_id"), users))
+ is_disqualified = list(map(itemgetter("is_disqualified"), users))
+ ranking = list(tie_ranker(users, key=itemgetter("score", "cumtime", "tiebreaker")))
+ old_rating = list(map(itemgetter("last_rating"), users))
+ old_volatility = list(map(itemgetter("volatility"), users))
+ times_ranked = list(map(itemgetter("times"), users))
+ rating, volatility = recalculate_ratings(
+ old_rating, old_volatility, ranking, times_ranked, is_disqualified
+ )
now = timezone.now()
- ratings = [Rating(user_id=i, contest=contest, rating=r, volatility=v, last_rated=now, participation_id=p, rank=z)
- for i, p, r, v, z in zip(user_ids, participation_ids, rating, volatility, ranking)]
+ ratings = [
+ Rating(
+ user_id=i,
+ contest=contest,
+ rating=r,
+ volatility=v,
+ last_rated=now,
+ participation_id=p,
+ rank=z,
+ )
+ for i, p, r, v, z in zip(
+ user_ids, participation_ids, rating, volatility, ranking
+ )
+ ]
Rating.objects.bulk_create(ratings)
- Profile.objects.filter(contest_history__contest=contest, contest_history__virtual=0).update(
- rating=Subquery(Rating.objects.filter(user=OuterRef('id'))
- .order_by('-contest__end_time').values('rating')[:1]))
+ Profile.objects.filter(
+ contest_history__contest=contest, contest_history__virtual=0
+ ).update(
+ rating=Subquery(
+ Rating.objects.filter(user=OuterRef("id"))
+ .order_by("-contest__end_time")
+ .values("rating")[:1]
+ )
+ )
# inspired by rate_all_view
def rate_tc(apps, schema_editor):
- Contest = apps.get_model('judge', 'Contest')
- Rating = apps.get_model('judge', 'Rating')
- Profile = apps.get_model('judge', 'Profile')
+ Contest = apps.get_model("judge", "Contest")
+ Rating = apps.get_model("judge", "Rating")
+ Profile = apps.get_model("judge", "Profile")
with schema_editor.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"):
tc_rate_contest(contest, Rating, Profile)
# inspired by rate_all_view
def rate_elo_mmr(apps, schema_editor):
- Rating = apps.get_model('judge', 'Rating')
- Profile = apps.get_model('judge', 'Profile')
+ Rating = apps.get_model("judge", "Rating")
+ Profile = apps.get_model("judge", "Profile")
with schema_editor.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)
# Don't populate Rating
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0122_username_display_override'),
+ ("judge", "0122_username_display_override"),
]
operations = [
migrations.RunPython(migrations.RunPython.noop, rate_tc, atomic=True),
migrations.AddField(
- model_name='rating',
- name='mean',
- field=models.FloatField(verbose_name='raw rating'),
+ model_name="rating",
+ name="mean",
+ field=models.FloatField(verbose_name="raw rating"),
),
migrations.AddField(
- model_name='rating',
- name='performance',
- field=models.FloatField(verbose_name='contest performance'),
+ model_name="rating",
+ name="performance",
+ field=models.FloatField(verbose_name="contest performance"),
),
migrations.RemoveField(
- model_name='rating',
- name='volatility',
- field=models.IntegerField(verbose_name='volatility'),
+ model_name="rating",
+ name="volatility",
+ field=models.IntegerField(verbose_name="volatility"),
),
migrations.RunPython(rate_elo_mmr, migrations.RunPython.noop, atomic=True),
]
diff --git a/judge/migrations/0124_contest_show_short_display.py b/judge/migrations/0124_contest_show_short_display.py
index ac805fee38..6e8cca474d 100644
--- a/judge/migrations/0124_contest_show_short_display.py
+++ b/judge/migrations/0124_contest_show_short_display.py
@@ -4,15 +4,18 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0123_contest_rating_elo_mmr'),
+ ("judge", "0123_contest_rating_elo_mmr"),
]
operations = [
migrations.AddField(
- model_name='contest',
- name='show_short_display',
- field=models.BooleanField(default=False, help_text='Whether to show a section containing contest settings on the contest page or not.', verbose_name='show short form settings display'),
+ model_name="contest",
+ name="show_short_display",
+ field=models.BooleanField(
+ default=False,
+ help_text="Whether to show a section containing contest settings on the contest page or not.",
+ verbose_name="show short form settings display",
+ ),
),
]
diff --git a/judge/migrations/0125_organization_classes.py b/judge/migrations/0125_organization_classes.py
index 66bcc3692d..5d2bfdca5d 100644
--- a/judge/migrations/0125_organization_classes.py
+++ b/judge/migrations/0125_organization_classes.py
@@ -5,48 +5,128 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0124_contest_show_short_display'),
+ ("judge", "0124_contest_show_short_display"),
]
operations = [
migrations.AddField(
- model_name='organization',
- name='class_required',
- field=models.BooleanField(default=False, help_text='whether members are compelled to select a class when joining', verbose_name='class membership required'),
+ model_name="organization",
+ name="class_required",
+ field=models.BooleanField(
+ default=False,
+ help_text="whether members are compelled to select a class when joining",
+ verbose_name="class membership required",
+ ),
),
migrations.CreateModel(
- name='Class',
+ name="Class",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('name', models.CharField(max_length=128, unique=True, verbose_name='class name')),
- ('slug', models.SlugField(help_text='class name shown in URLs', max_length=128, verbose_name='class slug')),
- ('description', models.TextField(blank=True, verbose_name='class description')),
- ('is_active', models.BooleanField(default=True, verbose_name='is class active')),
- ('access_code', models.CharField(blank=True, help_text='student access code', max_length=7, null=True, verbose_name='access code')),
- ('admins', models.ManyToManyField(help_text='those who can approve membership to this class', related_name='class_admin_of', to='judge.Profile', verbose_name='administrators')),
- ('members', models.ManyToManyField(blank=True, related_name='classes', related_query_name='class', to='judge.Profile', verbose_name='members')),
- ('organization', models.ForeignKey(help_text='the organization that this class belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='classes', related_query_name='class', to='judge.Organization', verbose_name='organization')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "name",
+ models.CharField(
+ max_length=128, unique=True, verbose_name="class name"
+ ),
+ ),
+ (
+ "slug",
+ models.SlugField(
+ help_text="class name shown in URLs",
+ max_length=128,
+ verbose_name="class slug",
+ ),
+ ),
+ (
+ "description",
+ models.TextField(blank=True, verbose_name="class description"),
+ ),
+ (
+ "is_active",
+ models.BooleanField(default=True, verbose_name="is class active"),
+ ),
+ (
+ "access_code",
+ models.CharField(
+ blank=True,
+ help_text="student access code",
+ max_length=7,
+ null=True,
+ verbose_name="access code",
+ ),
+ ),
+ (
+ "admins",
+ models.ManyToManyField(
+ help_text="those who can approve membership to this class",
+ related_name="class_admin_of",
+ to="judge.Profile",
+ verbose_name="administrators",
+ ),
+ ),
+ (
+ "members",
+ models.ManyToManyField(
+ blank=True,
+ related_name="classes",
+ related_query_name="class",
+ to="judge.Profile",
+ verbose_name="members",
+ ),
+ ),
+ (
+ "organization",
+ models.ForeignKey(
+ help_text="the organization that this class belongs to",
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="classes",
+ related_query_name="class",
+ to="judge.Organization",
+ verbose_name="organization",
+ ),
+ ),
],
options={
- 'verbose_name': 'class',
- 'verbose_name_plural': 'classes',
- 'ordering': ['organization', 'name'],
+ "verbose_name": "class",
+ "verbose_name_plural": "classes",
+ "ordering": ["organization", "name"],
},
),
migrations.AddField(
- model_name='contest',
- name='classes',
- field=models.ManyToManyField(blank=True, help_text='If organization private, only these classes may see the contest', to='judge.Class', verbose_name='classes'),
+ model_name="contest",
+ name="classes",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="If organization private, only these classes may see the contest",
+ to="judge.Class",
+ verbose_name="classes",
+ ),
),
migrations.AddConstraint(
- model_name='class',
- constraint=models.UniqueConstraint(condition=models.Q(is_active=True), fields=('name',), name='unique_active_name'),
+ model_name="class",
+ constraint=models.UniqueConstraint(
+ condition=models.Q(is_active=True),
+ fields=("name",),
+ name="unique_active_name",
+ ),
),
migrations.AddField(
- model_name='organizationrequest',
- name='request_class',
- field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='judge.Class', verbose_name='class'),
+ model_name="organizationrequest",
+ name="request_class",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ to="judge.Class",
+ verbose_name="class",
+ ),
),
]
diff --git a/judge/migrations/0126_infer_private_bools.py b/judge/migrations/0126_infer_private_bools.py
index 163dfea154..fc7474b28c 100644
--- a/judge/migrations/0126_infer_private_bools.py
+++ b/judge/migrations/0126_infer_private_bools.py
@@ -4,8 +4,8 @@
def sync_private_booleans(apps, schema_editor):
- Problem = apps.get_model('judge', 'Problem')
- Contest = apps.get_model('judge', 'Contest')
+ Problem = apps.get_model("judge", "Problem")
+ Contest = apps.get_model("judge", "Contest")
db_alias = schema_editor.connection.alias
for problem in Problem.objects.using(db_alias).all():
@@ -29,9 +29,8 @@ def sync_private_booleans(apps, schema_editor):
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0125_organization_classes'),
+ ("judge", "0125_organization_classes"),
]
operations = [
diff --git a/judge/migrations/0127_tester_see_scoreboard.py b/judge/migrations/0127_tester_see_scoreboard.py
index 9346fa9db7..4678e53452 100644
--- a/judge/migrations/0127_tester_see_scoreboard.py
+++ b/judge/migrations/0127_tester_see_scoreboard.py
@@ -4,15 +4,18 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0126_infer_private_bools'),
+ ("judge", "0126_infer_private_bools"),
]
operations = [
migrations.AddField(
- model_name='contest',
- name='tester_see_scoreboard',
- field=models.BooleanField(default=False, help_text='If testers can see the scoreboard.', verbose_name='testers see scoreboard'),
+ model_name="contest",
+ name="tester_see_scoreboard",
+ field=models.BooleanField(
+ default=False,
+ help_text="If testers can see the scoreboard.",
+ verbose_name="testers see scoreboard",
+ ),
),
]
diff --git a/judge/migrations/0128_limit_join_organizations.py b/judge/migrations/0128_limit_join_organizations.py
index d346802169..ba9ad5090d 100644
--- a/judge/migrations/0128_limit_join_organizations.py
+++ b/judge/migrations/0128_limit_join_organizations.py
@@ -4,20 +4,27 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0127_tester_see_scoreboard'),
+ ("judge", "0127_tester_see_scoreboard"),
]
operations = [
migrations.AddField(
- model_name='contest',
- name='join_organizations',
- field=models.ManyToManyField(blank=True, help_text='If non-empty, only these organizations may join the contest', related_name='join_only_contests', to='judge.Organization', verbose_name='join organizations'),
+ model_name="contest",
+ name="join_organizations",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="If non-empty, only these organizations may join the contest",
+ related_name="join_only_contests",
+ to="judge.Organization",
+ verbose_name="join organizations",
+ ),
),
migrations.AddField(
- model_name='contest',
- name='limit_join_organizations',
- field=models.BooleanField(default=False, verbose_name='limit organizations that can join'),
+ model_name="contest",
+ name="limit_join_organizations",
+ field=models.BooleanField(
+ default=False, verbose_name="limit organizations that can join"
+ ),
),
]
diff --git a/judge/migrations/0129_see_scoreboard_subs.py b/judge/migrations/0129_see_scoreboard_subs.py
index 4418f3d021..9ca03b9d43 100644
--- a/judge/migrations/0129_see_scoreboard_subs.py
+++ b/judge/migrations/0129_see_scoreboard_subs.py
@@ -4,20 +4,29 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0128_limit_join_organizations'),
+ ("judge", "0128_limit_join_organizations"),
]
operations = [
migrations.AddField(
- model_name='contest',
- name='tester_see_submissions',
- field=models.BooleanField(default=False, help_text='If testers can see in-contest submissions.', verbose_name='testers see submissions'),
+ model_name="contest",
+ name="tester_see_submissions",
+ field=models.BooleanField(
+ default=False,
+ help_text="If testers can see in-contest submissions.",
+ verbose_name="testers see submissions",
+ ),
),
migrations.AddField(
- model_name='contest',
- name='view_contest_submissions',
- field=models.ManyToManyField(blank=True, help_text='These users will be able to see in-contest submissions.', related_name='view_contest_submissions', to='judge.Profile', verbose_name='can see contest submissions'),
+ model_name="contest",
+ name="view_contest_submissions",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="These users will be able to see in-contest submissions.",
+ related_name="view_contest_submissions",
+ to="judge.Profile",
+ verbose_name="can see contest submissions",
+ ),
),
]
diff --git a/judge/migrations/0130_blogpost_change_visibility.py b/judge/migrations/0130_blogpost_change_visibility.py
index a90123c429..db4c250fe0 100644
--- a/judge/migrations/0130_blogpost_change_visibility.py
+++ b/judge/migrations/0130_blogpost_change_visibility.py
@@ -4,14 +4,20 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0129_see_scoreboard_subs'),
+ ("judge", "0129_see_scoreboard_subs"),
]
operations = [
migrations.AlterModelOptions(
- name='blogpost',
- options={'permissions': (('edit_all_post', 'Edit all posts'), ('change_post_visibility', 'Edit post visibility')), 'verbose_name': 'blog post', 'verbose_name_plural': 'blog posts'},
+ name="blogpost",
+ options={
+ "permissions": (
+ ("edit_all_post", "Edit all posts"),
+ ("change_post_visibility", "Edit post visibility"),
+ ),
+ "verbose_name": "blog post",
+ "verbose_name_plural": "blog posts",
+ },
),
]
diff --git a/judge/migrations/0131_spectate_contests.py b/judge/migrations/0131_spectate_contests.py
index 6cfc95354b..675165b93d 100644
--- a/judge/migrations/0131_spectate_contests.py
+++ b/judge/migrations/0131_spectate_contests.py
@@ -4,30 +4,48 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0130_blogpost_change_visibility'),
+ ("judge", "0130_blogpost_change_visibility"),
]
operations = [
migrations.AddField(
- model_name='contest',
- name='spectators',
- field=models.ManyToManyField(blank=True, help_text='These users will be able to spectate the contest, but not see the problems ahead of time.', related_name='spectated_contests', to='judge.Profile'),
+ model_name="contest",
+ name="spectators",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="These users will be able to spectate the contest, but not see the problems ahead of time.",
+ related_name="spectated_contests",
+ to="judge.Profile",
+ ),
),
migrations.AlterField(
- model_name='contest',
- name='authors',
- field=models.ManyToManyField(help_text='These users will be able to edit the contest.', related_name='authored_contests', to='judge.Profile'),
+ model_name="contest",
+ name="authors",
+ field=models.ManyToManyField(
+ help_text="These users will be able to edit the contest.",
+ related_name="authored_contests",
+ to="judge.Profile",
+ ),
),
migrations.AlterField(
- model_name='contest',
- name='curators',
- field=models.ManyToManyField(blank=True, help_text='These users will be able to edit the contest, but will not be listed as authors.', related_name='curated_contests', to='judge.Profile'),
+ model_name="contest",
+ name="curators",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="These users will be able to edit the contest, but will not be listed as authors.",
+ related_name="curated_contests",
+ to="judge.Profile",
+ ),
),
migrations.AlterField(
- model_name='contest',
- name='testers',
- field=models.ManyToManyField(blank=True, help_text='These users will be able to view the contest, but not edit it.', related_name='tested_contests', to='judge.Profile'),
+ model_name="contest",
+ name="testers",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="These users will be able to view the contest, but not edit it.",
+ related_name="tested_contests",
+ to="judge.Profile",
+ ),
),
]
diff --git a/judge/migrations/0132_no_self_vote.py b/judge/migrations/0132_no_self_vote.py
index 078d63a10b..c57f33a995 100644
--- a/judge/migrations/0132_no_self_vote.py
+++ b/judge/migrations/0132_no_self_vote.py
@@ -4,20 +4,21 @@
def delete_self_votes(apps, schema_editor):
- Comment = apps.get_model('judge', 'Comment')
- CommentVote = apps.get_model('judge', 'CommentVote')
+ Comment = apps.get_model("judge", "Comment")
+ CommentVote = apps.get_model("judge", "CommentVote")
- CommentVote.objects.filter(voter=F('comment__author')).delete()
+ CommentVote.objects.filter(voter=F("comment__author")).delete()
- votes = CommentVote.objects.filter(comment=OuterRef('id')).order_by().values('comment')
- total_votes = votes.annotate(total=Sum('score')).values('total')
+ votes = (
+ CommentVote.objects.filter(comment=OuterRef("id")).order_by().values("comment")
+ )
+ total_votes = votes.annotate(total=Sum("score")).values("total")
Comment.objects.update(score=Coalesce(Subquery(total_votes), 0))
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0131_spectate_contests'),
+ ("judge", "0131_spectate_contests"),
]
operations = [
diff --git a/judge/migrations/0133_add_problem_data_hints.py b/judge/migrations/0133_add_problem_data_hints.py
index 39d2ed0c3c..df98408d51 100644
--- a/judge/migrations/0133_add_problem_data_hints.py
+++ b/judge/migrations/0133_add_problem_data_hints.py
@@ -10,453 +10,1604 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0132_no_self_vote'),
+ ("judge", "0132_no_self_vote"),
]
operations = [
migrations.AlterModelOptions(
- name='commentlock',
- options={'permissions': (('override_comment_lock', 'Override comment lock'),), 'verbose_name': 'comment lock', 'verbose_name_plural': 'comment locks'},
+ name="commentlock",
+ options={
+ "permissions": (("override_comment_lock", "Override comment lock"),),
+ "verbose_name": "comment lock",
+ "verbose_name_plural": "comment locks",
+ },
),
migrations.AlterModelOptions(
- name='contestproblem',
- options={'ordering': ('order',), 'verbose_name': 'contest problem', 'verbose_name_plural': 'contest problems'},
+ name="contestproblem",
+ options={
+ "ordering": ("order",),
+ "verbose_name": "contest problem",
+ "verbose_name_plural": "contest problems",
+ },
),
migrations.AlterModelOptions(
- name='problemclarification',
- options={'verbose_name': 'problem clarification', 'verbose_name_plural': 'problem clarifications'},
+ name="problemclarification",
+ options={
+ "verbose_name": "problem clarification",
+ "verbose_name_plural": "problem clarifications",
+ },
),
migrations.AlterModelOptions(
- name='submissionsource',
- options={'verbose_name': 'submission source', 'verbose_name_plural': 'submission sources'},
+ name="submissionsource",
+ options={
+ "verbose_name": "submission source",
+ "verbose_name_plural": "submission sources",
+ },
),
migrations.AlterModelOptions(
- name='ticket',
- options={'verbose_name': 'ticket', 'verbose_name_plural': 'tickets'},
+ name="ticket",
+ options={"verbose_name": "ticket", "verbose_name_plural": "tickets"},
),
migrations.AlterModelOptions(
- name='ticketmessage',
- options={'verbose_name': 'ticket message', 'verbose_name_plural': 'ticket messages'},
+ name="ticketmessage",
+ options={
+ "verbose_name": "ticket message",
+ "verbose_name_plural": "ticket messages",
+ },
),
migrations.AlterModelOptions(
- name='webauthncredential',
- options={'verbose_name': 'WebAuthn credential', 'verbose_name_plural': 'WebAuthn credentials'},
- ),
- migrations.AlterField(
- model_name='blogpost',
- name='og_image',
- field=models.CharField(blank=True, default='', max_length=150, verbose_name='OpenGraph image'),
- ),
- migrations.AlterField(
- model_name='class',
- name='access_code',
- field=models.CharField(blank=True, help_text='Student access code.', max_length=7, null=True, verbose_name='access code'),
- ),
- migrations.AlterField(
- model_name='class',
- name='admins',
- field=models.ManyToManyField(help_text='Those who can approve membership to this class.', related_name='class_admin_of', to='judge.Profile', verbose_name='administrators'),
- ),
- migrations.AlterField(
- model_name='class',
- name='organization',
- field=models.ForeignKey(help_text='The organization that this class belongs to.', on_delete=django.db.models.deletion.CASCADE, related_name='classes', related_query_name='class', to='judge.organization', verbose_name='organization'),
- ),
- migrations.AlterField(
- model_name='class',
- name='slug',
- field=models.SlugField(help_text='Class name shown in URLs.', max_length=128, verbose_name='class slug'),
- ),
- migrations.AlterField(
- model_name='comment',
- name='hidden',
- field=models.BooleanField(default=0, verbose_name='hidden'),
- ),
- migrations.AlterField(
- model_name='comment',
- name='level',
+ name="webauthncredential",
+ options={
+ "verbose_name": "WebAuthn credential",
+ "verbose_name_plural": "WebAuthn credentials",
+ },
+ ),
+ migrations.AlterField(
+ model_name="blogpost",
+ name="og_image",
+ field=models.CharField(
+ blank=True, default="", max_length=150, verbose_name="OpenGraph image"
+ ),
+ ),
+ migrations.AlterField(
+ model_name="class",
+ name="access_code",
+ field=models.CharField(
+ blank=True,
+ help_text="Student access code.",
+ max_length=7,
+ null=True,
+ verbose_name="access code",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="class",
+ name="admins",
+ field=models.ManyToManyField(
+ help_text="Those who can approve membership to this class.",
+ related_name="class_admin_of",
+ to="judge.Profile",
+ verbose_name="administrators",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="class",
+ name="organization",
+ field=models.ForeignKey(
+ help_text="The organization that this class belongs to.",
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="classes",
+ related_query_name="class",
+ to="judge.organization",
+ verbose_name="organization",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="class",
+ name="slug",
+ field=models.SlugField(
+ help_text="Class name shown in URLs.",
+ max_length=128,
+ verbose_name="class slug",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="comment",
+ name="hidden",
+ field=models.BooleanField(default=0, verbose_name="hidden"),
+ ),
+ migrations.AlterField(
+ model_name="comment",
+ name="level",
field=models.PositiveIntegerField(editable=False),
),
migrations.AlterField(
- model_name='comment',
- name='lft',
+ model_name="comment",
+ name="lft",
field=models.PositiveIntegerField(editable=False),
),
migrations.AlterField(
- model_name='comment',
- name='rght',
+ model_name="comment",
+ name="rght",
field=models.PositiveIntegerField(editable=False),
),
migrations.AlterField(
- model_name='contest',
- name='authors',
- field=models.ManyToManyField(help_text='These users will be able to edit the contest.', related_name='authored_contests', to='judge.Profile', verbose_name='authors'),
- ),
- migrations.AlterField(
- model_name='contest',
- name='classes',
- field=models.ManyToManyField(blank=True, help_text='If organization private, only these classes may see the contest.', to='judge.Class', verbose_name='classes'),
- ),
- migrations.AlterField(
- model_name='contest',
- name='curators',
- field=models.ManyToManyField(blank=True, help_text='These users will be able to edit the contest, but will not be listed as authors.', related_name='curated_contests', to='judge.Profile', verbose_name='curators'),
- ),
- migrations.AlterField(
- model_name='contest',
- name='format_name',
- field=models.CharField(choices=[('atcoder', 'AtCoder'), ('default', 'Default'), ('ecoo', 'ECOO'), ('icpc', 'ICPC'), ('ioi', 'IOI (pre-2016)'), ('ioi16', 'IOI')], default='default', help_text='The contest format module to use.', max_length=32, verbose_name='contest format'),
- ),
- migrations.AlterField(
- model_name='contest',
- name='join_organizations',
- field=models.ManyToManyField(blank=True, help_text='If non-empty, only these organizations may join the contest.', related_name='join_only_contests', to='judge.Organization', verbose_name='join organizations'),
- ),
- migrations.AlterField(
- model_name='contest',
- name='logo_override_image',
- field=models.CharField(blank=True, default='', help_text='This image will replace the default site logo for users inside the contest.', max_length=150, verbose_name='logo override image'),
- ),
- migrations.AlterField(
- model_name='contest',
- name='organizations',
- field=models.ManyToManyField(blank=True, help_text='If non-empty, only these organizations may see the contest.', to='judge.Organization', verbose_name='organizations'),
- ),
- migrations.AlterField(
- model_name='contest',
- name='private_contestants',
- field=models.ManyToManyField(blank=True, help_text='If non-empty, only these users may see the contest.', related_name='_judge_contest_private_contestants_+', to='judge.Profile', verbose_name='private contestants'),
- ),
- migrations.AlterField(
- model_name='contest',
- name='rating_ceiling',
- field=models.IntegerField(blank=True, help_text='Do not rate users who have a higher rating.', null=True, verbose_name='rating ceiling'),
- ),
- migrations.AlterField(
- model_name='contest',
- name='rating_floor',
- field=models.IntegerField(blank=True, help_text='Do not rate users who have a lower rating.', null=True, verbose_name='rating floor'),
- ),
- migrations.AlterField(
- model_name='contest',
- name='scoreboard_visibility',
- field=models.CharField(choices=[('V', 'Visible'), ('C', 'Hidden for duration of contest'), ('P', 'Hidden for duration of participation'), ('H', 'Hidden permanently')], default='V', help_text='Scoreboard visibility through the duration of the contest.', max_length=1, verbose_name='scoreboard visibility'),
- ),
- migrations.AlterField(
- model_name='contest',
- name='spectators',
- field=models.ManyToManyField(blank=True, help_text='These users will be able to spectate the contest, but not see the problems ahead of time.', related_name='spectated_contests', to='judge.Profile', verbose_name='spectators'),
- ),
- migrations.AlterField(
- model_name='contest',
- name='testers',
- field=models.ManyToManyField(blank=True, help_text='These users will be able to view the contest, but not edit it.', related_name='tested_contests', to='judge.Profile', verbose_name='testers'),
- ),
- migrations.AlterField(
- model_name='contestproblem',
- name='max_submissions',
- field=models.IntegerField(blank=True, default=None, help_text='Maximum number of submissions for this problem, or leave blank for no limit.', null=True, validators=[judge.models.contest.MinValueOrNoneValidator(1, "Why include a problem you can't submit to?")], verbose_name='max submissions'),
- ),
- migrations.AlterField(
- model_name='judge',
- name='auth_key',
- field=models.CharField(help_text='A key to authenticate this judge.', max_length=100, verbose_name='authentication key'),
- ),
- migrations.AlterField(
- model_name='judge',
- name='last_ip',
- field=models.GenericIPAddressField(blank=True, null=True, verbose_name='last connected IP'),
- ),
- migrations.AlterField(
- model_name='judge',
- name='name',
- field=models.CharField(help_text='Server name, hostname-style.', max_length=50, unique=True, verbose_name='judge name'),
- ),
- migrations.AlterField(
- model_name='language',
- name='common_name',
- field=models.CharField(help_text='Common name for the language. For example, the common name for C++03, C++11, and C++14 would be "C++".', max_length=10, verbose_name='common name'),
- ),
- migrations.AlterField(
- model_name='language',
- name='description',
- field=models.TextField(blank=True, help_text='Use this field to inform users of quirks with your environment, additional restrictions, etc.', verbose_name='language description'),
- ),
- migrations.AlterField(
- model_name='languagelimit',
- name='memory_limit',
- field=models.IntegerField(validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1048576)], verbose_name='memory limit'),
- ),
- migrations.AlterField(
- model_name='languagelimit',
- name='time_limit',
- field=models.FloatField(validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(60)], verbose_name='time limit'),
- ),
- migrations.AlterField(
- model_name='license',
- name='display',
- field=models.CharField(blank=True, help_text='Displayed on pages under this license.', max_length=256, verbose_name='short name'),
- ),
- migrations.AlterField(
- model_name='license',
- name='icon',
- field=models.CharField(blank=True, help_text='URL to the icon.', max_length=256, verbose_name='icon'),
- ),
- migrations.AlterField(
- model_name='miscconfig',
- name='key',
- field=models.CharField(db_index=True, max_length=30, verbose_name='key'),
- ),
- migrations.AlterField(
- model_name='miscconfig',
- name='value',
- field=models.TextField(blank=True, verbose_name='value'),
- ),
- migrations.AlterField(
- model_name='navigationbar',
- name='level',
+ model_name="contest",
+ name="authors",
+ field=models.ManyToManyField(
+ help_text="These users will be able to edit the contest.",
+ related_name="authored_contests",
+ to="judge.Profile",
+ verbose_name="authors",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="contest",
+ name="classes",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="If organization private, only these classes may see the contest.",
+ to="judge.Class",
+ verbose_name="classes",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="contest",
+ name="curators",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="These users will be able to edit the contest, but will not be listed as authors.",
+ related_name="curated_contests",
+ to="judge.Profile",
+ verbose_name="curators",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="contest",
+ name="format_name",
+ field=models.CharField(
+ choices=[
+ ("atcoder", "AtCoder"),
+ ("default", "Default"),
+ ("ecoo", "ECOO"),
+ ("icpc", "ICPC"),
+ ("ioi", "IOI (pre-2016)"),
+ ("ioi16", "IOI"),
+ ],
+ default="default",
+ help_text="The contest format module to use.",
+ max_length=32,
+ verbose_name="contest format",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="contest",
+ name="join_organizations",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="If non-empty, only these organizations may join the contest.",
+ related_name="join_only_contests",
+ to="judge.Organization",
+ verbose_name="join organizations",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="contest",
+ name="logo_override_image",
+ field=models.CharField(
+ blank=True,
+ default="",
+ help_text="This image will replace the default site logo for users inside the contest.",
+ max_length=150,
+ verbose_name="logo override image",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="contest",
+ name="organizations",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="If non-empty, only these organizations may see the contest.",
+ to="judge.Organization",
+ verbose_name="organizations",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="contest",
+ name="private_contestants",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="If non-empty, only these users may see the contest.",
+ related_name="_judge_contest_private_contestants_+",
+ to="judge.Profile",
+ verbose_name="private contestants",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="contest",
+ name="rating_ceiling",
+ field=models.IntegerField(
+ blank=True,
+ help_text="Do not rate users who have a higher rating.",
+ null=True,
+ verbose_name="rating ceiling",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="contest",
+ name="rating_floor",
+ field=models.IntegerField(
+ blank=True,
+ help_text="Do not rate users who have a lower rating.",
+ null=True,
+ verbose_name="rating floor",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="contest",
+ name="scoreboard_visibility",
+ field=models.CharField(
+ choices=[
+ ("V", "Visible"),
+ ("C", "Hidden for duration of contest"),
+ ("P", "Hidden for duration of participation"),
+ ("H", "Hidden permanently"),
+ ],
+ default="V",
+ help_text="Scoreboard visibility through the duration of the contest.",
+ max_length=1,
+ verbose_name="scoreboard visibility",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="contest",
+ name="spectators",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="These users will be able to spectate the contest, but not see the problems ahead of time.",
+ related_name="spectated_contests",
+ to="judge.Profile",
+ verbose_name="spectators",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="contest",
+ name="testers",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="These users will be able to view the contest, but not edit it.",
+ related_name="tested_contests",
+ to="judge.Profile",
+ verbose_name="testers",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="contestproblem",
+ name="max_submissions",
+ field=models.IntegerField(
+ blank=True,
+ default=None,
+ help_text="Maximum number of submissions for this problem, or leave blank for no limit.",
+ null=True,
+ validators=[
+ judge.models.contest.MinValueOrNoneValidator(
+ 1, "Why include a problem you can't submit to?"
+ )
+ ],
+ verbose_name="max submissions",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="judge",
+ name="auth_key",
+ field=models.CharField(
+ help_text="A key to authenticate this judge.",
+ max_length=100,
+ verbose_name="authentication key",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="judge",
+ name="last_ip",
+ field=models.GenericIPAddressField(
+ blank=True, null=True, verbose_name="last connected IP"
+ ),
+ ),
+ migrations.AlterField(
+ model_name="judge",
+ name="name",
+ field=models.CharField(
+ help_text="Server name, hostname-style.",
+ max_length=50,
+ unique=True,
+ verbose_name="judge name",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="language",
+ name="common_name",
+ field=models.CharField(
+ help_text='Common name for the language. For example, the common name for C++03, C++11, and C++14 would be "C++".',
+ max_length=10,
+ verbose_name="common name",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="language",
+ name="description",
+ field=models.TextField(
+ blank=True,
+ help_text="Use this field to inform users of quirks with your environment, additional restrictions, etc.",
+ verbose_name="language description",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="languagelimit",
+ name="memory_limit",
+ field=models.IntegerField(
+ validators=[
+ django.core.validators.MinValueValidator(0),
+ django.core.validators.MaxValueValidator(1048576),
+ ],
+ verbose_name="memory limit",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="languagelimit",
+ name="time_limit",
+ field=models.FloatField(
+ validators=[
+ django.core.validators.MinValueValidator(0),
+ django.core.validators.MaxValueValidator(60),
+ ],
+ verbose_name="time limit",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="license",
+ name="display",
+ field=models.CharField(
+ blank=True,
+ help_text="Displayed on pages under this license.",
+ max_length=256,
+ verbose_name="short name",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="license",
+ name="icon",
+ field=models.CharField(
+ blank=True,
+ help_text="URL to the icon.",
+ max_length=256,
+ verbose_name="icon",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="miscconfig",
+ name="key",
+ field=models.CharField(db_index=True, max_length=30, verbose_name="key"),
+ ),
+ migrations.AlterField(
+ model_name="miscconfig",
+ name="value",
+ field=models.TextField(blank=True, verbose_name="value"),
+ ),
+ migrations.AlterField(
+ model_name="navigationbar",
+ name="level",
field=models.PositiveIntegerField(editable=False),
),
migrations.AlterField(
- model_name='navigationbar',
- name='lft',
+ model_name="navigationbar",
+ name="lft",
field=models.PositiveIntegerField(editable=False),
),
migrations.AlterField(
- model_name='navigationbar',
- name='rght',
+ model_name="navigationbar",
+ name="rght",
field=models.PositiveIntegerField(editable=False),
),
migrations.AlterField(
- model_name='organization',
- name='access_code',
- field=models.CharField(blank=True, help_text='Student access code.', max_length=7, null=True, verbose_name='access code'),
- ),
- migrations.AlterField(
- model_name='organization',
- name='admins',
- field=models.ManyToManyField(help_text='Those who can edit this organization.', related_name='admin_of', to='judge.Profile', verbose_name='administrators'),
- ),
- migrations.AlterField(
- model_name='organization',
- name='class_required',
- field=models.BooleanField(default=False, help_text='Whether members are compelled to select a class when joining.', verbose_name='class membership required'),
- ),
- migrations.AlterField(
- model_name='organization',
- name='is_open',
- field=models.BooleanField(default=True, help_text='Allow joining organization.', verbose_name='is open organization?'),
- ),
- migrations.AlterField(
- model_name='organization',
- name='logo_override_image',
- field=models.CharField(blank=True, default='', help_text='This image will replace the default site logo for users viewing the organization.', max_length=150, verbose_name='logo override image'),
- ),
- migrations.AlterField(
- model_name='organization',
- name='short_name',
- field=models.CharField(help_text='Displayed beside user name during contests.', max_length=20, verbose_name='short name'),
- ),
- migrations.AlterField(
- model_name='organization',
- name='slots',
- field=models.IntegerField(blank=True, help_text='Maximum amount of users in this organization, only applicable to private organizations.', null=True, verbose_name='maximum size'),
- ),
- migrations.AlterField(
- model_name='organization',
- name='slug',
- field=models.SlugField(help_text='Organization name shown in URLs.', max_length=128, verbose_name='organization slug'),
- ),
- migrations.AlterField(
- model_name='problem',
- name='allowed_languages',
- field=models.ManyToManyField(help_text='List of allowed submission languages.', to='judge.Language', verbose_name='allowed languages'),
- ),
- migrations.AlterField(
- model_name='problem',
- name='authors',
- field=models.ManyToManyField(blank=True, help_text='These users will be able to edit the problem, and be listed as authors.', related_name='authored_problems', to='judge.Profile', verbose_name='creators'),
- ),
- migrations.AlterField(
- model_name='problem',
- name='code',
- field=models.CharField(help_text='A short, unique code for the problem, used in the URL after /problem/', max_length=20, unique=True, validators=[django.core.validators.RegexValidator('^[a-z0-9]+$', 'Problem code must be ^[a-z0-9]+$')], verbose_name='problem code'),
- ),
- migrations.AlterField(
- model_name='problem',
- name='curators',
- field=models.ManyToManyField(blank=True, help_text='These users will be able to edit the problem, but not be listed as authors.', related_name='curated_problems', to='judge.Profile', verbose_name='curators'),
- ),
- migrations.AlterField(
- model_name='problem',
- name='date',
- field=models.DateTimeField(blank=True, db_index=True, help_text="Doesn't have the magic ability to auto-publish due to backward compatibility.", null=True, verbose_name='date of publishing'),
- ),
- migrations.AlterField(
- model_name='problem',
- name='description',
- field=models.TextField(validators=[judge.models.problem.disallowed_characters_validator], verbose_name='problem body'),
- ),
- migrations.AlterField(
- model_name='problem',
- name='group',
- field=models.ForeignKey(help_text='The group of problem, shown under Category in the problem list.', on_delete=django.db.models.deletion.CASCADE, to='judge.problemgroup', verbose_name='problem group'),
- ),
- migrations.AlterField(
- model_name='problem',
- name='is_manually_managed',
- field=models.BooleanField(db_index=True, default=False, help_text='Whether judges should be allowed to manage data or not.', verbose_name='manually managed'),
- ),
- migrations.AlterField(
- model_name='problem',
- name='license',
- field=models.ForeignKey(blank=True, help_text='The license under which this problem is published.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='judge.license', verbose_name='license'),
- ),
- migrations.AlterField(
- model_name='problem',
- name='memory_limit',
- field=models.PositiveIntegerField(help_text='The memory limit for this problem, in kilobytes (e.g. 256mb = 262144 kilobytes).', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1048576)], verbose_name='memory limit'),
- ),
- migrations.AlterField(
- model_name='problem',
- name='name',
- field=models.CharField(db_index=True, help_text='The full name of the problem, as shown in the problem list.', max_length=100, verbose_name='problem name'),
- ),
- migrations.AlterField(
- model_name='problem',
- name='points',
- field=models.FloatField(help_text="Points awarded for problem completion. Points are displayed with a 'p' suffix if partial.", validators=[django.core.validators.MinValueValidator(0)], verbose_name='points'),
- ),
- migrations.AlterField(
- model_name='problem',
- name='short_circuit',
- field=models.BooleanField(default=False, verbose_name='short circuit'),
- ),
- migrations.AlterField(
- model_name='problem',
- name='testers',
- field=models.ManyToManyField(blank=True, help_text='These users will be able to view the private problem, but not edit it.', related_name='tested_problems', to='judge.Profile', verbose_name='testers'),
- ),
- migrations.AlterField(
- model_name='problem',
- name='time_limit',
- field=models.FloatField(help_text='The time limit for this problem, in seconds. Fractional seconds (e.g. 1.5) are supported.', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(60)], verbose_name='time limit'),
- ),
- migrations.AlterField(
- model_name='problem',
- name='types',
- field=models.ManyToManyField(help_text="The type of problem, as shown on the problem's page.", to='judge.ProblemType', verbose_name='problem types'),
- ),
- migrations.AlterField(
- model_name='problemclarification',
- name='description',
- field=models.TextField(validators=[judge.models.problem.disallowed_characters_validator], verbose_name='clarification body'),
- ),
- migrations.AlterField(
- model_name='problemdata',
- name='checker_args',
- field=models.TextField(blank=True, help_text='Checker arguments as a JSON object.', verbose_name='checker arguments'),
- ),
- migrations.AlterField(
- model_name='problemtestcase',
- name='checker_args',
- field=models.TextField(blank=True, help_text='Checker arguments as a JSON object.', verbose_name='checker arguments'),
- ),
- migrations.AlterField(
- model_name='problemtranslation',
- name='description',
- field=models.TextField(validators=[judge.models.problem.disallowed_characters_validator], verbose_name='translated description'),
- ),
- migrations.AlterField(
- model_name='problemtranslation',
- name='language',
- field=models.CharField(choices=[('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')], max_length=7, verbose_name='language'),
- ),
- migrations.AlterField(
- model_name='profile',
- name='ace_theme',
- field=models.CharField(choices=[('ambiance', 'Ambiance'), ('chaos', 'Chaos'), ('chrome', 'Chrome'), ('clouds', 'Clouds'), ('clouds_midnight', 'Clouds Midnight'), ('cobalt', 'Cobalt'), ('crimson_editor', 'Crimson Editor'), ('dawn', 'Dawn'), ('dreamweaver', 'Dreamweaver'), ('eclipse', 'Eclipse'), ('github', 'Github'), ('idle_fingers', 'Idle Fingers'), ('katzenmilch', 'Katzenmilch'), ('kr_theme', 'KR Theme'), ('kuroir', 'Kuroir'), ('merbivore', 'Merbivore'), ('merbivore_soft', 'Merbivore Soft'), ('mono_industrial', 'Mono Industrial'), ('monokai', 'Monokai'), ('pastel_on_dark', 'Pastel on Dark'), ('solarized_dark', 'Solarized Dark'), ('solarized_light', 'Solarized Light'), ('terminal', 'Terminal'), ('textmate', 'Textmate'), ('tomorrow', 'Tomorrow'), ('tomorrow_night', 'Tomorrow Night'), ('tomorrow_night_blue', 'Tomorrow Night Blue'), ('tomorrow_night_bright', 'Tomorrow Night Bright'), ('tomorrow_night_eighties', 'Tomorrow Night Eighties'), ('twilight', 'Twilight'), ('vibrant_ink', 'Vibrant Ink'), ('xcode', 'XCode')], default='github', max_length=30, verbose_name='Ace theme'),
- ),
- migrations.AlterField(
- model_name='profile',
- name='api_token',
- field=models.CharField(help_text='64-character hex-encoded API access token.', max_length=64, null=True, validators=[django.core.validators.RegexValidator('^[a-f0-9]{64}$', 'API token must be None or hexadecimal')], verbose_name='API token'),
- ),
- migrations.AlterField(
- model_name='profile',
- name='is_totp_enabled',
- field=models.BooleanField(default=False, help_text='Check to enable TOTP-based two-factor authentication.', verbose_name='TOTP 2FA enabled'),
- ),
- migrations.AlterField(
- model_name='profile',
- name='is_webauthn_enabled',
- field=models.BooleanField(default=False, help_text='Check to enable WebAuthn-based two-factor authentication.', verbose_name='WebAuthn 2FA enabled'),
- ),
- migrations.AlterField(
- model_name='profile',
- name='math_engine',
- field=models.CharField(choices=[('tex', 'Leave as LaTeX'), ('svg', 'SVG with PNG fallback'), ('mml', 'MathML only'), ('jax', 'MathJax with SVG/PNG fallback'), ('auto', 'Detect best quality')], default='auto', help_text='The rendering engine used to render math.', max_length=4, verbose_name='math engine'),
- ),
- migrations.AlterField(
- model_name='profile',
- name='scratch_codes',
- field=judge.models.profile.EncryptedNullCharField(blank=True, help_text='JSON array of 16-character Base32-encoded codes for scratch codes.', max_length=255, null=True, validators=[django.core.validators.RegexValidator('^(\\[\\])?$|^\\[("[A-Z0-9]{16}", *)*"[A-Z0-9]{16}"\\]$', 'Scratch codes must be empty or a JSON array of 16-character Base32 codes.')], verbose_name='scratch codes'),
- ),
- migrations.AlterField(
- model_name='profile',
- name='timezone',
- field=models.CharField(choices=[('Africa', [('Africa/Abidjan', 'Abidjan'), ('Africa/Accra', 'Accra'), ('Africa/Addis_Ababa', 'Addis_Ababa'), ('Africa/Algiers', 'Algiers'), ('Africa/Asmara', 'Asmara'), ('Africa/Asmera', 'Asmera'), ('Africa/Bamako', 'Bamako'), ('Africa/Bangui', 'Bangui'), ('Africa/Banjul', 'Banjul'), ('Africa/Bissau', 'Bissau'), ('Africa/Blantyre', 'Blantyre'), ('Africa/Brazzaville', 'Brazzaville'), ('Africa/Bujumbura', 'Bujumbura'), ('Africa/Cairo', 'Cairo'), ('Africa/Casablanca', 'Casablanca'), ('Africa/Ceuta', 'Ceuta'), ('Africa/Conakry', 'Conakry'), ('Africa/Dakar', 'Dakar'), ('Africa/Dar_es_Salaam', 'Dar_es_Salaam'), ('Africa/Djibouti', 'Djibouti'), ('Africa/Douala', 'Douala'), ('Africa/El_Aaiun', 'El_Aaiun'), ('Africa/Freetown', 'Freetown'), ('Africa/Gaborone', 'Gaborone'), ('Africa/Harare', 'Harare'), ('Africa/Johannesburg', 'Johannesburg'), ('Africa/Juba', 'Juba'), ('Africa/Kampala', 'Kampala'), ('Africa/Khartoum', 'Khartoum'), ('Africa/Kigali', 'Kigali'), ('Africa/Kinshasa', 'Kinshasa'), ('Africa/Lagos', 'Lagos'), ('Africa/Libreville', 'Libreville'), ('Africa/Lome', 'Lome'), ('Africa/Luanda', 'Luanda'), ('Africa/Lubumbashi', 'Lubumbashi'), ('Africa/Lusaka', 'Lusaka'), ('Africa/Malabo', 'Malabo'), ('Africa/Maputo', 'Maputo'), ('Africa/Maseru', 'Maseru'), ('Africa/Mbabane', 'Mbabane'), ('Africa/Mogadishu', 'Mogadishu'), ('Africa/Monrovia', 'Monrovia'), ('Africa/Nairobi', 'Nairobi'), ('Africa/Ndjamena', 'Ndjamena'), ('Africa/Niamey', 'Niamey'), ('Africa/Nouakchott', 'Nouakchott'), ('Africa/Ouagadougou', 'Ouagadougou'), ('Africa/Porto-Novo', 'Porto-Novo'), ('Africa/Sao_Tome', 'Sao_Tome'), ('Africa/Timbuktu', 'Timbuktu'), ('Africa/Tripoli', 'Tripoli'), ('Africa/Tunis', 'Tunis'), ('Africa/Windhoek', 'Windhoek')]), ('America', [('America/Adak', 'Adak'), ('America/Anchorage', 'Anchorage'), ('America/Anguilla', 'Anguilla'), ('America/Antigua', 'Antigua'), ('America/Araguaina', 'Araguaina'), ('America/Argentina/Buenos_Aires', 'Argentina/Buenos_Aires'), ('America/Argentina/Catamarca', 'Argentina/Catamarca'), ('America/Argentina/ComodRivadavia', 'Argentina/ComodRivadavia'), ('America/Argentina/Cordoba', 'Argentina/Cordoba'), ('America/Argentina/Jujuy', 'Argentina/Jujuy'), ('America/Argentina/La_Rioja', 'Argentina/La_Rioja'), ('America/Argentina/Mendoza', 'Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', 'Argentina/Rio_Gallegos'), ('America/Argentina/Salta', 'Argentina/Salta'), ('America/Argentina/San_Juan', 'Argentina/San_Juan'), ('America/Argentina/San_Luis', 'Argentina/San_Luis'), ('America/Argentina/Tucuman', 'Argentina/Tucuman'), ('America/Argentina/Ushuaia', 'Argentina/Ushuaia'), ('America/Aruba', 'Aruba'), ('America/Asuncion', 'Asuncion'), ('America/Atikokan', 'Atikokan'), ('America/Atka', 'Atka'), ('America/Bahia', 'Bahia'), ('America/Bahia_Banderas', 'Bahia_Banderas'), ('America/Barbados', 'Barbados'), ('America/Belem', 'Belem'), ('America/Belize', 'Belize'), ('America/Blanc-Sablon', 'Blanc-Sablon'), ('America/Boa_Vista', 'Boa_Vista'), ('America/Bogota', 'Bogota'), ('America/Boise', 'Boise'), ('America/Buenos_Aires', 'Buenos_Aires'), ('America/Cambridge_Bay', 'Cambridge_Bay'), ('America/Campo_Grande', 'Campo_Grande'), ('America/Cancun', 'Cancun'), ('America/Caracas', 'Caracas'), ('America/Catamarca', 'Catamarca'), ('America/Cayenne', 'Cayenne'), ('America/Cayman', 'Cayman'), ('America/Chicago', 'Chicago'), ('America/Chihuahua', 'Chihuahua'), ('America/Coral_Harbour', 'Coral_Harbour'), ('America/Cordoba', 'Cordoba'), ('America/Costa_Rica', 'Costa_Rica'), ('America/Creston', 'Creston'), ('America/Cuiaba', 'Cuiaba'), ('America/Curacao', 'Curacao'), ('America/Danmarkshavn', 'Danmarkshavn'), ('America/Dawson', 'Dawson'), ('America/Dawson_Creek', 'Dawson_Creek'), ('America/Denver', 'Denver'), ('America/Detroit', 'Detroit'), ('America/Dominica', 'Dominica'), ('America/Edmonton', 'Edmonton'), ('America/Eirunepe', 'Eirunepe'), ('America/El_Salvador', 'El_Salvador'), ('America/Ensenada', 'Ensenada'), ('America/Fort_Nelson', 'Fort_Nelson'), ('America/Fort_Wayne', 'Fort_Wayne'), ('America/Fortaleza', 'Fortaleza'), ('America/Glace_Bay', 'Glace_Bay'), ('America/Godthab', 'Godthab'), ('America/Goose_Bay', 'Goose_Bay'), ('America/Grand_Turk', 'Grand_Turk'), ('America/Grenada', 'Grenada'), ('America/Guadeloupe', 'Guadeloupe'), ('America/Guatemala', 'Guatemala'), ('America/Guayaquil', 'Guayaquil'), ('America/Guyana', 'Guyana'), ('America/Halifax', 'Halifax'), ('America/Havana', 'Havana'), ('America/Hermosillo', 'Hermosillo'), ('America/Indiana/Indianapolis', 'Indiana/Indianapolis'), ('America/Indiana/Knox', 'Indiana/Knox'), ('America/Indiana/Marengo', 'Indiana/Marengo'), ('America/Indiana/Petersburg', 'Indiana/Petersburg'), ('America/Indiana/Tell_City', 'Indiana/Tell_City'), ('America/Indiana/Vevay', 'Indiana/Vevay'), ('America/Indiana/Vincennes', 'Indiana/Vincennes'), ('America/Indiana/Winamac', 'Indiana/Winamac'), ('America/Indianapolis', 'Indianapolis'), ('America/Inuvik', 'Inuvik'), ('America/Iqaluit', 'Iqaluit'), ('America/Jamaica', 'Jamaica'), ('America/Jujuy', 'Jujuy'), ('America/Juneau', 'Juneau'), ('America/Kentucky/Louisville', 'Kentucky/Louisville'), ('America/Kentucky/Monticello', 'Kentucky/Monticello'), ('America/Knox_IN', 'Knox_IN'), ('America/Kralendijk', 'Kralendijk'), ('America/La_Paz', 'La_Paz'), ('America/Lima', 'Lima'), ('America/Los_Angeles', 'Los_Angeles'), ('America/Louisville', 'Louisville'), ('America/Lower_Princes', 'Lower_Princes'), ('America/Maceio', 'Maceio'), ('America/Managua', 'Managua'), ('America/Manaus', 'Manaus'), ('America/Marigot', 'Marigot'), ('America/Martinique', 'Martinique'), ('America/Matamoros', 'Matamoros'), ('America/Mazatlan', 'Mazatlan'), ('America/Mendoza', 'Mendoza'), ('America/Menominee', 'Menominee'), ('America/Merida', 'Merida'), ('America/Metlakatla', 'Metlakatla'), ('America/Mexico_City', 'Mexico_City'), ('America/Miquelon', 'Miquelon'), ('America/Moncton', 'Moncton'), ('America/Monterrey', 'Monterrey'), ('America/Montevideo', 'Montevideo'), ('America/Montreal', 'Montreal'), ('America/Montserrat', 'Montserrat'), ('America/Nassau', 'Nassau'), ('America/New_York', 'New_York'), ('America/Nipigon', 'Nipigon'), ('America/Nome', 'Nome'), ('America/Noronha', 'Noronha'), ('America/North_Dakota/Beulah', 'North_Dakota/Beulah'), ('America/North_Dakota/Center', 'North_Dakota/Center'), ('America/North_Dakota/New_Salem', 'North_Dakota/New_Salem'), ('America/Nuuk', 'Nuuk'), ('America/Ojinaga', 'Ojinaga'), ('America/Panama', 'Panama'), ('America/Pangnirtung', 'Pangnirtung'), ('America/Paramaribo', 'Paramaribo'), ('America/Phoenix', 'Phoenix'), ('America/Port-au-Prince', 'Port-au-Prince'), ('America/Port_of_Spain', 'Port_of_Spain'), ('America/Porto_Acre', 'Porto_Acre'), ('America/Porto_Velho', 'Porto_Velho'), ('America/Puerto_Rico', 'Puerto_Rico'), ('America/Punta_Arenas', 'Punta_Arenas'), ('America/Rainy_River', 'Rainy_River'), ('America/Rankin_Inlet', 'Rankin_Inlet'), ('America/Recife', 'Recife'), ('America/Regina', 'Regina'), ('America/Resolute', 'Resolute'), ('America/Rio_Branco', 'Rio_Branco'), ('America/Rosario', 'Rosario'), ('America/Santa_Isabel', 'Santa_Isabel'), ('America/Santarem', 'Santarem'), ('America/Santiago', 'Santiago'), ('America/Santo_Domingo', 'Santo_Domingo'), ('America/Sao_Paulo', 'Sao_Paulo'), ('America/Scoresbysund', 'Scoresbysund'), ('America/Shiprock', 'Shiprock'), ('America/Sitka', 'Sitka'), ('America/St_Barthelemy', 'St_Barthelemy'), ('America/St_Johns', 'St_Johns'), ('America/St_Kitts', 'St_Kitts'), ('America/St_Lucia', 'St_Lucia'), ('America/St_Thomas', 'St_Thomas'), ('America/St_Vincent', 'St_Vincent'), ('America/Swift_Current', 'Swift_Current'), ('America/Tegucigalpa', 'Tegucigalpa'), ('America/Thule', 'Thule'), ('America/Thunder_Bay', 'Thunder_Bay'), ('America/Tijuana', 'Tijuana'), ('America/Toronto', 'Toronto'), ('America/Tortola', 'Tortola'), ('America/Vancouver', 'Vancouver'), ('America/Virgin', 'Virgin'), ('America/Whitehorse', 'Whitehorse'), ('America/Winnipeg', 'Winnipeg'), ('America/Yakutat', 'Yakutat'), ('America/Yellowknife', 'Yellowknife')]), ('Antarctica', [('Antarctica/Casey', 'Casey'), ('Antarctica/Davis', 'Davis'), ('Antarctica/DumontDUrville', 'DumontDUrville'), ('Antarctica/Macquarie', 'Macquarie'), ('Antarctica/Mawson', 'Mawson'), ('Antarctica/McMurdo', 'McMurdo'), ('Antarctica/Palmer', 'Palmer'), ('Antarctica/Rothera', 'Rothera'), ('Antarctica/South_Pole', 'South_Pole'), ('Antarctica/Syowa', 'Syowa'), ('Antarctica/Troll', 'Troll'), ('Antarctica/Vostok', 'Vostok')]), ('Arctic', [('Arctic/Longyearbyen', 'Longyearbyen')]), ('Asia', [('Asia/Aden', 'Aden'), ('Asia/Almaty', 'Almaty'), ('Asia/Amman', 'Amman'), ('Asia/Anadyr', 'Anadyr'), ('Asia/Aqtau', 'Aqtau'), ('Asia/Aqtobe', 'Aqtobe'), ('Asia/Ashgabat', 'Ashgabat'), ('Asia/Ashkhabad', 'Ashkhabad'), ('Asia/Atyrau', 'Atyrau'), ('Asia/Baghdad', 'Baghdad'), ('Asia/Bahrain', 'Bahrain'), ('Asia/Baku', 'Baku'), ('Asia/Bangkok', 'Bangkok'), ('Asia/Barnaul', 'Barnaul'), ('Asia/Beirut', 'Beirut'), ('Asia/Bishkek', 'Bishkek'), ('Asia/Brunei', 'Brunei'), ('Asia/Calcutta', 'Calcutta'), ('Asia/Chita', 'Chita'), ('Asia/Choibalsan', 'Choibalsan'), ('Asia/Chongqing', 'Chongqing'), ('Asia/Chungking', 'Chungking'), ('Asia/Colombo', 'Colombo'), ('Asia/Dacca', 'Dacca'), ('Asia/Damascus', 'Damascus'), ('Asia/Dhaka', 'Dhaka'), ('Asia/Dili', 'Dili'), ('Asia/Dubai', 'Dubai'), ('Asia/Dushanbe', 'Dushanbe'), ('Asia/Famagusta', 'Famagusta'), ('Asia/Gaza', 'Gaza'), ('Asia/Harbin', 'Harbin'), ('Asia/Hebron', 'Hebron'), ('Asia/Ho_Chi_Minh', 'Ho_Chi_Minh'), ('Asia/Hong_Kong', 'Hong_Kong'), ('Asia/Hovd', 'Hovd'), ('Asia/Irkutsk', 'Irkutsk'), ('Asia/Istanbul', 'Istanbul'), ('Asia/Jakarta', 'Jakarta'), ('Asia/Jayapura', 'Jayapura'), ('Asia/Jerusalem', 'Jerusalem'), ('Asia/Kabul', 'Kabul'), ('Asia/Kamchatka', 'Kamchatka'), ('Asia/Karachi', 'Karachi'), ('Asia/Kashgar', 'Kashgar'), ('Asia/Kathmandu', 'Kathmandu'), ('Asia/Katmandu', 'Katmandu'), ('Asia/Khandyga', 'Khandyga'), ('Asia/Kolkata', 'Kolkata'), ('Asia/Krasnoyarsk', 'Krasnoyarsk'), ('Asia/Kuala_Lumpur', 'Kuala_Lumpur'), ('Asia/Kuching', 'Kuching'), ('Asia/Kuwait', 'Kuwait'), ('Asia/Macao', 'Macao'), ('Asia/Macau', 'Macau'), ('Asia/Magadan', 'Magadan'), ('Asia/Makassar', 'Makassar'), ('Asia/Manila', 'Manila'), ('Asia/Muscat', 'Muscat'), ('Asia/Nicosia', 'Nicosia'), ('Asia/Novokuznetsk', 'Novokuznetsk'), ('Asia/Novosibirsk', 'Novosibirsk'), ('Asia/Omsk', 'Omsk'), ('Asia/Oral', 'Oral'), ('Asia/Phnom_Penh', 'Phnom_Penh'), ('Asia/Pontianak', 'Pontianak'), ('Asia/Pyongyang', 'Pyongyang'), ('Asia/Qatar', 'Qatar'), ('Asia/Qostanay', 'Qostanay'), ('Asia/Qyzylorda', 'Qyzylorda'), ('Asia/Rangoon', 'Rangoon'), ('Asia/Riyadh', 'Riyadh'), ('Asia/Saigon', 'Saigon'), ('Asia/Sakhalin', 'Sakhalin'), ('Asia/Samarkand', 'Samarkand'), ('Asia/Seoul', 'Seoul'), ('Asia/Shanghai', 'Shanghai'), ('Asia/Singapore', 'Singapore'), ('Asia/Srednekolymsk', 'Srednekolymsk'), ('Asia/Taipei', 'Taipei'), ('Asia/Tashkent', 'Tashkent'), ('Asia/Tbilisi', 'Tbilisi'), ('Asia/Tehran', 'Tehran'), ('Asia/Tel_Aviv', 'Tel_Aviv'), ('Asia/Thimbu', 'Thimbu'), ('Asia/Thimphu', 'Thimphu'), ('Asia/Tokyo', 'Tokyo'), ('Asia/Tomsk', 'Tomsk'), ('Asia/Ujung_Pandang', 'Ujung_Pandang'), ('Asia/Ulaanbaatar', 'Ulaanbaatar'), ('Asia/Ulan_Bator', 'Ulan_Bator'), ('Asia/Urumqi', 'Urumqi'), ('Asia/Ust-Nera', 'Ust-Nera'), ('Asia/Vientiane', 'Vientiane'), ('Asia/Vladivostok', 'Vladivostok'), ('Asia/Yakutsk', 'Yakutsk'), ('Asia/Yangon', 'Yangon'), ('Asia/Yekaterinburg', 'Yekaterinburg'), ('Asia/Yerevan', 'Yerevan')]), ('Atlantic', [('Atlantic/Azores', 'Azores'), ('Atlantic/Bermuda', 'Bermuda'), ('Atlantic/Canary', 'Canary'), ('Atlantic/Cape_Verde', 'Cape_Verde'), ('Atlantic/Faeroe', 'Faeroe'), ('Atlantic/Faroe', 'Faroe'), ('Atlantic/Jan_Mayen', 'Jan_Mayen'), ('Atlantic/Madeira', 'Madeira'), ('Atlantic/Reykjavik', 'Reykjavik'), ('Atlantic/South_Georgia', 'South_Georgia'), ('Atlantic/St_Helena', 'St_Helena'), ('Atlantic/Stanley', 'Stanley')]), ('Australia', [('Australia/ACT', 'ACT'), ('Australia/Adelaide', 'Adelaide'), ('Australia/Brisbane', 'Brisbane'), ('Australia/Broken_Hill', 'Broken_Hill'), ('Australia/Canberra', 'Canberra'), ('Australia/Currie', 'Currie'), ('Australia/Darwin', 'Darwin'), ('Australia/Eucla', 'Eucla'), ('Australia/Hobart', 'Hobart'), ('Australia/LHI', 'LHI'), ('Australia/Lindeman', 'Lindeman'), ('Australia/Lord_Howe', 'Lord_Howe'), ('Australia/Melbourne', 'Melbourne'), ('Australia/NSW', 'NSW'), ('Australia/North', 'North'), ('Australia/Perth', 'Perth'), ('Australia/Queensland', 'Queensland'), ('Australia/South', 'South'), ('Australia/Sydney', 'Sydney'), ('Australia/Tasmania', 'Tasmania'), ('Australia/Victoria', 'Victoria'), ('Australia/West', 'West'), ('Australia/Yancowinna', 'Yancowinna')]), ('Brazil', [('Brazil/Acre', 'Acre'), ('Brazil/DeNoronha', 'DeNoronha'), ('Brazil/East', 'East'), ('Brazil/West', 'West')]), ('Canada', [('Canada/Atlantic', 'Atlantic'), ('Canada/Central', 'Central'), ('Canada/Eastern', 'Eastern'), ('Canada/Mountain', 'Mountain'), ('Canada/Newfoundland', 'Newfoundland'), ('Canada/Pacific', 'Pacific'), ('Canada/Saskatchewan', 'Saskatchewan'), ('Canada/Yukon', 'Yukon')]), ('Chile', [('Chile/Continental', 'Continental'), ('Chile/EasterIsland', 'EasterIsland')]), ('Etc', [('Etc/Greenwich', 'Greenwich'), ('Etc/UCT', 'UCT'), ('Etc/UTC', 'UTC'), ('Etc/Universal', 'Universal'), ('Etc/Zulu', 'Zulu')]), ('Europe', [('Europe/Amsterdam', 'Amsterdam'), ('Europe/Andorra', 'Andorra'), ('Europe/Astrakhan', 'Astrakhan'), ('Europe/Athens', 'Athens'), ('Europe/Belfast', 'Belfast'), ('Europe/Belgrade', 'Belgrade'), ('Europe/Berlin', 'Berlin'), ('Europe/Bratislava', 'Bratislava'), ('Europe/Brussels', 'Brussels'), ('Europe/Bucharest', 'Bucharest'), ('Europe/Budapest', 'Budapest'), ('Europe/Busingen', 'Busingen'), ('Europe/Chisinau', 'Chisinau'), ('Europe/Copenhagen', 'Copenhagen'), ('Europe/Dublin', 'Dublin'), ('Europe/Gibraltar', 'Gibraltar'), ('Europe/Guernsey', 'Guernsey'), ('Europe/Helsinki', 'Helsinki'), ('Europe/Isle_of_Man', 'Isle_of_Man'), ('Europe/Istanbul', 'Istanbul'), ('Europe/Jersey', 'Jersey'), ('Europe/Kaliningrad', 'Kaliningrad'), ('Europe/Kiev', 'Kiev'), ('Europe/Kirov', 'Kirov'), ('Europe/Kyiv', 'Kyiv'), ('Europe/Lisbon', 'Lisbon'), ('Europe/Ljubljana', 'Ljubljana'), ('Europe/London', 'London'), ('Europe/Luxembourg', 'Luxembourg'), ('Europe/Madrid', 'Madrid'), ('Europe/Malta', 'Malta'), ('Europe/Mariehamn', 'Mariehamn'), ('Europe/Minsk', 'Minsk'), ('Europe/Monaco', 'Monaco'), ('Europe/Moscow', 'Moscow'), ('Europe/Nicosia', 'Nicosia'), ('Europe/Oslo', 'Oslo'), ('Europe/Paris', 'Paris'), ('Europe/Podgorica', 'Podgorica'), ('Europe/Prague', 'Prague'), ('Europe/Riga', 'Riga'), ('Europe/Rome', 'Rome'), ('Europe/Samara', 'Samara'), ('Europe/San_Marino', 'San_Marino'), ('Europe/Sarajevo', 'Sarajevo'), ('Europe/Saratov', 'Saratov'), ('Europe/Simferopol', 'Simferopol'), ('Europe/Skopje', 'Skopje'), ('Europe/Sofia', 'Sofia'), ('Europe/Stockholm', 'Stockholm'), ('Europe/Tallinn', 'Tallinn'), ('Europe/Tirane', 'Tirane'), ('Europe/Tiraspol', 'Tiraspol'), ('Europe/Ulyanovsk', 'Ulyanovsk'), ('Europe/Uzhgorod', 'Uzhgorod'), ('Europe/Vaduz', 'Vaduz'), ('Europe/Vatican', 'Vatican'), ('Europe/Vienna', 'Vienna'), ('Europe/Vilnius', 'Vilnius'), ('Europe/Volgograd', 'Volgograd'), ('Europe/Warsaw', 'Warsaw'), ('Europe/Zagreb', 'Zagreb'), ('Europe/Zaporozhye', 'Zaporozhye'), ('Europe/Zurich', 'Zurich')]), ('Indian', [('Indian/Antananarivo', 'Antananarivo'), ('Indian/Chagos', 'Chagos'), ('Indian/Christmas', 'Christmas'), ('Indian/Cocos', 'Cocos'), ('Indian/Comoro', 'Comoro'), ('Indian/Kerguelen', 'Kerguelen'), ('Indian/Mahe', 'Mahe'), ('Indian/Maldives', 'Maldives'), ('Indian/Mauritius', 'Mauritius'), ('Indian/Mayotte', 'Mayotte'), ('Indian/Reunion', 'Reunion')]), ('Mexico', [('Mexico/BajaNorte', 'BajaNorte'), ('Mexico/BajaSur', 'BajaSur'), ('Mexico/General', 'General')]), ('Other', [('CET', 'CET'), ('CST6CDT', 'CST6CDT'), ('Cuba', 'Cuba'), ('EET', 'EET'), ('EST', 'EST'), ('EST5EDT', 'EST5EDT'), ('Egypt', 'Egypt'), ('Eire', 'Eire'), ('GB', 'GB'), ('GB-Eire', 'GB-Eire'), ('Greenwich', 'Greenwich'), ('HST', 'HST'), ('Hongkong', 'Hongkong'), ('Iceland', 'Iceland'), ('Iran', 'Iran'), ('Israel', 'Israel'), ('Jamaica', 'Jamaica'), ('Japan', 'Japan'), ('Kwajalein', 'Kwajalein'), ('Libya', 'Libya'), ('MET', 'MET'), ('MST', 'MST'), ('MST7MDT', 'MST7MDT'), ('NZ', 'NZ'), ('NZ-CHAT', 'NZ-CHAT'), ('Navajo', 'Navajo'), ('PRC', 'PRC'), ('PST8PDT', 'PST8PDT'), ('Poland', 'Poland'), ('Portugal', 'Portugal'), ('ROC', 'ROC'), ('ROK', 'ROK'), ('Singapore', 'Singapore'), ('Turkey', 'Turkey'), ('UCT', 'UCT'), ('UTC', 'UTC'), ('Universal', 'Universal'), ('W-SU', 'W-SU'), ('WET', 'WET'), ('Zulu', 'Zulu')]), ('Pacific', [('Pacific/Apia', 'Apia'), ('Pacific/Auckland', 'Auckland'), ('Pacific/Bougainville', 'Bougainville'), ('Pacific/Chatham', 'Chatham'), ('Pacific/Chuuk', 'Chuuk'), ('Pacific/Easter', 'Easter'), ('Pacific/Efate', 'Efate'), ('Pacific/Enderbury', 'Enderbury'), ('Pacific/Fakaofo', 'Fakaofo'), ('Pacific/Fiji', 'Fiji'), ('Pacific/Funafuti', 'Funafuti'), ('Pacific/Galapagos', 'Galapagos'), ('Pacific/Gambier', 'Gambier'), ('Pacific/Guadalcanal', 'Guadalcanal'), ('Pacific/Guam', 'Guam'), ('Pacific/Honolulu', 'Honolulu'), ('Pacific/Johnston', 'Johnston'), ('Pacific/Kanton', 'Kanton'), ('Pacific/Kiritimati', 'Kiritimati'), ('Pacific/Kosrae', 'Kosrae'), ('Pacific/Kwajalein', 'Kwajalein'), ('Pacific/Majuro', 'Majuro'), ('Pacific/Marquesas', 'Marquesas'), ('Pacific/Midway', 'Midway'), ('Pacific/Nauru', 'Nauru'), ('Pacific/Niue', 'Niue'), ('Pacific/Norfolk', 'Norfolk'), ('Pacific/Noumea', 'Noumea'), ('Pacific/Pago_Pago', 'Pago_Pago'), ('Pacific/Palau', 'Palau'), ('Pacific/Pitcairn', 'Pitcairn'), ('Pacific/Pohnpei', 'Pohnpei'), ('Pacific/Ponape', 'Ponape'), ('Pacific/Port_Moresby', 'Port_Moresby'), ('Pacific/Rarotonga', 'Rarotonga'), ('Pacific/Saipan', 'Saipan'), ('Pacific/Samoa', 'Samoa'), ('Pacific/Tahiti', 'Tahiti'), ('Pacific/Tarawa', 'Tarawa'), ('Pacific/Tongatapu', 'Tongatapu'), ('Pacific/Truk', 'Truk'), ('Pacific/Wake', 'Wake'), ('Pacific/Wallis', 'Wallis'), ('Pacific/Yap', 'Yap')]), ('US', [('US/Alaska', 'Alaska'), ('US/Aleutian', 'Aleutian'), ('US/Arizona', 'Arizona'), ('US/Central', 'Central'), ('US/East-Indiana', 'East-Indiana'), ('US/Eastern', 'Eastern'), ('US/Hawaii', 'Hawaii'), ('US/Indiana-Starke', 'Indiana-Starke'), ('US/Michigan', 'Michigan'), ('US/Mountain', 'Mountain'), ('US/Pacific', 'Pacific'), ('US/Samoa', 'Samoa')])], default='America/Toronto', max_length=50, verbose_name='time zone'),
- ),
- migrations.AlterField(
- model_name='profile',
- name='totp_key',
- field=judge.models.profile.EncryptedNullCharField(blank=True, help_text='32-character Base32-encoded key for TOTP.', max_length=32, null=True, validators=[django.core.validators.RegexValidator('^$|^[A-Z2-7]{32}$', 'TOTP key must be empty or Base32.')], verbose_name='TOTP key'),
- ),
- migrations.AlterField(
- model_name='profile',
- name='username_display_override',
- field=models.CharField(blank=True, help_text='Name displayed in place of username.', max_length=100, verbose_name='display name override'),
- ),
- migrations.AlterField(
- model_name='solution',
- name='content',
- field=models.TextField(validators=[judge.models.problem.disallowed_characters_validator], verbose_name='editorial content'),
- ),
- migrations.AlterField(
- model_name='submission',
- name='problem',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.problem', verbose_name='problem'),
- ),
- migrations.AlterField(
- model_name='submission',
- name='result',
- field=models.CharField(blank=True, choices=[('AC', 'Accepted'), ('WA', 'Wrong Answer'), ('TLE', 'Time Limit Exceeded'), ('MLE', 'Memory Limit Exceeded'), ('OLE', 'Output Limit Exceeded'), ('IR', 'Invalid Return'), ('RTE', 'Runtime Error'), ('CE', 'Compile Error'), ('IE', 'Internal Error'), ('SC', 'Short Circuited'), ('AB', 'Aborted')], db_index=True, default=None, max_length=3, null=True, verbose_name='result'),
- ),
- migrations.AlterField(
- model_name='submission',
- name='user',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.profile', verbose_name='user'),
- ),
- migrations.AlterField(
- model_name='submissiontestcase',
- name='status',
- field=models.CharField(choices=[('AC', 'Accepted'), ('WA', 'Wrong Answer'), ('TLE', 'Time Limit Exceeded'), ('MLE', 'Memory Limit Exceeded'), ('OLE', 'Output Limit Exceeded'), ('IR', 'Invalid Return'), ('RTE', 'Runtime Error'), ('CE', 'Compile Error'), ('IE', 'Internal Error'), ('SC', 'Short Circuited'), ('AB', 'Aborted')], max_length=3, verbose_name='status flag'),
- ),
- migrations.AlterField(
- model_name='ticketmessage',
- name='user',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ticket_messages', to='judge.profile', verbose_name='user'),
+ model_name="organization",
+ name="access_code",
+ field=models.CharField(
+ blank=True,
+ help_text="Student access code.",
+ max_length=7,
+ null=True,
+ verbose_name="access code",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="organization",
+ name="admins",
+ field=models.ManyToManyField(
+ help_text="Those who can edit this organization.",
+ related_name="admin_of",
+ to="judge.Profile",
+ verbose_name="administrators",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="organization",
+ name="class_required",
+ field=models.BooleanField(
+ default=False,
+ help_text="Whether members are compelled to select a class when joining.",
+ verbose_name="class membership required",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="organization",
+ name="is_open",
+ field=models.BooleanField(
+ default=True,
+ help_text="Allow joining organization.",
+ verbose_name="is open organization?",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="organization",
+ name="logo_override_image",
+ field=models.CharField(
+ blank=True,
+ default="",
+ help_text="This image will replace the default site logo for users viewing the organization.",
+ max_length=150,
+ verbose_name="logo override image",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="organization",
+ name="short_name",
+ field=models.CharField(
+ help_text="Displayed beside user name during contests.",
+ max_length=20,
+ verbose_name="short name",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="organization",
+ name="slots",
+ field=models.IntegerField(
+ blank=True,
+ help_text="Maximum amount of users in this organization, only applicable to private organizations.",
+ null=True,
+ verbose_name="maximum size",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="organization",
+ name="slug",
+ field=models.SlugField(
+ help_text="Organization name shown in URLs.",
+ max_length=128,
+ verbose_name="organization slug",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="problem",
+ name="allowed_languages",
+ field=models.ManyToManyField(
+ help_text="List of allowed submission languages.",
+ to="judge.Language",
+ verbose_name="allowed languages",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="problem",
+ name="authors",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="These users will be able to edit the problem, and be listed as authors.",
+ related_name="authored_problems",
+ to="judge.Profile",
+ verbose_name="creators",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="problem",
+ name="code",
+ field=models.CharField(
+ help_text="A short, unique code for the problem, used in the URL after /problem/",
+ max_length=20,
+ unique=True,
+ validators=[
+ django.core.validators.RegexValidator(
+ "^[a-z0-9]+$", "Problem code must be ^[a-z0-9]+$"
+ )
+ ],
+ verbose_name="problem code",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="problem",
+ name="curators",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="These users will be able to edit the problem, but not be listed as authors.",
+ related_name="curated_problems",
+ to="judge.Profile",
+ verbose_name="curators",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="problem",
+ name="date",
+ field=models.DateTimeField(
+ blank=True,
+ db_index=True,
+ help_text="Doesn't have the magic ability to auto-publish due to backward compatibility.",
+ null=True,
+ verbose_name="date of publishing",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="problem",
+ name="description",
+ field=models.TextField(
+ validators=[judge.models.problem.disallowed_characters_validator],
+ verbose_name="problem body",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="problem",
+ name="group",
+ field=models.ForeignKey(
+ help_text="The group of problem, shown under Category in the problem list.",
+ on_delete=django.db.models.deletion.CASCADE,
+ to="judge.problemgroup",
+ verbose_name="problem group",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="problem",
+ name="is_manually_managed",
+ field=models.BooleanField(
+ db_index=True,
+ default=False,
+ help_text="Whether judges should be allowed to manage data or not.",
+ verbose_name="manually managed",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="problem",
+ name="license",
+ field=models.ForeignKey(
+ blank=True,
+ help_text="The license under which this problem is published.",
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to="judge.license",
+ verbose_name="license",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="problem",
+ name="memory_limit",
+ field=models.PositiveIntegerField(
+ help_text="The memory limit for this problem, in kilobytes (e.g. 256mb = 262144 kilobytes).",
+ validators=[
+ django.core.validators.MinValueValidator(0),
+ django.core.validators.MaxValueValidator(1048576),
+ ],
+ verbose_name="memory limit",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="problem",
+ name="name",
+ field=models.CharField(
+ db_index=True,
+ help_text="The full name of the problem, as shown in the problem list.",
+ max_length=100,
+ verbose_name="problem name",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="problem",
+ name="points",
+ field=models.FloatField(
+ help_text="Points awarded for problem completion. Points are displayed with a 'p' suffix if partial.",
+ validators=[django.core.validators.MinValueValidator(0)],
+ verbose_name="points",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="problem",
+ name="short_circuit",
+ field=models.BooleanField(default=False, verbose_name="short circuit"),
+ ),
+ migrations.AlterField(
+ model_name="problem",
+ name="testers",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="These users will be able to view the private problem, but not edit it.",
+ related_name="tested_problems",
+ to="judge.Profile",
+ verbose_name="testers",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="problem",
+ name="time_limit",
+ field=models.FloatField(
+ help_text="The time limit for this problem, in seconds. Fractional seconds (e.g. 1.5) are supported.",
+ validators=[
+ django.core.validators.MinValueValidator(0),
+ django.core.validators.MaxValueValidator(60),
+ ],
+ verbose_name="time limit",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="problem",
+ name="types",
+ field=models.ManyToManyField(
+ help_text="The type of problem, as shown on the problem's page.",
+ to="judge.ProblemType",
+ verbose_name="problem types",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="problemclarification",
+ name="description",
+ field=models.TextField(
+ validators=[judge.models.problem.disallowed_characters_validator],
+ verbose_name="clarification body",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="problemdata",
+ name="checker_args",
+ field=models.TextField(
+ blank=True,
+ help_text="Checker arguments as a JSON object.",
+ verbose_name="checker arguments",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="problemtestcase",
+ name="checker_args",
+ field=models.TextField(
+ blank=True,
+ help_text="Checker arguments as a JSON object.",
+ verbose_name="checker arguments",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="problemtranslation",
+ name="description",
+ field=models.TextField(
+ validators=[judge.models.problem.disallowed_characters_validator],
+ verbose_name="translated description",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="problemtranslation",
+ name="language",
+ field=models.CharField(
+ choices=[
+ ("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"),
+ ],
+ max_length=7,
+ verbose_name="language",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="profile",
+ name="ace_theme",
+ field=models.CharField(
+ choices=[
+ ("ambiance", "Ambiance"),
+ ("chaos", "Chaos"),
+ ("chrome", "Chrome"),
+ ("clouds", "Clouds"),
+ ("clouds_midnight", "Clouds Midnight"),
+ ("cobalt", "Cobalt"),
+ ("crimson_editor", "Crimson Editor"),
+ ("dawn", "Dawn"),
+ ("dreamweaver", "Dreamweaver"),
+ ("eclipse", "Eclipse"),
+ ("github", "Github"),
+ ("idle_fingers", "Idle Fingers"),
+ ("katzenmilch", "Katzenmilch"),
+ ("kr_theme", "KR Theme"),
+ ("kuroir", "Kuroir"),
+ ("merbivore", "Merbivore"),
+ ("merbivore_soft", "Merbivore Soft"),
+ ("mono_industrial", "Mono Industrial"),
+ ("monokai", "Monokai"),
+ ("pastel_on_dark", "Pastel on Dark"),
+ ("solarized_dark", "Solarized Dark"),
+ ("solarized_light", "Solarized Light"),
+ ("terminal", "Terminal"),
+ ("textmate", "Textmate"),
+ ("tomorrow", "Tomorrow"),
+ ("tomorrow_night", "Tomorrow Night"),
+ ("tomorrow_night_blue", "Tomorrow Night Blue"),
+ ("tomorrow_night_bright", "Tomorrow Night Bright"),
+ ("tomorrow_night_eighties", "Tomorrow Night Eighties"),
+ ("twilight", "Twilight"),
+ ("vibrant_ink", "Vibrant Ink"),
+ ("xcode", "XCode"),
+ ],
+ default="github",
+ max_length=30,
+ verbose_name="Ace theme",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="profile",
+ name="api_token",
+ field=models.CharField(
+ help_text="64-character hex-encoded API access token.",
+ max_length=64,
+ null=True,
+ validators=[
+ django.core.validators.RegexValidator(
+ "^[a-f0-9]{64}$", "API token must be None or hexadecimal"
+ )
+ ],
+ verbose_name="API token",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="profile",
+ name="is_totp_enabled",
+ field=models.BooleanField(
+ default=False,
+ help_text="Check to enable TOTP-based two-factor authentication.",
+ verbose_name="TOTP 2FA enabled",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="profile",
+ name="is_webauthn_enabled",
+ field=models.BooleanField(
+ default=False,
+ help_text="Check to enable WebAuthn-based two-factor authentication.",
+ verbose_name="WebAuthn 2FA enabled",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="profile",
+ name="math_engine",
+ field=models.CharField(
+ choices=[
+ ("tex", "Leave as LaTeX"),
+ ("svg", "SVG with PNG fallback"),
+ ("mml", "MathML only"),
+ ("jax", "MathJax with SVG/PNG fallback"),
+ ("auto", "Detect best quality"),
+ ],
+ default="auto",
+ help_text="The rendering engine used to render math.",
+ max_length=4,
+ verbose_name="math engine",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="profile",
+ name="scratch_codes",
+ field=judge.models.profile.EncryptedNullCharField(
+ blank=True,
+ help_text="JSON array of 16-character Base32-encoded codes for scratch codes.",
+ max_length=255,
+ null=True,
+ validators=[
+ django.core.validators.RegexValidator(
+ '^(\\[\\])?$|^\\[("[A-Z0-9]{16}", *)*"[A-Z0-9]{16}"\\]$',
+ "Scratch codes must be empty or a JSON array of 16-character Base32 codes.",
+ )
+ ],
+ verbose_name="scratch codes",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="profile",
+ name="timezone",
+ field=models.CharField(
+ choices=[
+ (
+ "Africa",
+ [
+ ("Africa/Abidjan", "Abidjan"),
+ ("Africa/Accra", "Accra"),
+ ("Africa/Addis_Ababa", "Addis_Ababa"),
+ ("Africa/Algiers", "Algiers"),
+ ("Africa/Asmara", "Asmara"),
+ ("Africa/Asmera", "Asmera"),
+ ("Africa/Bamako", "Bamako"),
+ ("Africa/Bangui", "Bangui"),
+ ("Africa/Banjul", "Banjul"),
+ ("Africa/Bissau", "Bissau"),
+ ("Africa/Blantyre", "Blantyre"),
+ ("Africa/Brazzaville", "Brazzaville"),
+ ("Africa/Bujumbura", "Bujumbura"),
+ ("Africa/Cairo", "Cairo"),
+ ("Africa/Casablanca", "Casablanca"),
+ ("Africa/Ceuta", "Ceuta"),
+ ("Africa/Conakry", "Conakry"),
+ ("Africa/Dakar", "Dakar"),
+ ("Africa/Dar_es_Salaam", "Dar_es_Salaam"),
+ ("Africa/Djibouti", "Djibouti"),
+ ("Africa/Douala", "Douala"),
+ ("Africa/El_Aaiun", "El_Aaiun"),
+ ("Africa/Freetown", "Freetown"),
+ ("Africa/Gaborone", "Gaborone"),
+ ("Africa/Harare", "Harare"),
+ ("Africa/Johannesburg", "Johannesburg"),
+ ("Africa/Juba", "Juba"),
+ ("Africa/Kampala", "Kampala"),
+ ("Africa/Khartoum", "Khartoum"),
+ ("Africa/Kigali", "Kigali"),
+ ("Africa/Kinshasa", "Kinshasa"),
+ ("Africa/Lagos", "Lagos"),
+ ("Africa/Libreville", "Libreville"),
+ ("Africa/Lome", "Lome"),
+ ("Africa/Luanda", "Luanda"),
+ ("Africa/Lubumbashi", "Lubumbashi"),
+ ("Africa/Lusaka", "Lusaka"),
+ ("Africa/Malabo", "Malabo"),
+ ("Africa/Maputo", "Maputo"),
+ ("Africa/Maseru", "Maseru"),
+ ("Africa/Mbabane", "Mbabane"),
+ ("Africa/Mogadishu", "Mogadishu"),
+ ("Africa/Monrovia", "Monrovia"),
+ ("Africa/Nairobi", "Nairobi"),
+ ("Africa/Ndjamena", "Ndjamena"),
+ ("Africa/Niamey", "Niamey"),
+ ("Africa/Nouakchott", "Nouakchott"),
+ ("Africa/Ouagadougou", "Ouagadougou"),
+ ("Africa/Porto-Novo", "Porto-Novo"),
+ ("Africa/Sao_Tome", "Sao_Tome"),
+ ("Africa/Timbuktu", "Timbuktu"),
+ ("Africa/Tripoli", "Tripoli"),
+ ("Africa/Tunis", "Tunis"),
+ ("Africa/Windhoek", "Windhoek"),
+ ],
+ ),
+ (
+ "America",
+ [
+ ("America/Adak", "Adak"),
+ ("America/Anchorage", "Anchorage"),
+ ("America/Anguilla", "Anguilla"),
+ ("America/Antigua", "Antigua"),
+ ("America/Araguaina", "Araguaina"),
+ (
+ "America/Argentina/Buenos_Aires",
+ "Argentina/Buenos_Aires",
+ ),
+ ("America/Argentina/Catamarca", "Argentina/Catamarca"),
+ (
+ "America/Argentina/ComodRivadavia",
+ "Argentina/ComodRivadavia",
+ ),
+ ("America/Argentina/Cordoba", "Argentina/Cordoba"),
+ ("America/Argentina/Jujuy", "Argentina/Jujuy"),
+ ("America/Argentina/La_Rioja", "Argentina/La_Rioja"),
+ ("America/Argentina/Mendoza", "Argentina/Mendoza"),
+ (
+ "America/Argentina/Rio_Gallegos",
+ "Argentina/Rio_Gallegos",
+ ),
+ ("America/Argentina/Salta", "Argentina/Salta"),
+ ("America/Argentina/San_Juan", "Argentina/San_Juan"),
+ ("America/Argentina/San_Luis", "Argentina/San_Luis"),
+ ("America/Argentina/Tucuman", "Argentina/Tucuman"),
+ ("America/Argentina/Ushuaia", "Argentina/Ushuaia"),
+ ("America/Aruba", "Aruba"),
+ ("America/Asuncion", "Asuncion"),
+ ("America/Atikokan", "Atikokan"),
+ ("America/Atka", "Atka"),
+ ("America/Bahia", "Bahia"),
+ ("America/Bahia_Banderas", "Bahia_Banderas"),
+ ("America/Barbados", "Barbados"),
+ ("America/Belem", "Belem"),
+ ("America/Belize", "Belize"),
+ ("America/Blanc-Sablon", "Blanc-Sablon"),
+ ("America/Boa_Vista", "Boa_Vista"),
+ ("America/Bogota", "Bogota"),
+ ("America/Boise", "Boise"),
+ ("America/Buenos_Aires", "Buenos_Aires"),
+ ("America/Cambridge_Bay", "Cambridge_Bay"),
+ ("America/Campo_Grande", "Campo_Grande"),
+ ("America/Cancun", "Cancun"),
+ ("America/Caracas", "Caracas"),
+ ("America/Catamarca", "Catamarca"),
+ ("America/Cayenne", "Cayenne"),
+ ("America/Cayman", "Cayman"),
+ ("America/Chicago", "Chicago"),
+ ("America/Chihuahua", "Chihuahua"),
+ ("America/Coral_Harbour", "Coral_Harbour"),
+ ("America/Cordoba", "Cordoba"),
+ ("America/Costa_Rica", "Costa_Rica"),
+ ("America/Creston", "Creston"),
+ ("America/Cuiaba", "Cuiaba"),
+ ("America/Curacao", "Curacao"),
+ ("America/Danmarkshavn", "Danmarkshavn"),
+ ("America/Dawson", "Dawson"),
+ ("America/Dawson_Creek", "Dawson_Creek"),
+ ("America/Denver", "Denver"),
+ ("America/Detroit", "Detroit"),
+ ("America/Dominica", "Dominica"),
+ ("America/Edmonton", "Edmonton"),
+ ("America/Eirunepe", "Eirunepe"),
+ ("America/El_Salvador", "El_Salvador"),
+ ("America/Ensenada", "Ensenada"),
+ ("America/Fort_Nelson", "Fort_Nelson"),
+ ("America/Fort_Wayne", "Fort_Wayne"),
+ ("America/Fortaleza", "Fortaleza"),
+ ("America/Glace_Bay", "Glace_Bay"),
+ ("America/Godthab", "Godthab"),
+ ("America/Goose_Bay", "Goose_Bay"),
+ ("America/Grand_Turk", "Grand_Turk"),
+ ("America/Grenada", "Grenada"),
+ ("America/Guadeloupe", "Guadeloupe"),
+ ("America/Guatemala", "Guatemala"),
+ ("America/Guayaquil", "Guayaquil"),
+ ("America/Guyana", "Guyana"),
+ ("America/Halifax", "Halifax"),
+ ("America/Havana", "Havana"),
+ ("America/Hermosillo", "Hermosillo"),
+ ("America/Indiana/Indianapolis", "Indiana/Indianapolis"),
+ ("America/Indiana/Knox", "Indiana/Knox"),
+ ("America/Indiana/Marengo", "Indiana/Marengo"),
+ ("America/Indiana/Petersburg", "Indiana/Petersburg"),
+ ("America/Indiana/Tell_City", "Indiana/Tell_City"),
+ ("America/Indiana/Vevay", "Indiana/Vevay"),
+ ("America/Indiana/Vincennes", "Indiana/Vincennes"),
+ ("America/Indiana/Winamac", "Indiana/Winamac"),
+ ("America/Indianapolis", "Indianapolis"),
+ ("America/Inuvik", "Inuvik"),
+ ("America/Iqaluit", "Iqaluit"),
+ ("America/Jamaica", "Jamaica"),
+ ("America/Jujuy", "Jujuy"),
+ ("America/Juneau", "Juneau"),
+ ("America/Kentucky/Louisville", "Kentucky/Louisville"),
+ ("America/Kentucky/Monticello", "Kentucky/Monticello"),
+ ("America/Knox_IN", "Knox_IN"),
+ ("America/Kralendijk", "Kralendijk"),
+ ("America/La_Paz", "La_Paz"),
+ ("America/Lima", "Lima"),
+ ("America/Los_Angeles", "Los_Angeles"),
+ ("America/Louisville", "Louisville"),
+ ("America/Lower_Princes", "Lower_Princes"),
+ ("America/Maceio", "Maceio"),
+ ("America/Managua", "Managua"),
+ ("America/Manaus", "Manaus"),
+ ("America/Marigot", "Marigot"),
+ ("America/Martinique", "Martinique"),
+ ("America/Matamoros", "Matamoros"),
+ ("America/Mazatlan", "Mazatlan"),
+ ("America/Mendoza", "Mendoza"),
+ ("America/Menominee", "Menominee"),
+ ("America/Merida", "Merida"),
+ ("America/Metlakatla", "Metlakatla"),
+ ("America/Mexico_City", "Mexico_City"),
+ ("America/Miquelon", "Miquelon"),
+ ("America/Moncton", "Moncton"),
+ ("America/Monterrey", "Monterrey"),
+ ("America/Montevideo", "Montevideo"),
+ ("America/Montreal", "Montreal"),
+ ("America/Montserrat", "Montserrat"),
+ ("America/Nassau", "Nassau"),
+ ("America/New_York", "New_York"),
+ ("America/Nipigon", "Nipigon"),
+ ("America/Nome", "Nome"),
+ ("America/Noronha", "Noronha"),
+ ("America/North_Dakota/Beulah", "North_Dakota/Beulah"),
+ ("America/North_Dakota/Center", "North_Dakota/Center"),
+ (
+ "America/North_Dakota/New_Salem",
+ "North_Dakota/New_Salem",
+ ),
+ ("America/Nuuk", "Nuuk"),
+ ("America/Ojinaga", "Ojinaga"),
+ ("America/Panama", "Panama"),
+ ("America/Pangnirtung", "Pangnirtung"),
+ ("America/Paramaribo", "Paramaribo"),
+ ("America/Phoenix", "Phoenix"),
+ ("America/Port-au-Prince", "Port-au-Prince"),
+ ("America/Port_of_Spain", "Port_of_Spain"),
+ ("America/Porto_Acre", "Porto_Acre"),
+ ("America/Porto_Velho", "Porto_Velho"),
+ ("America/Puerto_Rico", "Puerto_Rico"),
+ ("America/Punta_Arenas", "Punta_Arenas"),
+ ("America/Rainy_River", "Rainy_River"),
+ ("America/Rankin_Inlet", "Rankin_Inlet"),
+ ("America/Recife", "Recife"),
+ ("America/Regina", "Regina"),
+ ("America/Resolute", "Resolute"),
+ ("America/Rio_Branco", "Rio_Branco"),
+ ("America/Rosario", "Rosario"),
+ ("America/Santa_Isabel", "Santa_Isabel"),
+ ("America/Santarem", "Santarem"),
+ ("America/Santiago", "Santiago"),
+ ("America/Santo_Domingo", "Santo_Domingo"),
+ ("America/Sao_Paulo", "Sao_Paulo"),
+ ("America/Scoresbysund", "Scoresbysund"),
+ ("America/Shiprock", "Shiprock"),
+ ("America/Sitka", "Sitka"),
+ ("America/St_Barthelemy", "St_Barthelemy"),
+ ("America/St_Johns", "St_Johns"),
+ ("America/St_Kitts", "St_Kitts"),
+ ("America/St_Lucia", "St_Lucia"),
+ ("America/St_Thomas", "St_Thomas"),
+ ("America/St_Vincent", "St_Vincent"),
+ ("America/Swift_Current", "Swift_Current"),
+ ("America/Tegucigalpa", "Tegucigalpa"),
+ ("America/Thule", "Thule"),
+ ("America/Thunder_Bay", "Thunder_Bay"),
+ ("America/Tijuana", "Tijuana"),
+ ("America/Toronto", "Toronto"),
+ ("America/Tortola", "Tortola"),
+ ("America/Vancouver", "Vancouver"),
+ ("America/Virgin", "Virgin"),
+ ("America/Whitehorse", "Whitehorse"),
+ ("America/Winnipeg", "Winnipeg"),
+ ("America/Yakutat", "Yakutat"),
+ ("America/Yellowknife", "Yellowknife"),
+ ],
+ ),
+ (
+ "Antarctica",
+ [
+ ("Antarctica/Casey", "Casey"),
+ ("Antarctica/Davis", "Davis"),
+ ("Antarctica/DumontDUrville", "DumontDUrville"),
+ ("Antarctica/Macquarie", "Macquarie"),
+ ("Antarctica/Mawson", "Mawson"),
+ ("Antarctica/McMurdo", "McMurdo"),
+ ("Antarctica/Palmer", "Palmer"),
+ ("Antarctica/Rothera", "Rothera"),
+ ("Antarctica/South_Pole", "South_Pole"),
+ ("Antarctica/Syowa", "Syowa"),
+ ("Antarctica/Troll", "Troll"),
+ ("Antarctica/Vostok", "Vostok"),
+ ],
+ ),
+ ("Arctic", [("Arctic/Longyearbyen", "Longyearbyen")]),
+ (
+ "Asia",
+ [
+ ("Asia/Aden", "Aden"),
+ ("Asia/Almaty", "Almaty"),
+ ("Asia/Amman", "Amman"),
+ ("Asia/Anadyr", "Anadyr"),
+ ("Asia/Aqtau", "Aqtau"),
+ ("Asia/Aqtobe", "Aqtobe"),
+ ("Asia/Ashgabat", "Ashgabat"),
+ ("Asia/Ashkhabad", "Ashkhabad"),
+ ("Asia/Atyrau", "Atyrau"),
+ ("Asia/Baghdad", "Baghdad"),
+ ("Asia/Bahrain", "Bahrain"),
+ ("Asia/Baku", "Baku"),
+ ("Asia/Bangkok", "Bangkok"),
+ ("Asia/Barnaul", "Barnaul"),
+ ("Asia/Beirut", "Beirut"),
+ ("Asia/Bishkek", "Bishkek"),
+ ("Asia/Brunei", "Brunei"),
+ ("Asia/Calcutta", "Calcutta"),
+ ("Asia/Chita", "Chita"),
+ ("Asia/Choibalsan", "Choibalsan"),
+ ("Asia/Chongqing", "Chongqing"),
+ ("Asia/Chungking", "Chungking"),
+ ("Asia/Colombo", "Colombo"),
+ ("Asia/Dacca", "Dacca"),
+ ("Asia/Damascus", "Damascus"),
+ ("Asia/Dhaka", "Dhaka"),
+ ("Asia/Dili", "Dili"),
+ ("Asia/Dubai", "Dubai"),
+ ("Asia/Dushanbe", "Dushanbe"),
+ ("Asia/Famagusta", "Famagusta"),
+ ("Asia/Gaza", "Gaza"),
+ ("Asia/Harbin", "Harbin"),
+ ("Asia/Hebron", "Hebron"),
+ ("Asia/Ho_Chi_Minh", "Ho_Chi_Minh"),
+ ("Asia/Hong_Kong", "Hong_Kong"),
+ ("Asia/Hovd", "Hovd"),
+ ("Asia/Irkutsk", "Irkutsk"),
+ ("Asia/Istanbul", "Istanbul"),
+ ("Asia/Jakarta", "Jakarta"),
+ ("Asia/Jayapura", "Jayapura"),
+ ("Asia/Jerusalem", "Jerusalem"),
+ ("Asia/Kabul", "Kabul"),
+ ("Asia/Kamchatka", "Kamchatka"),
+ ("Asia/Karachi", "Karachi"),
+ ("Asia/Kashgar", "Kashgar"),
+ ("Asia/Kathmandu", "Kathmandu"),
+ ("Asia/Katmandu", "Katmandu"),
+ ("Asia/Khandyga", "Khandyga"),
+ ("Asia/Kolkata", "Kolkata"),
+ ("Asia/Krasnoyarsk", "Krasnoyarsk"),
+ ("Asia/Kuala_Lumpur", "Kuala_Lumpur"),
+ ("Asia/Kuching", "Kuching"),
+ ("Asia/Kuwait", "Kuwait"),
+ ("Asia/Macao", "Macao"),
+ ("Asia/Macau", "Macau"),
+ ("Asia/Magadan", "Magadan"),
+ ("Asia/Makassar", "Makassar"),
+ ("Asia/Manila", "Manila"),
+ ("Asia/Muscat", "Muscat"),
+ ("Asia/Nicosia", "Nicosia"),
+ ("Asia/Novokuznetsk", "Novokuznetsk"),
+ ("Asia/Novosibirsk", "Novosibirsk"),
+ ("Asia/Omsk", "Omsk"),
+ ("Asia/Oral", "Oral"),
+ ("Asia/Phnom_Penh", "Phnom_Penh"),
+ ("Asia/Pontianak", "Pontianak"),
+ ("Asia/Pyongyang", "Pyongyang"),
+ ("Asia/Qatar", "Qatar"),
+ ("Asia/Qostanay", "Qostanay"),
+ ("Asia/Qyzylorda", "Qyzylorda"),
+ ("Asia/Rangoon", "Rangoon"),
+ ("Asia/Riyadh", "Riyadh"),
+ ("Asia/Saigon", "Saigon"),
+ ("Asia/Sakhalin", "Sakhalin"),
+ ("Asia/Samarkand", "Samarkand"),
+ ("Asia/Seoul", "Seoul"),
+ ("Asia/Shanghai", "Shanghai"),
+ ("Asia/Singapore", "Singapore"),
+ ("Asia/Srednekolymsk", "Srednekolymsk"),
+ ("Asia/Taipei", "Taipei"),
+ ("Asia/Tashkent", "Tashkent"),
+ ("Asia/Tbilisi", "Tbilisi"),
+ ("Asia/Tehran", "Tehran"),
+ ("Asia/Tel_Aviv", "Tel_Aviv"),
+ ("Asia/Thimbu", "Thimbu"),
+ ("Asia/Thimphu", "Thimphu"),
+ ("Asia/Tokyo", "Tokyo"),
+ ("Asia/Tomsk", "Tomsk"),
+ ("Asia/Ujung_Pandang", "Ujung_Pandang"),
+ ("Asia/Ulaanbaatar", "Ulaanbaatar"),
+ ("Asia/Ulan_Bator", "Ulan_Bator"),
+ ("Asia/Urumqi", "Urumqi"),
+ ("Asia/Ust-Nera", "Ust-Nera"),
+ ("Asia/Vientiane", "Vientiane"),
+ ("Asia/Vladivostok", "Vladivostok"),
+ ("Asia/Yakutsk", "Yakutsk"),
+ ("Asia/Yangon", "Yangon"),
+ ("Asia/Yekaterinburg", "Yekaterinburg"),
+ ("Asia/Yerevan", "Yerevan"),
+ ],
+ ),
+ (
+ "Atlantic",
+ [
+ ("Atlantic/Azores", "Azores"),
+ ("Atlantic/Bermuda", "Bermuda"),
+ ("Atlantic/Canary", "Canary"),
+ ("Atlantic/Cape_Verde", "Cape_Verde"),
+ ("Atlantic/Faeroe", "Faeroe"),
+ ("Atlantic/Faroe", "Faroe"),
+ ("Atlantic/Jan_Mayen", "Jan_Mayen"),
+ ("Atlantic/Madeira", "Madeira"),
+ ("Atlantic/Reykjavik", "Reykjavik"),
+ ("Atlantic/South_Georgia", "South_Georgia"),
+ ("Atlantic/St_Helena", "St_Helena"),
+ ("Atlantic/Stanley", "Stanley"),
+ ],
+ ),
+ (
+ "Australia",
+ [
+ ("Australia/ACT", "ACT"),
+ ("Australia/Adelaide", "Adelaide"),
+ ("Australia/Brisbane", "Brisbane"),
+ ("Australia/Broken_Hill", "Broken_Hill"),
+ ("Australia/Canberra", "Canberra"),
+ ("Australia/Currie", "Currie"),
+ ("Australia/Darwin", "Darwin"),
+ ("Australia/Eucla", "Eucla"),
+ ("Australia/Hobart", "Hobart"),
+ ("Australia/LHI", "LHI"),
+ ("Australia/Lindeman", "Lindeman"),
+ ("Australia/Lord_Howe", "Lord_Howe"),
+ ("Australia/Melbourne", "Melbourne"),
+ ("Australia/NSW", "NSW"),
+ ("Australia/North", "North"),
+ ("Australia/Perth", "Perth"),
+ ("Australia/Queensland", "Queensland"),
+ ("Australia/South", "South"),
+ ("Australia/Sydney", "Sydney"),
+ ("Australia/Tasmania", "Tasmania"),
+ ("Australia/Victoria", "Victoria"),
+ ("Australia/West", "West"),
+ ("Australia/Yancowinna", "Yancowinna"),
+ ],
+ ),
+ (
+ "Brazil",
+ [
+ ("Brazil/Acre", "Acre"),
+ ("Brazil/DeNoronha", "DeNoronha"),
+ ("Brazil/East", "East"),
+ ("Brazil/West", "West"),
+ ],
+ ),
+ (
+ "Canada",
+ [
+ ("Canada/Atlantic", "Atlantic"),
+ ("Canada/Central", "Central"),
+ ("Canada/Eastern", "Eastern"),
+ ("Canada/Mountain", "Mountain"),
+ ("Canada/Newfoundland", "Newfoundland"),
+ ("Canada/Pacific", "Pacific"),
+ ("Canada/Saskatchewan", "Saskatchewan"),
+ ("Canada/Yukon", "Yukon"),
+ ],
+ ),
+ (
+ "Chile",
+ [
+ ("Chile/Continental", "Continental"),
+ ("Chile/EasterIsland", "EasterIsland"),
+ ],
+ ),
+ (
+ "Etc",
+ [
+ ("Etc/Greenwich", "Greenwich"),
+ ("Etc/UCT", "UCT"),
+ ("Etc/UTC", "UTC"),
+ ("Etc/Universal", "Universal"),
+ ("Etc/Zulu", "Zulu"),
+ ],
+ ),
+ (
+ "Europe",
+ [
+ ("Europe/Amsterdam", "Amsterdam"),
+ ("Europe/Andorra", "Andorra"),
+ ("Europe/Astrakhan", "Astrakhan"),
+ ("Europe/Athens", "Athens"),
+ ("Europe/Belfast", "Belfast"),
+ ("Europe/Belgrade", "Belgrade"),
+ ("Europe/Berlin", "Berlin"),
+ ("Europe/Bratislava", "Bratislava"),
+ ("Europe/Brussels", "Brussels"),
+ ("Europe/Bucharest", "Bucharest"),
+ ("Europe/Budapest", "Budapest"),
+ ("Europe/Busingen", "Busingen"),
+ ("Europe/Chisinau", "Chisinau"),
+ ("Europe/Copenhagen", "Copenhagen"),
+ ("Europe/Dublin", "Dublin"),
+ ("Europe/Gibraltar", "Gibraltar"),
+ ("Europe/Guernsey", "Guernsey"),
+ ("Europe/Helsinki", "Helsinki"),
+ ("Europe/Isle_of_Man", "Isle_of_Man"),
+ ("Europe/Istanbul", "Istanbul"),
+ ("Europe/Jersey", "Jersey"),
+ ("Europe/Kaliningrad", "Kaliningrad"),
+ ("Europe/Kiev", "Kiev"),
+ ("Europe/Kirov", "Kirov"),
+ ("Europe/Kyiv", "Kyiv"),
+ ("Europe/Lisbon", "Lisbon"),
+ ("Europe/Ljubljana", "Ljubljana"),
+ ("Europe/London", "London"),
+ ("Europe/Luxembourg", "Luxembourg"),
+ ("Europe/Madrid", "Madrid"),
+ ("Europe/Malta", "Malta"),
+ ("Europe/Mariehamn", "Mariehamn"),
+ ("Europe/Minsk", "Minsk"),
+ ("Europe/Monaco", "Monaco"),
+ ("Europe/Moscow", "Moscow"),
+ ("Europe/Nicosia", "Nicosia"),
+ ("Europe/Oslo", "Oslo"),
+ ("Europe/Paris", "Paris"),
+ ("Europe/Podgorica", "Podgorica"),
+ ("Europe/Prague", "Prague"),
+ ("Europe/Riga", "Riga"),
+ ("Europe/Rome", "Rome"),
+ ("Europe/Samara", "Samara"),
+ ("Europe/San_Marino", "San_Marino"),
+ ("Europe/Sarajevo", "Sarajevo"),
+ ("Europe/Saratov", "Saratov"),
+ ("Europe/Simferopol", "Simferopol"),
+ ("Europe/Skopje", "Skopje"),
+ ("Europe/Sofia", "Sofia"),
+ ("Europe/Stockholm", "Stockholm"),
+ ("Europe/Tallinn", "Tallinn"),
+ ("Europe/Tirane", "Tirane"),
+ ("Europe/Tiraspol", "Tiraspol"),
+ ("Europe/Ulyanovsk", "Ulyanovsk"),
+ ("Europe/Uzhgorod", "Uzhgorod"),
+ ("Europe/Vaduz", "Vaduz"),
+ ("Europe/Vatican", "Vatican"),
+ ("Europe/Vienna", "Vienna"),
+ ("Europe/Vilnius", "Vilnius"),
+ ("Europe/Volgograd", "Volgograd"),
+ ("Europe/Warsaw", "Warsaw"),
+ ("Europe/Zagreb", "Zagreb"),
+ ("Europe/Zaporozhye", "Zaporozhye"),
+ ("Europe/Zurich", "Zurich"),
+ ],
+ ),
+ (
+ "Indian",
+ [
+ ("Indian/Antananarivo", "Antananarivo"),
+ ("Indian/Chagos", "Chagos"),
+ ("Indian/Christmas", "Christmas"),
+ ("Indian/Cocos", "Cocos"),
+ ("Indian/Comoro", "Comoro"),
+ ("Indian/Kerguelen", "Kerguelen"),
+ ("Indian/Mahe", "Mahe"),
+ ("Indian/Maldives", "Maldives"),
+ ("Indian/Mauritius", "Mauritius"),
+ ("Indian/Mayotte", "Mayotte"),
+ ("Indian/Reunion", "Reunion"),
+ ],
+ ),
+ (
+ "Mexico",
+ [
+ ("Mexico/BajaNorte", "BajaNorte"),
+ ("Mexico/BajaSur", "BajaSur"),
+ ("Mexico/General", "General"),
+ ],
+ ),
+ (
+ "Other",
+ [
+ ("CET", "CET"),
+ ("CST6CDT", "CST6CDT"),
+ ("Cuba", "Cuba"),
+ ("EET", "EET"),
+ ("EST", "EST"),
+ ("EST5EDT", "EST5EDT"),
+ ("Egypt", "Egypt"),
+ ("Eire", "Eire"),
+ ("GB", "GB"),
+ ("GB-Eire", "GB-Eire"),
+ ("Greenwich", "Greenwich"),
+ ("HST", "HST"),
+ ("Hongkong", "Hongkong"),
+ ("Iceland", "Iceland"),
+ ("Iran", "Iran"),
+ ("Israel", "Israel"),
+ ("Jamaica", "Jamaica"),
+ ("Japan", "Japan"),
+ ("Kwajalein", "Kwajalein"),
+ ("Libya", "Libya"),
+ ("MET", "MET"),
+ ("MST", "MST"),
+ ("MST7MDT", "MST7MDT"),
+ ("NZ", "NZ"),
+ ("NZ-CHAT", "NZ-CHAT"),
+ ("Navajo", "Navajo"),
+ ("PRC", "PRC"),
+ ("PST8PDT", "PST8PDT"),
+ ("Poland", "Poland"),
+ ("Portugal", "Portugal"),
+ ("ROC", "ROC"),
+ ("ROK", "ROK"),
+ ("Singapore", "Singapore"),
+ ("Turkey", "Turkey"),
+ ("UCT", "UCT"),
+ ("UTC", "UTC"),
+ ("Universal", "Universal"),
+ ("W-SU", "W-SU"),
+ ("WET", "WET"),
+ ("Zulu", "Zulu"),
+ ],
+ ),
+ (
+ "Pacific",
+ [
+ ("Pacific/Apia", "Apia"),
+ ("Pacific/Auckland", "Auckland"),
+ ("Pacific/Bougainville", "Bougainville"),
+ ("Pacific/Chatham", "Chatham"),
+ ("Pacific/Chuuk", "Chuuk"),
+ ("Pacific/Easter", "Easter"),
+ ("Pacific/Efate", "Efate"),
+ ("Pacific/Enderbury", "Enderbury"),
+ ("Pacific/Fakaofo", "Fakaofo"),
+ ("Pacific/Fiji", "Fiji"),
+ ("Pacific/Funafuti", "Funafuti"),
+ ("Pacific/Galapagos", "Galapagos"),
+ ("Pacific/Gambier", "Gambier"),
+ ("Pacific/Guadalcanal", "Guadalcanal"),
+ ("Pacific/Guam", "Guam"),
+ ("Pacific/Honolulu", "Honolulu"),
+ ("Pacific/Johnston", "Johnston"),
+ ("Pacific/Kanton", "Kanton"),
+ ("Pacific/Kiritimati", "Kiritimati"),
+ ("Pacific/Kosrae", "Kosrae"),
+ ("Pacific/Kwajalein", "Kwajalein"),
+ ("Pacific/Majuro", "Majuro"),
+ ("Pacific/Marquesas", "Marquesas"),
+ ("Pacific/Midway", "Midway"),
+ ("Pacific/Nauru", "Nauru"),
+ ("Pacific/Niue", "Niue"),
+ ("Pacific/Norfolk", "Norfolk"),
+ ("Pacific/Noumea", "Noumea"),
+ ("Pacific/Pago_Pago", "Pago_Pago"),
+ ("Pacific/Palau", "Palau"),
+ ("Pacific/Pitcairn", "Pitcairn"),
+ ("Pacific/Pohnpei", "Pohnpei"),
+ ("Pacific/Ponape", "Ponape"),
+ ("Pacific/Port_Moresby", "Port_Moresby"),
+ ("Pacific/Rarotonga", "Rarotonga"),
+ ("Pacific/Saipan", "Saipan"),
+ ("Pacific/Samoa", "Samoa"),
+ ("Pacific/Tahiti", "Tahiti"),
+ ("Pacific/Tarawa", "Tarawa"),
+ ("Pacific/Tongatapu", "Tongatapu"),
+ ("Pacific/Truk", "Truk"),
+ ("Pacific/Wake", "Wake"),
+ ("Pacific/Wallis", "Wallis"),
+ ("Pacific/Yap", "Yap"),
+ ],
+ ),
+ (
+ "US",
+ [
+ ("US/Alaska", "Alaska"),
+ ("US/Aleutian", "Aleutian"),
+ ("US/Arizona", "Arizona"),
+ ("US/Central", "Central"),
+ ("US/East-Indiana", "East-Indiana"),
+ ("US/Eastern", "Eastern"),
+ ("US/Hawaii", "Hawaii"),
+ ("US/Indiana-Starke", "Indiana-Starke"),
+ ("US/Michigan", "Michigan"),
+ ("US/Mountain", "Mountain"),
+ ("US/Pacific", "Pacific"),
+ ("US/Samoa", "Samoa"),
+ ],
+ ),
+ ],
+ default="America/Toronto",
+ max_length=50,
+ verbose_name="time zone",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="profile",
+ name="totp_key",
+ field=judge.models.profile.EncryptedNullCharField(
+ blank=True,
+ help_text="32-character Base32-encoded key for TOTP.",
+ max_length=32,
+ null=True,
+ validators=[
+ django.core.validators.RegexValidator(
+ "^$|^[A-Z2-7]{32}$", "TOTP key must be empty or Base32."
+ )
+ ],
+ verbose_name="TOTP key",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="profile",
+ name="username_display_override",
+ field=models.CharField(
+ blank=True,
+ help_text="Name displayed in place of username.",
+ max_length=100,
+ verbose_name="display name override",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="solution",
+ name="content",
+ field=models.TextField(
+ validators=[judge.models.problem.disallowed_characters_validator],
+ verbose_name="editorial content",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="submission",
+ name="problem",
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="judge.problem",
+ verbose_name="problem",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="submission",
+ name="result",
+ field=models.CharField(
+ blank=True,
+ choices=[
+ ("AC", "Accepted"),
+ ("WA", "Wrong Answer"),
+ ("TLE", "Time Limit Exceeded"),
+ ("MLE", "Memory Limit Exceeded"),
+ ("OLE", "Output Limit Exceeded"),
+ ("IR", "Invalid Return"),
+ ("RTE", "Runtime Error"),
+ ("CE", "Compile Error"),
+ ("IE", "Internal Error"),
+ ("SC", "Short Circuited"),
+ ("AB", "Aborted"),
+ ],
+ db_index=True,
+ default=None,
+ max_length=3,
+ null=True,
+ verbose_name="result",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="submission",
+ name="user",
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="judge.profile",
+ verbose_name="user",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="submissiontestcase",
+ name="status",
+ field=models.CharField(
+ choices=[
+ ("AC", "Accepted"),
+ ("WA", "Wrong Answer"),
+ ("TLE", "Time Limit Exceeded"),
+ ("MLE", "Memory Limit Exceeded"),
+ ("OLE", "Output Limit Exceeded"),
+ ("IR", "Invalid Return"),
+ ("RTE", "Runtime Error"),
+ ("CE", "Compile Error"),
+ ("IE", "Internal Error"),
+ ("SC", "Short Circuited"),
+ ("AB", "Aborted"),
+ ],
+ max_length=3,
+ verbose_name="status flag",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="ticketmessage",
+ name="user",
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="ticket_messages",
+ to="judge.profile",
+ verbose_name="user",
+ ),
),
migrations.AddField(
- model_name='problemdata',
- name='nobigmath',
- field=models.BooleanField(blank=True, null=True, verbose_name='disable biginteger/bigdecimal'),
+ model_name="problemdata",
+ name="nobigmath",
+ field=models.BooleanField(
+ blank=True, null=True, verbose_name="disable biginteger/bigdecimal"
+ ),
),
migrations.AddField(
- model_name='problemdata',
- name='unicode',
- field=models.BooleanField(blank=True, null=True, verbose_name='enable unicode'),
+ model_name="problemdata",
+ name="unicode",
+ field=models.BooleanField(
+ blank=True, null=True, verbose_name="enable unicode"
+ ),
),
]
diff --git a/judge/migrations/0134_add_voting_functionality.py b/judge/migrations/0134_add_voting_functionality.py
index c622304ea2..33f63e4593 100644
--- a/judge/migrations/0134_add_voting_functionality.py
+++ b/judge/migrations/0134_add_voting_functionality.py
@@ -4,41 +4,83 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0133_add_problem_data_hints'),
+ ("judge", "0133_add_problem_data_hints"),
]
operations = [
migrations.AddField(
- model_name='profile',
- name='is_banned_from_problem_voting',
- field=models.BooleanField(default=False,
- help_text="User will not be able to vote on problems' point values.",
- verbose_name='banned from voting on problem point values'),
+ model_name="profile",
+ name="is_banned_from_problem_voting",
+ field=models.BooleanField(
+ default=False,
+ help_text="User will not be able to vote on problems' point values.",
+ verbose_name="banned from voting on problem point values",
+ ),
),
migrations.CreateModel(
- name='ProblemPointsVote',
+ name="ProblemPointsVote",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('points', models.IntegerField(help_text='The amount of points the voter thinks this problem deserves.',
- validators=[django.core.validators.MinValueValidator(1),
- django.core.validators.MaxValueValidator(50)],
- verbose_name='proposed points')),
- ('note', models.TextField(blank=True, default='', help_text='Justification for problem point value.',
- max_length=8192, verbose_name='note')),
- ('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
- related_name='problem_points_votes', to='judge.Problem',
- verbose_name='problem')),
- ('voter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
- related_name='problem_points_votes', to='judge.Profile',
- verbose_name='voter')),
- ('vote_time', models.DateTimeField(auto_now_add=True, help_text='The time this vote was cast.',
- verbose_name='vote time')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "points",
+ models.IntegerField(
+ help_text="The amount of points the voter thinks this problem deserves.",
+ validators=[
+ django.core.validators.MinValueValidator(1),
+ django.core.validators.MaxValueValidator(50),
+ ],
+ verbose_name="proposed points",
+ ),
+ ),
+ (
+ "note",
+ models.TextField(
+ blank=True,
+ default="",
+ help_text="Justification for problem point value.",
+ max_length=8192,
+ verbose_name="note",
+ ),
+ ),
+ (
+ "problem",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="problem_points_votes",
+ to="judge.Problem",
+ verbose_name="problem",
+ ),
+ ),
+ (
+ "voter",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="problem_points_votes",
+ to="judge.Profile",
+ verbose_name="voter",
+ ),
+ ),
+ (
+ "vote_time",
+ models.DateTimeField(
+ auto_now_add=True,
+ help_text="The time this vote was cast.",
+ verbose_name="vote time",
+ ),
+ ),
],
options={
- 'verbose_name': 'problem vote',
- 'verbose_name_plural': 'problem votes',
+ "verbose_name": "problem vote",
+ "verbose_name_plural": "problem votes",
},
),
]
diff --git a/judge/migrations/0135_disable_judge.py b/judge/migrations/0135_disable_judge.py
index 5d745feb26..3223998a3c 100644
--- a/judge/migrations/0135_disable_judge.py
+++ b/judge/migrations/0135_disable_judge.py
@@ -4,15 +4,18 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0134_add_voting_functionality'),
+ ("judge", "0134_add_voting_functionality"),
]
operations = [
migrations.AddField(
- model_name='judge',
- name='is_disabled',
- field=models.BooleanField(default=False, help_text='Whether this judge should be removed from judging queue.', verbose_name='disable judge'),
+ model_name="judge",
+ name="is_disabled",
+ field=models.BooleanField(
+ default=False,
+ help_text="Whether this judge should be removed from judging queue.",
+ verbose_name="disable judge",
+ ),
),
]
diff --git a/judge/migrations/0136_remove_zombie_editorials.py b/judge/migrations/0136_remove_zombie_editorials.py
index 418c16ee5b..002c429211 100644
--- a/judge/migrations/0136_remove_zombie_editorials.py
+++ b/judge/migrations/0136_remove_zombie_editorials.py
@@ -3,21 +3,26 @@
def delete_null_solutions(apps, scheme_editor):
- model = apps.get_model('judge', 'Solution')
+ model = apps.get_model("judge", "Solution")
model.objects.filter(problem=None).delete()
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0135_disable_judge'),
+ ("judge", "0135_disable_judge"),
]
operations = [
migrations.RunPython(delete_null_solutions),
migrations.AlterField(
- model_name='solution',
- name='problem',
- field=models.OneToOneField(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='solution', to='judge.Problem', verbose_name='associated problem'),
+ model_name="solution",
+ name="problem",
+ field=models.OneToOneField(
+ blank=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="solution",
+ to="judge.Problem",
+ verbose_name="associated problem",
+ ),
),
]
diff --git a/judge/migrations/0137_profile_site_theme.py b/judge/migrations/0137_profile_site_theme.py
index 5cf2ba411e..8d822e6c54 100644
--- a/judge/migrations/0137_profile_site_theme.py
+++ b/judge/migrations/0137_profile_site_theme.py
@@ -4,15 +4,23 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0136_remove_zombie_editorials'),
+ ("judge", "0136_remove_zombie_editorials"),
]
operations = [
migrations.AddField(
- model_name='profile',
- name='site_theme',
- field=models.CharField(choices=[('auto', 'Follow system default'), ('light', 'Light'), ('dark', 'Dark')], default='auto', max_length=10, verbose_name='site theme'),
+ model_name="profile",
+ name="site_theme",
+ field=models.CharField(
+ choices=[
+ ("auto", "Follow system default"),
+ ("light", "Light"),
+ ("dark", "Dark"),
+ ],
+ default="auto",
+ max_length=10,
+ verbose_name="site theme",
+ ),
),
]
diff --git a/judge/migrations/0138_dark_ace_theme.py b/judge/migrations/0138_dark_ace_theme.py
index 110e873f82..14c89dc5a7 100644
--- a/judge/migrations/0138_dark_ace_theme.py
+++ b/judge/migrations/0138_dark_ace_theme.py
@@ -4,26 +4,64 @@
def github_to_auto(apps, schema_editor):
- Profile = apps.get_model('judge', 'Profile')
- Profile.objects.filter(ace_theme='github').update(ace_theme='auto')
+ Profile = apps.get_model("judge", "Profile")
+ Profile.objects.filter(ace_theme="github").update(ace_theme="auto")
def auto_to_github(apps, schema_editor):
- Profile = apps.get_model('judge', 'Profile')
- Profile.objects.filter(ace_theme='auto').update(ace_theme='github')
+ Profile = apps.get_model("judge", "Profile")
+ Profile.objects.filter(ace_theme="auto").update(ace_theme="github")
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0137_profile_site_theme'),
+ ("judge", "0137_profile_site_theme"),
]
operations = [
migrations.AlterField(
- model_name='profile',
- name='ace_theme',
- field=models.CharField(choices=[('auto', 'Follow theme default'), ('ambiance', 'Ambiance'), ('chaos', 'Chaos'), ('chrome', 'Chrome'), ('clouds', 'Clouds'), ('clouds_midnight', 'Clouds Midnight'), ('cobalt', 'Cobalt'), ('crimson_editor', 'Crimson Editor'), ('dawn', 'Dawn'), ('dreamweaver', 'Dreamweaver'), ('eclipse', 'Eclipse'), ('github', 'Github'), ('idle_fingers', 'Idle Fingers'), ('katzenmilch', 'Katzenmilch'), ('kr_theme', 'KR Theme'), ('kuroir', 'Kuroir'), ('merbivore', 'Merbivore'), ('merbivore_soft', 'Merbivore Soft'), ('mono_industrial', 'Mono Industrial'), ('monokai', 'Monokai'), ('pastel_on_dark', 'Pastel on Dark'), ('solarized_dark', 'Solarized Dark'), ('solarized_light', 'Solarized Light'), ('terminal', 'Terminal'), ('textmate', 'Textmate'), ('tomorrow', 'Tomorrow'), ('tomorrow_night', 'Tomorrow Night'), ('tomorrow_night_blue', 'Tomorrow Night Blue'), ('tomorrow_night_bright', 'Tomorrow Night Bright'), ('tomorrow_night_eighties', 'Tomorrow Night Eighties'), ('twilight', 'Twilight'), ('vibrant_ink', 'Vibrant Ink'), ('xcode', 'XCode')], default='auto', max_length=30, verbose_name='Ace theme'),
+ model_name="profile",
+ name="ace_theme",
+ field=models.CharField(
+ choices=[
+ ("auto", "Follow theme default"),
+ ("ambiance", "Ambiance"),
+ ("chaos", "Chaos"),
+ ("chrome", "Chrome"),
+ ("clouds", "Clouds"),
+ ("clouds_midnight", "Clouds Midnight"),
+ ("cobalt", "Cobalt"),
+ ("crimson_editor", "Crimson Editor"),
+ ("dawn", "Dawn"),
+ ("dreamweaver", "Dreamweaver"),
+ ("eclipse", "Eclipse"),
+ ("github", "Github"),
+ ("idle_fingers", "Idle Fingers"),
+ ("katzenmilch", "Katzenmilch"),
+ ("kr_theme", "KR Theme"),
+ ("kuroir", "Kuroir"),
+ ("merbivore", "Merbivore"),
+ ("merbivore_soft", "Merbivore Soft"),
+ ("mono_industrial", "Mono Industrial"),
+ ("monokai", "Monokai"),
+ ("pastel_on_dark", "Pastel on Dark"),
+ ("solarized_dark", "Solarized Dark"),
+ ("solarized_light", "Solarized Light"),
+ ("terminal", "Terminal"),
+ ("textmate", "Textmate"),
+ ("tomorrow", "Tomorrow"),
+ ("tomorrow_night", "Tomorrow Night"),
+ ("tomorrow_night_blue", "Tomorrow Night Blue"),
+ ("tomorrow_night_bright", "Tomorrow Night Bright"),
+ ("tomorrow_night_eighties", "Tomorrow Night Eighties"),
+ ("twilight", "Twilight"),
+ ("vibrant_ink", "Vibrant Ink"),
+ ("xcode", "XCode"),
+ ],
+ default="auto",
+ max_length=30,
+ verbose_name="Ace theme",
+ ),
),
migrations.RunPython(github_to_auto, auto_to_github, atomic=True),
]
diff --git a/judge/migrations/0139_user_index_refactor.py b/judge/migrations/0139_user_index_refactor.py
index eda597e2d7..1994a73035 100644
--- a/judge/migrations/0139_user_index_refactor.py
+++ b/judge/migrations/0139_user_index_refactor.py
@@ -4,37 +4,44 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0138_dark_ace_theme'),
+ ("judge", "0138_dark_ace_theme"),
]
operations = [
migrations.AlterField(
- model_name='profile',
- name='performance_points',
+ model_name="profile",
+ name="performance_points",
field=models.FloatField(default=0),
),
migrations.AlterField(
- model_name='profile',
- name='points',
+ model_name="profile",
+ name="points",
field=models.FloatField(default=0),
),
migrations.AlterField(
- model_name='profile',
- name='problem_count',
+ model_name="profile",
+ name="problem_count",
field=models.IntegerField(default=0),
),
migrations.AddIndex(
- model_name='profile',
- index=models.Index(fields=['is_unlisted', '-performance_points'], name='judge_profi_is_unli_1410d8_idx'),
+ model_name="profile",
+ index=models.Index(
+ fields=["is_unlisted", "-performance_points"],
+ name="judge_profi_is_unli_1410d8_idx",
+ ),
),
migrations.AddIndex(
- model_name='profile',
- index=models.Index(fields=['is_unlisted', '-rating'], name='judge_profi_is_unli_bcf16a_idx'),
+ model_name="profile",
+ index=models.Index(
+ fields=["is_unlisted", "-rating"], name="judge_profi_is_unli_bcf16a_idx"
+ ),
),
migrations.AddIndex(
- model_name='profile',
- index=models.Index(fields=['is_unlisted', '-problem_count'], name='judge_profi_is_unli_171bf3_idx'),
+ model_name="profile",
+ index=models.Index(
+ fields=["is_unlisted", "-problem_count"],
+ name="judge_profi_is_unli_171bf3_idx",
+ ),
),
]
diff --git a/judge/migrations/0140_submission_index_refactor.py b/judge/migrations/0140_submission_index_refactor.py
index 230c957c95..d0b9b0f56a 100644
--- a/judge/migrations/0140_submission_index_refactor.py
+++ b/judge/migrations/0140_submission_index_refactor.py
@@ -4,41 +4,69 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0139_user_index_refactor'),
+ ("judge", "0139_user_index_refactor"),
]
operations = [
migrations.AlterField(
- model_name='submission',
- name='points',
- field=models.FloatField(null=True, verbose_name='points granted'),
+ model_name="submission",
+ name="points",
+ field=models.FloatField(null=True, verbose_name="points granted"),
),
migrations.AlterField(
- model_name='submission',
- name='result',
- field=models.CharField(blank=True, choices=[('AC', 'Accepted'), ('WA', 'Wrong Answer'), ('TLE', 'Time Limit Exceeded'), ('MLE', 'Memory Limit Exceeded'), ('OLE', 'Output Limit Exceeded'), ('IR', 'Invalid Return'), ('RTE', 'Runtime Error'), ('CE', 'Compile Error'), ('IE', 'Internal Error'), ('SC', 'Short Circuited'), ('AB', 'Aborted')], default=None, max_length=3, null=True, verbose_name='result'),
+ model_name="submission",
+ name="result",
+ field=models.CharField(
+ blank=True,
+ choices=[
+ ("AC", "Accepted"),
+ ("WA", "Wrong Answer"),
+ ("TLE", "Time Limit Exceeded"),
+ ("MLE", "Memory Limit Exceeded"),
+ ("OLE", "Output Limit Exceeded"),
+ ("IR", "Invalid Return"),
+ ("RTE", "Runtime Error"),
+ ("CE", "Compile Error"),
+ ("IE", "Internal Error"),
+ ("SC", "Short Circuited"),
+ ("AB", "Aborted"),
+ ],
+ default=None,
+ max_length=3,
+ null=True,
+ verbose_name="result",
+ ),
),
migrations.AlterField(
- model_name='submission',
- name='time',
- field=models.FloatField(null=True, verbose_name='execution time'),
+ model_name="submission",
+ name="time",
+ field=models.FloatField(null=True, verbose_name="execution time"),
),
migrations.AddIndex(
- model_name='submission',
- index=models.Index(fields=['problem', 'user', '-points', '-time'], name='judge_submi_problem_8d5e0a_idx'),
+ model_name="submission",
+ index=models.Index(
+ fields=["problem", "user", "-points", "-time"],
+ name="judge_submi_problem_8d5e0a_idx",
+ ),
),
migrations.AddIndex(
- model_name='submission',
- index=models.Index(fields=['result', '-id'], name='judge_submi_result_7a005c_idx'),
+ model_name="submission",
+ index=models.Index(
+ fields=["result", "-id"], name="judge_submi_result_7a005c_idx"
+ ),
),
migrations.AddIndex(
- model_name='submission',
- index=models.Index(fields=['result', 'language', '-id'], name='judge_submi_result_ba9a62_idx'),
+ model_name="submission",
+ index=models.Index(
+ fields=["result", "language", "-id"],
+ name="judge_submi_result_ba9a62_idx",
+ ),
),
migrations.AddIndex(
- model_name='submission',
- index=models.Index(fields=['language', '-id'], name='judge_submi_languag_dfe850_idx'),
+ model_name="submission",
+ index=models.Index(
+ fields=["language", "-id"], name="judge_submi_languag_dfe850_idx"
+ ),
),
]
diff --git a/judge/migrations/0141_submission_extra_index.py b/judge/migrations/0141_submission_extra_index.py
index e32dbfcca0..93946b9adf 100644
--- a/judge/migrations/0141_submission_extra_index.py
+++ b/judge/migrations/0141_submission_extra_index.py
@@ -4,30 +4,41 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0140_submission_index_refactor'),
+ ("judge", "0140_submission_index_refactor"),
]
operations = [
migrations.AddIndex(
- model_name='submission',
- index=models.Index(fields=['result', 'problem'], name='judge_submi_result_a77e42_idx'),
+ model_name="submission",
+ index=models.Index(
+ fields=["result", "problem"], name="judge_submi_result_a77e42_idx"
+ ),
),
migrations.AddIndex(
- model_name='submission',
- index=models.Index(fields=['language', 'problem', 'result'], name='judge_submi_languag_380ab4_idx'),
+ model_name="submission",
+ index=models.Index(
+ fields=["language", "problem", "result"],
+ name="judge_submi_languag_380ab4_idx",
+ ),
),
migrations.AddIndex(
- model_name='submission',
- index=models.Index(fields=['problem', 'result'], name='judge_submi_problem_49f8ec_idx'),
+ model_name="submission",
+ index=models.Index(
+ fields=["problem", "result"], name="judge_submi_problem_49f8ec_idx"
+ ),
),
migrations.AddIndex(
- model_name='submission',
- index=models.Index(fields=['user', 'problem', 'result'], name='judge_submi_user_id_650db3_idx'),
+ model_name="submission",
+ index=models.Index(
+ fields=["user", "problem", "result"],
+ name="judge_submi_user_id_650db3_idx",
+ ),
),
migrations.AddIndex(
- model_name='submission',
- index=models.Index(fields=['user', 'result'], name='judge_submi_user_id_ec9a4b_idx'),
+ model_name="submission",
+ index=models.Index(
+ fields=["user", "result"], name="judge_submi_user_id_ec9a4b_idx"
+ ),
),
]
diff --git a/judge/migrations/0142_comment_revision_count.py b/judge/migrations/0142_comment_revision_count.py
index 370956b328..855e68c517 100644
--- a/judge/migrations/0142_comment_revision_count.py
+++ b/judge/migrations/0142_comment_revision_count.py
@@ -4,15 +4,16 @@
def populate_revisions(apps, schema_editor):
db_alias = schema_editor.connection.alias
- ContentType = apps.get_model('contenttypes', 'ContentType')
+ ContentType = apps.get_model("contenttypes", "ContentType")
try:
- content_type = ContentType.objects.get(app_label='judge', model='Comment')
+ content_type = ContentType.objects.get(app_label="judge", model="Comment")
except ObjectDoesNotExist:
# If you don't have content types, then you obviously haven't had any edited comments.
# Therefore, it's safe to all revision counts as zero.
pass
else:
- schema_editor.execute("""\
+ schema_editor.execute(
+ """\
UPDATE `judge_comment` INNER JOIN (
SELECT CAST(`reversion_version`.`object_id` AS INT) AS `id`, COUNT(*) AS `count`
FROM `reversion_version`
@@ -21,19 +22,23 @@ def populate_revisions(apps, schema_editor):
GROUP BY 1
) `versions` ON (`judge_comment`.`id` = `versions`.`id`)
SET `judge_comment`.`revisions` = `versions`.`count`;
-""", (content_type.id, db_alias))
+""",
+ (content_type.id, db_alias),
+ )
class Migration(migrations.Migration):
dependencies = [
- ('judge', '0141_submission_extra_index'),
+ ("judge", "0141_submission_extra_index"),
]
operations = [
migrations.AddField(
- model_name='comment',
- name='revisions',
- field=models.IntegerField(default=0, verbose_name='revisions'),
+ model_name="comment",
+ name="revisions",
+ field=models.IntegerField(default=0, verbose_name="revisions"),
+ ),
+ migrations.RunPython(
+ populate_revisions, migrations.RunPython.noop, atomic=False, elidable=True
),
- migrations.RunPython(populate_revisions, migrations.RunPython.noop, atomic=False, elidable=True),
]
diff --git a/judge/migrations/0143_contest_problem_rank_index.py b/judge/migrations/0143_contest_problem_rank_index.py
index 1985712685..28335a5e9e 100644
--- a/judge/migrations/0143_contest_problem_rank_index.py
+++ b/judge/migrations/0143_contest_problem_rank_index.py
@@ -4,14 +4,16 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('judge', '0142_comment_revision_count'),
+ ("judge", "0142_comment_revision_count"),
]
operations = [
migrations.AddIndex(
- model_name='submission',
- index=models.Index(fields=['contest_object', 'problem', 'user', '-points', '-time'], name='judge_submi_contest_59fbe3_idx'),
+ model_name="submission",
+ index=models.Index(
+ fields=["contest_object", "problem", "user", "-points", "-time"],
+ name="judge_submi_contest_59fbe3_idx",
+ ),
),
]
diff --git a/judge/migrations/0144_submission_index_cleanup.py b/judge/migrations/0144_submission_index_cleanup.py
index 778714e621..a4109ccade 100644
--- a/judge/migrations/0144_submission_index_cleanup.py
+++ b/judge/migrations/0144_submission_index_cleanup.py
@@ -4,39 +4,62 @@
class Migration(migrations.Migration):
dependencies = [
- ('judge', '0143_contest_problem_rank_index'),
+ ("judge", "0143_contest_problem_rank_index"),
]
operations = [
migrations.AlterField(
- model_name='submission',
- name='contest_object',
- field=models.ForeignKey(blank=True, db_index=False, null=True, on_delete=django.db.models.deletion.SET_NULL,
- related_name='+', to='judge.contest', verbose_name='contest'),
+ model_name="submission",
+ name="contest_object",
+ field=models.ForeignKey(
+ blank=True,
+ db_index=False,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="+",
+ to="judge.contest",
+ verbose_name="contest",
+ ),
),
migrations.AlterField(
- model_name='submission',
- name='language',
- field=models.ForeignKey(db_index=False, on_delete=django.db.models.deletion.CASCADE, to='judge.language',
- verbose_name='submission language'),
+ model_name="submission",
+ name="language",
+ field=models.ForeignKey(
+ db_index=False,
+ on_delete=django.db.models.deletion.CASCADE,
+ to="judge.language",
+ verbose_name="submission language",
+ ),
),
migrations.AlterField(
- model_name='submission',
- name='problem',
- field=models.ForeignKey(db_index=False, on_delete=django.db.models.deletion.CASCADE, to='judge.problem',
- verbose_name='problem'),
+ model_name="submission",
+ name="problem",
+ field=models.ForeignKey(
+ db_index=False,
+ on_delete=django.db.models.deletion.CASCADE,
+ to="judge.problem",
+ verbose_name="problem",
+ ),
),
migrations.AlterField(
- model_name='submission',
- name='user',
- field=models.ForeignKey(db_index=False, on_delete=django.db.models.deletion.CASCADE, to='judge.profile',
- verbose_name='user'),
+ model_name="submission",
+ name="user",
+ field=models.ForeignKey(
+ db_index=False,
+ on_delete=django.db.models.deletion.CASCADE,
+ to="judge.profile",
+ verbose_name="user",
+ ),
),
migrations.AlterField(
- model_name='submissiontestcase',
- name='submission',
- field=models.ForeignKey(db_index=False, on_delete=django.db.models.deletion.CASCADE,
- related_name='test_cases', to='judge.submission',
- verbose_name='associated submission'),
+ model_name="submissiontestcase",
+ name="submission",
+ field=models.ForeignKey(
+ db_index=False,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="test_cases",
+ to="judge.submission",
+ verbose_name="associated submission",
+ ),
),
]
diff --git a/judge/models/__init__.py b/judge/models/__init__.py
index 11621e5083..7c416564ce 100644
--- a/judge/models/__init__.py
+++ b/judge/models/__init__.py
@@ -1,29 +1,69 @@
from reversion import revisions
-from judge.models.choices import ACE_THEMES, EFFECTIVE_MATH_ENGINES, MATH_ENGINES_CHOICES, TIMEZONE
+from judge.models.choices import (
+ ACE_THEMES,
+ EFFECTIVE_MATH_ENGINES,
+ MATH_ENGINES_CHOICES,
+ TIMEZONE,
+)
from judge.models.comment import Comment, CommentLock, CommentVote
-from judge.models.contest import Contest, ContestMoss, ContestParticipation, ContestProblem, ContestSubmission, \
- ContestTag, Rating
+from judge.models.contest import (
+ Contest,
+ ContestMoss,
+ ContestParticipation,
+ ContestProblem,
+ ContestSubmission,
+ ContestTag,
+ Rating,
+)
from judge.models.interface import BlogPost, MiscConfig, NavigationBar, validate_regex
-from judge.models.problem import LanguageLimit, License, Problem, ProblemClarification, ProblemGroup, \
- ProblemPointsVote, ProblemTranslation, ProblemType, Solution, SubmissionSourceAccess, \
- TranslatedProblemQuerySet
-from judge.models.problem_data import CHECKERS, ProblemData, ProblemTestCase, problem_data_storage, \
- problem_directory_file
-from judge.models.profile import Class, Organization, OrganizationRequest, Profile, WebAuthnCredential
+from judge.models.problem import (
+ LanguageLimit,
+ License,
+ Problem,
+ ProblemClarification,
+ ProblemGroup,
+ ProblemPointsVote,
+ ProblemTranslation,
+ ProblemType,
+ Solution,
+ SubmissionSourceAccess,
+ TranslatedProblemQuerySet,
+)
+from judge.models.problem_data import (
+ CHECKERS,
+ ProblemData,
+ ProblemTestCase,
+ problem_data_storage,
+ problem_directory_file,
+)
+from judge.models.profile import (
+ Class,
+ Organization,
+ OrganizationRequest,
+ Profile,
+ WebAuthnCredential,
+)
from judge.models.runtime import Judge, Language, RuntimeVersion
-from judge.models.submission import SUBMISSION_RESULT, Submission, SubmissionSource, SubmissionTestCase
+from judge.models.submission import (
+ SUBMISSION_RESULT,
+ Submission,
+ SubmissionSource,
+ SubmissionTestCase,
+)
from judge.models.ticket import Ticket, TicketMessage
-revisions.register(Profile, exclude=['points', 'last_access', 'ip', 'rating'])
-revisions.register(Problem, follow=['language_limits'])
+revisions.register(Profile, exclude=["points", "last_access", "ip", "rating"])
+revisions.register(Problem, follow=["language_limits"])
revisions.register(LanguageLimit)
-revisions.register(Contest, follow=['contest_problems'])
+revisions.register(Contest, follow=["contest_problems"])
revisions.register(ContestProblem)
revisions.register(Organization)
revisions.register(BlogPost)
revisions.register(Solution)
-revisions.register(Judge, fields=['name', 'created', 'auth_key', 'description'])
+revisions.register(Judge, fields=["name", "created", "auth_key", "description"])
revisions.register(Language)
-revisions.register(Comment, fields=['author', 'time', 'page', 'score', 'body', 'hidden', 'parent'])
+revisions.register(
+ Comment, fields=["author", "time", "page", "score", "body", "hidden", "parent"]
+)
del revisions
diff --git a/judge/models/choices.py b/judge/models/choices.py
index 0797d59efa..f1c3b8d9c7 100644
--- a/judge/models/choices.py
+++ b/judge/models/choices.py
@@ -8,11 +8,11 @@
def make_timezones():
data = defaultdict(list)
for tz in pytz.all_timezones:
- if '/' in tz:
- area, loc = tz.split('/', 1)
+ if "/" in tz:
+ area, loc = tz.split("/", 1)
else:
- area, loc = 'Other', tz
- if not loc.startswith('GMT'):
+ area, loc = "Other", tz
+ if not loc.startswith("GMT"):
data[area].append((tz, loc))
return sorted(data.items(), key=itemgetter(0))
@@ -21,53 +21,53 @@ def make_timezones():
del make_timezones
ACE_THEMES = (
- ('auto', _('Follow site theme')),
- ('ambiance', 'Ambiance'),
- ('chaos', 'Chaos'),
- ('chrome', 'Chrome'),
- ('clouds', 'Clouds'),
- ('clouds_midnight', 'Clouds Midnight'),
- ('cobalt', 'Cobalt'),
- ('crimson_editor', 'Crimson Editor'),
- ('dawn', 'Dawn'),
- ('dreamweaver', 'Dreamweaver'),
- ('eclipse', 'Eclipse'),
- ('github', 'Github'),
- ('idle_fingers', 'Idle Fingers'),
- ('katzenmilch', 'Katzenmilch'),
- ('kr_theme', 'KR Theme'),
- ('kuroir', 'Kuroir'),
- ('merbivore', 'Merbivore'),
- ('merbivore_soft', 'Merbivore Soft'),
- ('mono_industrial', 'Mono Industrial'),
- ('monokai', 'Monokai'),
- ('pastel_on_dark', 'Pastel on Dark'),
- ('solarized_dark', 'Solarized Dark'),
- ('solarized_light', 'Solarized Light'),
- ('terminal', 'Terminal'),
- ('textmate', 'Textmate'),
- ('tomorrow', 'Tomorrow'),
- ('tomorrow_night', 'Tomorrow Night'),
- ('tomorrow_night_blue', 'Tomorrow Night Blue'),
- ('tomorrow_night_bright', 'Tomorrow Night Bright'),
- ('tomorrow_night_eighties', 'Tomorrow Night Eighties'),
- ('twilight', 'Twilight'),
- ('vibrant_ink', 'Vibrant Ink'),
- ('xcode', 'XCode'),
+ ("auto", _("Follow site theme")),
+ ("ambiance", "Ambiance"),
+ ("chaos", "Chaos"),
+ ("chrome", "Chrome"),
+ ("clouds", "Clouds"),
+ ("clouds_midnight", "Clouds Midnight"),
+ ("cobalt", "Cobalt"),
+ ("crimson_editor", "Crimson Editor"),
+ ("dawn", "Dawn"),
+ ("dreamweaver", "Dreamweaver"),
+ ("eclipse", "Eclipse"),
+ ("github", "Github"),
+ ("idle_fingers", "Idle Fingers"),
+ ("katzenmilch", "Katzenmilch"),
+ ("kr_theme", "KR Theme"),
+ ("kuroir", "Kuroir"),
+ ("merbivore", "Merbivore"),
+ ("merbivore_soft", "Merbivore Soft"),
+ ("mono_industrial", "Mono Industrial"),
+ ("monokai", "Monokai"),
+ ("pastel_on_dark", "Pastel on Dark"),
+ ("solarized_dark", "Solarized Dark"),
+ ("solarized_light", "Solarized Light"),
+ ("terminal", "Terminal"),
+ ("textmate", "Textmate"),
+ ("tomorrow", "Tomorrow"),
+ ("tomorrow_night", "Tomorrow Night"),
+ ("tomorrow_night_blue", "Tomorrow Night Blue"),
+ ("tomorrow_night_bright", "Tomorrow Night Bright"),
+ ("tomorrow_night_eighties", "Tomorrow Night Eighties"),
+ ("twilight", "Twilight"),
+ ("vibrant_ink", "Vibrant Ink"),
+ ("xcode", "XCode"),
)
MATH_ENGINES_CHOICES = (
- ('tex', _('Leave as LaTeX')),
- ('svg', _('SVG only')),
- ('mml', _('MathML only')),
- ('jax', _('MathJax with SVG fallback')),
- ('auto', _('Detect best quality')),
+ ("tex", _("Leave as LaTeX")),
+ ("svg", _("SVG only")),
+ ("mml", _("MathML only")),
+ ("jax", _("MathJax with SVG fallback")),
+ ("auto", _("Detect best quality")),
)
-EFFECTIVE_MATH_ENGINES = ('svg', 'mml', 'tex', 'jax')
+EFFECTIVE_MATH_ENGINES = ("svg", "mml", "tex", "jax")
SITE_THEMES = (
- ('auto', _('Follow system default')),
- ('light', _('Light')),
- ('dark', _('Dark')),
+ ("auto", _("Follow system default")),
+ ("light", _("Light")),
+ ("dark", _("Dark")),
)
diff --git a/judge/models/comment.py b/judge/models/comment.py
index 0bc2b5b03c..384e5f94b5 100644
--- a/judge/models/comment.py
+++ b/judge/models/comment.py
@@ -17,66 +17,98 @@
from judge.models.profile import Profile
from judge.utils.cachedict import CacheDict
-__all__ = ['Comment', 'CommentLock', 'CommentVote']
+__all__ = ["Comment", "CommentLock", "CommentVote"]
-comment_validator = RegexValidator(r'^[pcs]:[a-z0-9]+$|^b:\d+$',
- _(r'Page code must be ^[pcs]:[a-z0-9]+$|^b:\d+$'))
+comment_validator = RegexValidator(
+ r"^[pcs]:[a-z0-9]+$|^b:\d+$", _(r"Page code must be ^[pcs]:[a-z0-9]+$|^b:\d+$")
+)
class Comment(MPTTModel):
- author = models.ForeignKey(Profile, verbose_name=_('commenter'), on_delete=CASCADE)
- time = models.DateTimeField(verbose_name=_('posted time'), auto_now_add=True)
- page = models.CharField(max_length=30, verbose_name=_('associated page'), db_index=True,
- validators=[comment_validator])
- score = models.IntegerField(verbose_name=_('votes'), default=0)
- body = models.TextField(verbose_name=_('body of comment'), max_length=8192)
- hidden = models.BooleanField(verbose_name=_('hidden'), default=0)
- parent = TreeForeignKey('self', verbose_name=_('parent'), null=True, blank=True, related_name='replies',
- on_delete=CASCADE)
- revisions = models.IntegerField(verbose_name=_('revisions'), default=0)
+ author = models.ForeignKey(Profile, verbose_name=_("commenter"), on_delete=CASCADE)
+ time = models.DateTimeField(verbose_name=_("posted time"), auto_now_add=True)
+ page = models.CharField(
+ max_length=30,
+ verbose_name=_("associated page"),
+ db_index=True,
+ validators=[comment_validator],
+ )
+ score = models.IntegerField(verbose_name=_("votes"), default=0)
+ body = models.TextField(verbose_name=_("body of comment"), max_length=8192)
+ hidden = models.BooleanField(verbose_name=_("hidden"), default=0)
+ parent = TreeForeignKey(
+ "self",
+ verbose_name=_("parent"),
+ null=True,
+ blank=True,
+ related_name="replies",
+ on_delete=CASCADE,
+ )
+ revisions = models.IntegerField(verbose_name=_("revisions"), default=0)
class Meta:
- verbose_name = _('comment')
- verbose_name_plural = _('comments')
+ verbose_name = _("comment")
+ verbose_name_plural = _("comments")
class MPTTMeta:
- order_insertion_by = ['-time']
+ order_insertion_by = ["-time"]
@classmethod
def most_recent(cls, user, n, batch=None):
- queryset = cls.objects.filter(hidden=False).select_related('author__user') \
- .defer('author__about', 'body').order_by('-id')
+ queryset = (
+ cls.objects.filter(hidden=False)
+ .select_related("author__user")
+ .defer("author__about", "body")
+ .order_by("-id")
+ )
- problem_cache = CacheDict(lambda code: Problem.objects.defer('description', 'summary').get(code=code))
- solution_cache = CacheDict(lambda code: Solution.objects.defer('content').get(problem__code=code))
- contest_cache = CacheDict(lambda key: Contest.objects.defer('description').get(key=key))
- blog_cache = CacheDict(lambda id: BlogPost.objects.defer('summary', 'content').get(id=id))
+ problem_cache = CacheDict(
+ lambda code: Problem.objects.defer("description", "summary").get(code=code)
+ )
+ solution_cache = CacheDict(
+ lambda code: Solution.objects.defer("content").get(problem__code=code)
+ )
+ contest_cache = CacheDict(
+ lambda key: Contest.objects.defer("description").get(key=key)
+ )
+ blog_cache = CacheDict(
+ lambda id: BlogPost.objects.defer("summary", "content").get(id=id)
+ )
- problem_access = CacheDict(lambda code: problem_cache[code].is_accessible_by(user))
- solution_access = CacheDict(lambda code: problem_access[code] and solution_cache[code].is_accessible_by(user))
- contest_access = CacheDict(lambda key: contest_cache[key].is_accessible_by(user))
+ problem_access = CacheDict(
+ lambda code: problem_cache[code].is_accessible_by(user)
+ )
+ solution_access = CacheDict(
+ lambda code: problem_access[code]
+ and solution_cache[code].is_accessible_by(user)
+ )
+ contest_access = CacheDict(
+ lambda key: contest_cache[key].is_accessible_by(user)
+ )
blog_access = CacheDict(lambda id: blog_cache[id].can_see(user))
if batch is None:
batch = 2 * n
output = []
for i in itertools.count(0):
- slice = queryset[i * batch:i * batch + batch]
+ slice = queryset[i * batch : i * batch + batch]
if not slice:
break
for comment in slice:
page_key = comment.page[2:]
try:
- if comment.page.startswith('p:'):
+ if comment.page.startswith("p:"):
has_access = problem_access[page_key]
comment.page_title = problem_cache[page_key].name
- elif comment.page.startswith('s:'):
+ elif comment.page.startswith("s:"):
has_access = solution_access[page_key]
- comment.page_title = _('Editorial for %s') % problem_cache[page_key].name
- elif comment.page.startswith('c:'):
+ comment.page_title = (
+ _("Editorial for %s") % problem_cache[page_key].name
+ )
+ elif comment.page.startswith("c:"):
has_access = contest_access[page_key]
comment.page_title = contest_cache[page_key].name
- elif comment.page.startswith('b:'):
+ elif comment.page.startswith("b:"):
has_access = blog_access[page_key]
comment.page_title = blog_cache[page_key].title
else:
@@ -94,40 +126,42 @@ def most_recent(cls, user, n, batch=None):
def link(self):
try:
link = None
- if self.page.startswith('p:'):
- link = reverse('problem_detail', args=(self.page[2:],))
- elif self.page.startswith('c:'):
- link = reverse('contest_view', args=(self.page[2:],))
- elif self.page.startswith('b:'):
- key = 'blog_slug:%s' % self.page[2:]
+ if self.page.startswith("p:"):
+ link = reverse("problem_detail", args=(self.page[2:],))
+ elif self.page.startswith("c:"):
+ link = reverse("contest_view", args=(self.page[2:],))
+ elif self.page.startswith("b:"):
+ key = "blog_slug:%s" % self.page[2:]
slug = cache.get(key)
if slug is None:
try:
slug = BlogPost.objects.get(id=self.page[2:]).slug
except ObjectDoesNotExist:
- slug = ''
+ slug = ""
cache.set(key, slug, 3600)
- link = reverse('blog_post', args=(self.page[2:], slug))
- elif self.page.startswith('s:'):
- link = reverse('problem_editorial', args=(self.page[2:],))
+ link = reverse("blog_post", args=(self.page[2:], slug))
+ elif self.page.startswith("s:"):
+ link = reverse("problem_editorial", args=(self.page[2:],))
except Exception:
- link = 'invalid'
+ link = "invalid"
return link
@classmethod
def get_page_title(cls, page):
try:
- if page.startswith('p:'):
- return Problem.objects.values_list('name', flat=True).get(code=page[2:])
- elif page.startswith('c:'):
- return Contest.objects.values_list('name', flat=True).get(key=page[2:])
- elif page.startswith('b:'):
- return BlogPost.objects.values_list('title', flat=True).get(id=page[2:])
- elif page.startswith('s:'):
- return _('Editorial for %s') % Problem.objects.values_list('name', flat=True).get(code=page[2:])
- return ''
+ if page.startswith("p:"):
+ return Problem.objects.values_list("name", flat=True).get(code=page[2:])
+ elif page.startswith("c:"):
+ return Contest.objects.values_list("name", flat=True).get(key=page[2:])
+ elif page.startswith("b:"):
+ return BlogPost.objects.values_list("title", flat=True).get(id=page[2:])
+ elif page.startswith("s:"):
+ return _("Editorial for %s") % Problem.objects.values_list(
+ "name", flat=True
+ ).get(code=page[2:])
+ return ""
except ObjectDoesNotExist:
- return ''
+ return ""
@cached_property
def page_title(self):
@@ -135,13 +169,15 @@ def page_title(self):
def is_accessible_by(self, user):
try:
- if self.page.startswith('p:'):
+ if self.page.startswith("p:"):
return Problem.objects.get(code=self.page[2:]).is_accessible_by(user)
- elif self.page.startswith('s:'):
- return Solution.objects.get(problem__code=self.page[2:]).is_accessible_by(user)
- elif self.page.startswith('c:'):
+ elif self.page.startswith("s:"):
+ return Solution.objects.get(
+ problem__code=self.page[2:]
+ ).is_accessible_by(user)
+ elif self.page.startswith("c:"):
return Contest.objects.get(key=self.page[2:]).is_accessible_by(user)
- elif self.page.startswith('b:'):
+ elif self.page.startswith("b:"):
return BlogPost.objects.get(id=self.page[2:]).can_see(user)
else:
return True
@@ -149,10 +185,13 @@ def is_accessible_by(self, user):
return False
def get_absolute_url(self):
- return '%s#comment-%d' % (self.link, self.id)
+ return "%s#comment-%d" % (self.link, self.id)
def __str__(self):
- return _('%(page)s by %(user)s') % {'page': self.page, 'user': self.author.user.username}
+ return _("%(page)s by %(user)s") % {
+ "page": self.page,
+ "user": self.author.user.username,
+ }
# Only use this when queried with
# .prefetch_related(Prefetch('votes', queryset=CommentVote.objects.filter(voter_id=profile_id)))
@@ -168,26 +207,28 @@ def __str__(self):
class CommentVote(models.Model):
- voter = models.ForeignKey(Profile, related_name='voted_comments', on_delete=CASCADE)
- comment = models.ForeignKey(Comment, related_name='votes', on_delete=CASCADE)
+ voter = models.ForeignKey(Profile, related_name="voted_comments", on_delete=CASCADE)
+ comment = models.ForeignKey(Comment, related_name="votes", on_delete=CASCADE)
score = models.IntegerField()
class Meta:
- unique_together = ['voter', 'comment']
- verbose_name = _('comment vote')
- verbose_name_plural = _('comment votes')
+ unique_together = ["voter", "comment"]
+ verbose_name = _("comment vote")
+ verbose_name_plural = _("comment votes")
class CommentLock(models.Model):
- page = models.CharField(max_length=30, verbose_name=_('associated page'), db_index=True,
- validators=[comment_validator])
+ page = models.CharField(
+ max_length=30,
+ verbose_name=_("associated page"),
+ db_index=True,
+ validators=[comment_validator],
+ )
class Meta:
- permissions = (
- ('override_comment_lock', _('Override comment lock')),
- )
- verbose_name = _('comment lock')
- verbose_name_plural = _('comment locks')
+ permissions = (("override_comment_lock", _("Override comment lock")),)
+ verbose_name = _("comment lock")
+ verbose_name_plural = _("comment locks")
def __str__(self):
return str(self.page)
diff --git a/judge/models/contest.py b/judge/models/contest.py
index 37f2010a47..8ca3fc5870 100644
--- a/judge/models/contest.py
+++ b/judge/models/contest.py
@@ -16,7 +16,14 @@
from judge.models.submission import Submission
from judge.ratings import rate_contest
-__all__ = ['Contest', 'ContestTag', 'ContestParticipation', 'ContestProblem', 'ContestSubmission', 'Rating']
+__all__ = [
+ "Contest",
+ "ContestTag",
+ "ContestParticipation",
+ "ContestProblem",
+ "ContestSubmission",
+ "Rating",
+]
class MinValueOrNoneValidator(MinValueValidator):
@@ -25,18 +32,28 @@ def compare(self, a, b):
class ContestTag(models.Model):
- color_validator = RegexValidator('^#(?:[A-Fa-f0-9]{3}){1,2}$', _('Invalid colour.'))
-
- name = models.CharField(max_length=20, verbose_name=_('tag name'), unique=True,
- validators=[RegexValidator(r'^[a-z-]+$', message=_('Lowercase letters and hyphens only.'))])
- color = models.CharField(max_length=7, verbose_name=_('tag colour'), validators=[color_validator])
- description = models.TextField(verbose_name=_('tag description'), blank=True)
+ color_validator = RegexValidator("^#(?:[A-Fa-f0-9]{3}){1,2}$", _("Invalid colour."))
+
+ name = models.CharField(
+ max_length=20,
+ verbose_name=_("tag name"),
+ unique=True,
+ validators=[
+ RegexValidator(
+ r"^[a-z-]+$", message=_("Lowercase letters and hyphens only.")
+ )
+ ],
+ )
+ color = models.CharField(
+ max_length=7, verbose_name=_("tag colour"), validators=[color_validator]
+ )
+ description = models.TextField(verbose_name=_("tag description"), blank=True)
def __str__(self):
return self.name
def get_absolute_url(self):
- return reverse('contest_tag', args=[self.name])
+ return reverse("contest_tag", args=[self.name])
@property
def text_color(self, cache={}):
@@ -45,136 +62,296 @@ def text_color(self, cache={}):
r, g, b = [ord(bytes.fromhex(i * 2)) for i in self.color[1:]]
else:
r, g, b = [i for i in bytes.fromhex(self.color[1:])]
- cache[self.color] = '#000' if 299 * r + 587 * g + 144 * b > 140000 else '#fff'
+ cache[self.color] = (
+ "#000" if 299 * r + 587 * g + 144 * b > 140000 else "#fff"
+ )
return cache[self.color]
class Meta:
- verbose_name = _('contest tag')
- verbose_name_plural = _('contest tags')
+ verbose_name = _("contest tag")
+ verbose_name_plural = _("contest tags")
class Contest(models.Model):
- SCOREBOARD_VISIBLE = 'V'
- SCOREBOARD_AFTER_CONTEST = 'C'
- SCOREBOARD_AFTER_PARTICIPATION = 'P'
- SCOREBOARD_HIDDEN = 'H'
+ SCOREBOARD_VISIBLE = "V"
+ SCOREBOARD_AFTER_CONTEST = "C"
+ SCOREBOARD_AFTER_PARTICIPATION = "P"
+ SCOREBOARD_HIDDEN = "H"
SCOREBOARD_VISIBILITY = (
- (SCOREBOARD_VISIBLE, _('Visible')),
- (SCOREBOARD_AFTER_CONTEST, _('Hidden for duration of contest')),
- (SCOREBOARD_AFTER_PARTICIPATION, _('Hidden for duration of participation')),
- (SCOREBOARD_HIDDEN, _('Hidden permanently')),
- )
- key = models.CharField(max_length=20, verbose_name=_('contest id'), unique=True,
- validators=[RegexValidator('^[a-z0-9]+$', _('Contest id must be ^[a-z0-9]+$'))])
- name = models.CharField(max_length=100, verbose_name=_('contest name'), db_index=True)
- authors = models.ManyToManyField(Profile, verbose_name=_('authors'),
- help_text=_('These users will be able to edit the contest.'),
- related_name='authored_contests')
- curators = models.ManyToManyField(Profile, verbose_name=_('curators'),
- help_text=_('These users will be able to edit the contest, '
- 'but will not be listed as authors.'),
- related_name='curated_contests', blank=True)
- testers = models.ManyToManyField(Profile, verbose_name=_('testers'),
- help_text=_('These users will be able to view the contest, but not edit it.'),
- blank=True, related_name='tested_contests')
- tester_see_scoreboard = models.BooleanField(verbose_name=_('testers see scoreboard'), default=False,
- help_text=_('If testers can see the scoreboard.'))
- tester_see_submissions = models.BooleanField(verbose_name=_('testers see submissions'), default=False,
- help_text=_('If testers can see in-contest submissions.'))
- spectators = models.ManyToManyField(Profile, verbose_name=_('spectators'),
- help_text=_('These users will be able to spectate the contest, '
- 'but not see the problems ahead of time.'),
- blank=True, related_name='spectated_contests')
- description = models.TextField(verbose_name=_('description'), blank=True)
- problems = models.ManyToManyField(Problem, verbose_name=_('problems'), through='ContestProblem')
- start_time = models.DateTimeField(verbose_name=_('start time'), db_index=True)
- end_time = models.DateTimeField(verbose_name=_('end time'), db_index=True)
- time_limit = models.DurationField(verbose_name=_('time limit'), blank=True, null=True)
- is_visible = models.BooleanField(verbose_name=_('publicly visible'), default=False,
- help_text=_('Should be set even for organization-private contests, where it '
- 'determines whether the contest is visible to members of the '
- 'specified organizations.'))
- is_rated = models.BooleanField(verbose_name=_('contest rated'), help_text=_('Whether this contest can be rated.'),
- default=False)
- view_contest_scoreboard = models.ManyToManyField(Profile, verbose_name=_('view contest scoreboard'), blank=True,
- related_name='view_contest_scoreboard',
- help_text=_('These users will be able to view the scoreboard.'))
- view_contest_submissions = models.ManyToManyField(Profile, verbose_name=_('can see contest submissions'),
- blank=True, related_name='view_contest_submissions',
- help_text=_('These users will be able '
- 'to see in-contest submissions.'))
- scoreboard_visibility = models.CharField(verbose_name=_('scoreboard visibility'), default=SCOREBOARD_VISIBLE,
- help_text=_('Scoreboard visibility through the duration of the contest.'),
- max_length=1, choices=SCOREBOARD_VISIBILITY)
- use_clarifications = models.BooleanField(verbose_name=_('no comments'),
- help_text=_('Use clarification system instead of comments.'),
- default=True)
- rating_floor = models.IntegerField(verbose_name=_('rating floor'),
- help_text=_('Do not rate users who have a lower rating.'), null=True, blank=True)
- rating_ceiling = models.IntegerField(verbose_name=_('rating ceiling'),
- help_text=_('Do not rate users who have a higher rating.'),
- null=True, blank=True)
- rate_all = models.BooleanField(verbose_name=_('rate all'), help_text=_('Rate all users who joined.'), default=False)
- rate_exclude = models.ManyToManyField(Profile, verbose_name=_('exclude from ratings'), blank=True,
- related_name='rate_exclude+')
- is_private = models.BooleanField(verbose_name=_('private to specific users'), default=False)
- private_contestants = models.ManyToManyField(Profile, blank=True, verbose_name=_('private contestants'),
- help_text=_('If non-empty, only these users may see the contest.'),
- related_name='private_contestants+')
- hide_problem_tags = models.BooleanField(verbose_name=_('hide problem tags'),
- help_text=_('Whether problem tags should be hidden by default.'),
- default=False)
- hide_problem_authors = models.BooleanField(verbose_name=_('hide problem authors'),
- help_text=_('Whether problem authors should be hidden by default.'),
- default=False)
- run_pretests_only = models.BooleanField(verbose_name=_('run pretests only'),
- help_text=_('Whether judges should grade pretests only, versus all '
- 'testcases. Commonly set during a contest, then unset '
- 'prior to rejudging user submissions when the contest ends.'),
- default=False)
- show_short_display = models.BooleanField(verbose_name=_('show short form settings display'),
- help_text=_('Whether to show a section containing contest settings '
- 'on the contest page or not.'),
- default=False)
- is_organization_private = models.BooleanField(verbose_name=_('private to organizations'), default=False)
- organizations = models.ManyToManyField(Organization, blank=True, verbose_name=_('organizations'),
- help_text=_('If non-empty, only these organizations may see the contest.'))
- limit_join_organizations = models.BooleanField(verbose_name=_('limit organizations that can join'), default=False)
- join_organizations = models.ManyToManyField(Organization, blank=True, verbose_name=_('join organizations'),
- help_text=_('If non-empty, only these organizations may join '
- 'the contest.'), related_name='join_only_contests')
- classes = models.ManyToManyField(Class, blank=True, verbose_name=_('classes'),
- help_text=_('If organization private, only these classes may see the contest.'))
- og_image = models.CharField(verbose_name=_('OpenGraph image'), default='', max_length=150, blank=True)
- logo_override_image = models.CharField(verbose_name=_('logo override image'), default='', max_length=150,
- blank=True,
- help_text=_('This image will replace the default site logo for users '
- 'inside the contest.'))
- tags = models.ManyToManyField(ContestTag, verbose_name=_('contest tags'), blank=True, related_name='contests')
- user_count = models.IntegerField(verbose_name=_('the amount of live participants'), default=0)
- summary = models.TextField(blank=True, verbose_name=_('contest summary'),
- help_text=_('Plain-text, shown in meta description tag, e.g. for social media.'))
- access_code = models.CharField(verbose_name=_('access code'), blank=True, default='', max_length=255,
- help_text=_('An optional code to prompt contestants before they are allowed '
- 'to join the contest. Leave it blank to disable.'))
- banned_users = models.ManyToManyField(Profile, verbose_name=_('personae non gratae'), blank=True,
- help_text=_('Bans the selected users from joining this contest.'))
- format_name = models.CharField(verbose_name=_('contest format'), default='default', max_length=32,
- choices=contest_format.choices(), help_text=_('The contest format module to use.'))
- format_config = JSONField(verbose_name=_('contest format configuration'), null=True, blank=True,
- help_text=_('A JSON object to serve as the configuration for the chosen contest format '
- 'module. Leave empty to use None. Exact format depends on the contest format '
- 'selected.'))
- problem_label_script = models.TextField(verbose_name=_('contest problem label script'), blank=True,
- help_text=_('A custom Lua function to generate problem labels. Requires a '
- 'single function with an integer parameter, the zero-indexed '
- 'contest problem index, and returns a string, the label.'))
- locked_after = models.DateTimeField(verbose_name=_('contest lock'), null=True, blank=True,
- help_text=_('Prevent submissions from this contest '
- 'from being rejudged after this date.'))
- points_precision = models.IntegerField(verbose_name=_('precision points'), default=3,
- validators=[MinValueValidator(0), MaxValueValidator(10)],
- help_text=_('Number of digits to round points to.'))
+ (SCOREBOARD_VISIBLE, _("Visible")),
+ (SCOREBOARD_AFTER_CONTEST, _("Hidden for duration of contest")),
+ (SCOREBOARD_AFTER_PARTICIPATION, _("Hidden for duration of participation")),
+ (SCOREBOARD_HIDDEN, _("Hidden permanently")),
+ )
+ key = models.CharField(
+ max_length=20,
+ verbose_name=_("contest id"),
+ unique=True,
+ validators=[RegexValidator("^[a-z0-9]+$", _("Contest id must be ^[a-z0-9]+$"))],
+ )
+ name = models.CharField(
+ max_length=100, verbose_name=_("contest name"), db_index=True
+ )
+ authors = models.ManyToManyField(
+ Profile,
+ verbose_name=_("authors"),
+ help_text=_("These users will be able to edit the contest."),
+ related_name="authored_contests",
+ )
+ curators = models.ManyToManyField(
+ Profile,
+ verbose_name=_("curators"),
+ help_text=_(
+ "These users will be able to edit the contest, "
+ "but will not be listed as authors."
+ ),
+ related_name="curated_contests",
+ blank=True,
+ )
+ testers = models.ManyToManyField(
+ Profile,
+ verbose_name=_("testers"),
+ help_text=_("These users will be able to view the contest, but not edit it."),
+ blank=True,
+ related_name="tested_contests",
+ )
+ tester_see_scoreboard = models.BooleanField(
+ verbose_name=_("testers see scoreboard"),
+ default=False,
+ help_text=_("If testers can see the scoreboard."),
+ )
+ tester_see_submissions = models.BooleanField(
+ verbose_name=_("testers see submissions"),
+ default=False,
+ help_text=_("If testers can see in-contest submissions."),
+ )
+ spectators = models.ManyToManyField(
+ Profile,
+ verbose_name=_("spectators"),
+ help_text=_(
+ "These users will be able to spectate the contest, "
+ "but not see the problems ahead of time."
+ ),
+ blank=True,
+ related_name="spectated_contests",
+ )
+ description = models.TextField(verbose_name=_("description"), blank=True)
+ problems = models.ManyToManyField(
+ Problem, verbose_name=_("problems"), through="ContestProblem"
+ )
+ start_time = models.DateTimeField(verbose_name=_("start time"), db_index=True)
+ end_time = models.DateTimeField(verbose_name=_("end time"), db_index=True)
+ time_limit = models.DurationField(
+ verbose_name=_("time limit"), blank=True, null=True
+ )
+ is_visible = models.BooleanField(
+ verbose_name=_("publicly visible"),
+ default=False,
+ help_text=_(
+ "Should be set even for organization-private contests, where it "
+ "determines whether the contest is visible to members of the "
+ "specified organizations."
+ ),
+ )
+ is_rated = models.BooleanField(
+ verbose_name=_("contest rated"),
+ help_text=_("Whether this contest can be rated."),
+ default=False,
+ )
+ view_contest_scoreboard = models.ManyToManyField(
+ Profile,
+ verbose_name=_("view contest scoreboard"),
+ blank=True,
+ related_name="view_contest_scoreboard",
+ help_text=_("These users will be able to view the scoreboard."),
+ )
+ view_contest_submissions = models.ManyToManyField(
+ Profile,
+ verbose_name=_("can see contest submissions"),
+ blank=True,
+ related_name="view_contest_submissions",
+ help_text=_("These users will be able " "to see in-contest submissions."),
+ )
+ scoreboard_visibility = models.CharField(
+ verbose_name=_("scoreboard visibility"),
+ default=SCOREBOARD_VISIBLE,
+ help_text=_("Scoreboard visibility through the duration of the contest."),
+ max_length=1,
+ choices=SCOREBOARD_VISIBILITY,
+ )
+ use_clarifications = models.BooleanField(
+ verbose_name=_("no comments"),
+ help_text=_("Use clarification system instead of comments."),
+ default=True,
+ )
+ rating_floor = models.IntegerField(
+ verbose_name=_("rating floor"),
+ help_text=_("Do not rate users who have a lower rating."),
+ null=True,
+ blank=True,
+ )
+ rating_ceiling = models.IntegerField(
+ verbose_name=_("rating ceiling"),
+ help_text=_("Do not rate users who have a higher rating."),
+ null=True,
+ blank=True,
+ )
+ rate_all = models.BooleanField(
+ verbose_name=_("rate all"),
+ help_text=_("Rate all users who joined."),
+ default=False,
+ )
+ rate_exclude = models.ManyToManyField(
+ Profile,
+ verbose_name=_("exclude from ratings"),
+ blank=True,
+ related_name="rate_exclude+",
+ )
+ is_private = models.BooleanField(
+ verbose_name=_("private to specific users"), default=False
+ )
+ private_contestants = models.ManyToManyField(
+ Profile,
+ blank=True,
+ verbose_name=_("private contestants"),
+ help_text=_("If non-empty, only these users may see the contest."),
+ related_name="private_contestants+",
+ )
+ hide_problem_tags = models.BooleanField(
+ verbose_name=_("hide problem tags"),
+ help_text=_("Whether problem tags should be hidden by default."),
+ default=False,
+ )
+ hide_problem_authors = models.BooleanField(
+ verbose_name=_("hide problem authors"),
+ help_text=_("Whether problem authors should be hidden by default."),
+ default=False,
+ )
+ run_pretests_only = models.BooleanField(
+ verbose_name=_("run pretests only"),
+ help_text=_(
+ "Whether judges should grade pretests only, versus all "
+ "testcases. Commonly set during a contest, then unset "
+ "prior to rejudging user submissions when the contest ends."
+ ),
+ default=False,
+ )
+ show_short_display = models.BooleanField(
+ verbose_name=_("show short form settings display"),
+ help_text=_(
+ "Whether to show a section containing contest settings "
+ "on the contest page or not."
+ ),
+ default=False,
+ )
+ is_organization_private = models.BooleanField(
+ verbose_name=_("private to organizations"), default=False
+ )
+ organizations = models.ManyToManyField(
+ Organization,
+ blank=True,
+ verbose_name=_("organizations"),
+ help_text=_("If non-empty, only these organizations may see the contest."),
+ )
+ limit_join_organizations = models.BooleanField(
+ verbose_name=_("limit organizations that can join"), default=False
+ )
+ join_organizations = models.ManyToManyField(
+ Organization,
+ blank=True,
+ verbose_name=_("join organizations"),
+ help_text=_("If non-empty, only these organizations may join " "the contest."),
+ related_name="join_only_contests",
+ )
+ classes = models.ManyToManyField(
+ Class,
+ blank=True,
+ verbose_name=_("classes"),
+ help_text=_("If organization private, only these classes may see the contest."),
+ )
+ og_image = models.CharField(
+ verbose_name=_("OpenGraph image"), default="", max_length=150, blank=True
+ )
+ logo_override_image = models.CharField(
+ verbose_name=_("logo override image"),
+ default="",
+ max_length=150,
+ blank=True,
+ help_text=_(
+ "This image will replace the default site logo for users "
+ "inside the contest."
+ ),
+ )
+ tags = models.ManyToManyField(
+ ContestTag, verbose_name=_("contest tags"), blank=True, related_name="contests"
+ )
+ user_count = models.IntegerField(
+ verbose_name=_("the amount of live participants"), default=0
+ )
+ summary = models.TextField(
+ blank=True,
+ verbose_name=_("contest summary"),
+ help_text=_(
+ "Plain-text, shown in meta description tag, e.g. for social media."
+ ),
+ )
+ access_code = models.CharField(
+ verbose_name=_("access code"),
+ blank=True,
+ default="",
+ max_length=255,
+ help_text=_(
+ "An optional code to prompt contestants before they are allowed "
+ "to join the contest. Leave it blank to disable."
+ ),
+ )
+ banned_users = models.ManyToManyField(
+ Profile,
+ verbose_name=_("personae non gratae"),
+ blank=True,
+ help_text=_("Bans the selected users from joining this contest."),
+ )
+ format_name = models.CharField(
+ verbose_name=_("contest format"),
+ default="default",
+ max_length=32,
+ choices=contest_format.choices(),
+ help_text=_("The contest format module to use."),
+ )
+ format_config = JSONField(
+ verbose_name=_("contest format configuration"),
+ null=True,
+ blank=True,
+ help_text=_(
+ "A JSON object to serve as the configuration for the chosen contest format "
+ "module. Leave empty to use None. Exact format depends on the contest format "
+ "selected."
+ ),
+ )
+ problem_label_script = models.TextField(
+ verbose_name=_("contest problem label script"),
+ blank=True,
+ help_text=_(
+ "A custom Lua function to generate problem labels. Requires a "
+ "single function with an integer parameter, the zero-indexed "
+ "contest problem index, and returns a string, the label."
+ ),
+ )
+ locked_after = models.DateTimeField(
+ verbose_name=_("contest lock"),
+ null=True,
+ blank=True,
+ help_text=_(
+ "Prevent submissions from this contest "
+ "from being rejudged after this date."
+ ),
+ )
+ points_precision = models.IntegerField(
+ verbose_name=_("precision points"),
+ default=3,
+ validators=[MinValueValidator(0), MaxValueValidator(10)],
+ help_text=_("Number of digits to round points to."),
+ )
@cached_property
def format_class(self):
@@ -191,13 +368,18 @@ def get_label_for_problem(self):
def DENY_ALL(obj, attr_name, is_setting):
raise AttributeError()
- lua = LuaRuntime(attribute_filter=DENY_ALL, register_eval=False, register_builtins=False)
+
+ lua = LuaRuntime(
+ attribute_filter=DENY_ALL, register_eval=False, register_builtins=False
+ )
return lua.eval(self.problem_label_script)
def clean(self):
# Django will complain if you didn't fill in start_time or end_time, so we don't have to.
if self.start_time and self.end_time and self.start_time >= self.end_time:
- raise ValidationError('What is this? A contest that ended before it starts?')
+ raise ValidationError(
+ "What is this? A contest that ended before it starts?"
+ )
self.format_class.validate(self.format_config)
try:
@@ -205,15 +387,21 @@ def clean(self):
# so test it to see if the script returns a valid label.
label = self.get_label_for_problem(0)
except Exception as e:
- raise ValidationError('Contest problem label script: %s' % e)
+ raise ValidationError("Contest problem label script: %s" % e)
else:
if not isinstance(label, str):
- raise ValidationError('Contest problem label script: script should return a string.')
+ raise ValidationError(
+ "Contest problem label script: script should return a string."
+ )
def is_in_contest(self, user):
if user.is_authenticated:
profile = user.profile
- return profile and profile.current_contest is not None and profile.current_contest.contest == self
+ return (
+ profile
+ and profile.current_contest is not None
+ and profile.current_contest.contest == self
+ )
return False
def can_see_own_scoreboard(self, user):
@@ -221,7 +409,11 @@ def can_see_own_scoreboard(self, user):
return True
if not self.started:
return False
- if not self.show_scoreboard and not self.is_in_contest(user) and not self.has_completed_contest(user):
+ if (
+ not self.show_scoreboard
+ and not self.is_in_contest(user)
+ and not self.has_completed_contest(user)
+ ):
return False
return True
@@ -230,7 +422,9 @@ def can_see_full_scoreboard(self, user):
return True
if not user.is_authenticated:
return False
- if user.has_perm('judge.see_private_contest') or user.has_perm('judge.edit_all_contest'):
+ if user.has_perm("judge.see_private_contest") or user.has_perm(
+ "judge.edit_all_contest"
+ ):
return True
if user.profile.id in self.editor_ids:
return True
@@ -240,13 +434,18 @@ def can_see_full_scoreboard(self, user):
return True
if self.view_contest_scoreboard.filter(id=user.profile.id).exists():
return True
- if self.scoreboard_visibility == self.SCOREBOARD_AFTER_PARTICIPATION and self.has_completed_contest(user):
+ if (
+ self.scoreboard_visibility == self.SCOREBOARD_AFTER_PARTICIPATION
+ and self.has_completed_contest(user)
+ ):
return True
return False
def has_completed_contest(self, user):
if user.is_authenticated:
- participation = self.users.filter(virtual=ContestParticipation.LIVE, user=user.profile).first()
+ participation = self.users.filter(
+ virtual=ContestParticipation.LIVE, user=user.profile
+ ).first()
if participation and participation.ended:
return True
return False
@@ -255,8 +454,11 @@ def has_completed_contest(self, user):
def show_scoreboard(self):
if not self.started:
return False
- if (self.scoreboard_visibility in (self.SCOREBOARD_AFTER_CONTEST, self.SCOREBOARD_AFTER_PARTICIPATION) and
- not self.ended):
+ if (
+ self.scoreboard_visibility
+ in (self.SCOREBOARD_AFTER_CONTEST, self.SCOREBOARD_AFTER_PARTICIPATION)
+ and not self.ended
+ ):
return False
return self.scoreboard_visibility != self.SCOREBOARD_HIDDEN
@@ -293,26 +495,35 @@ def ended(self):
@cached_property
def author_ids(self):
- return Contest.authors.through.objects.filter(contest=self).values_list('profile_id', flat=True)
+ return Contest.authors.through.objects.filter(contest=self).values_list(
+ "profile_id", flat=True
+ )
@cached_property
def editor_ids(self):
return self.author_ids.union(
- Contest.curators.through.objects.filter(contest=self).values_list('profile_id', flat=True))
+ Contest.curators.through.objects.filter(contest=self).values_list(
+ "profile_id", flat=True
+ )
+ )
@cached_property
def tester_ids(self):
- return Contest.testers.through.objects.filter(contest=self).values_list('profile_id', flat=True)
+ return Contest.testers.through.objects.filter(contest=self).values_list(
+ "profile_id", flat=True
+ )
@cached_property
def spectator_ids(self):
- return Contest.spectators.through.objects.filter(contest=self).values_list('profile_id', flat=True)
+ return Contest.spectators.through.objects.filter(contest=self).values_list(
+ "profile_id", flat=True
+ )
def __str__(self):
return self.name
def get_absolute_url(self):
- return reverse('contest_view', args=(self.key,))
+ return reverse("contest_view", args=(self.key,))
def update_user_count(self):
self.user_count = self.users.filter(virtual=0).count()
@@ -337,7 +548,9 @@ def access_check(self, user):
return
# If the user can view or edit all contests
- if user.has_perm('judge.see_private_contest') or user.has_perm('judge.edit_all_contest'):
+ if user.has_perm("judge.see_private_contest") or user.has_perm(
+ "judge.edit_all_contest"
+ ):
return
# User is organizer or curator for contest
@@ -363,8 +576,10 @@ def access_check(self, user):
if self.view_contest_scoreboard.filter(id=user.profile.id).exists():
return
- in_org = (self.organizations.filter(id__in=user.profile.organizations.all()).exists() or
- self.classes.filter(id__in=user.profile.classes.all()).exists())
+ in_org = (
+ self.organizations.filter(id__in=user.profile.organizations.all()).exists()
+ or self.classes.filter(id__in=user.profile.classes.all()).exists()
+ )
in_users = self.private_contestants.filter(id=user.profile.id).exists()
if not self.is_private and self.is_organization_private:
@@ -391,14 +606,16 @@ def is_live_joinable_by(self, user):
return False
# Can be populated using annotate for performance in list views.
- editor_or_tester = getattr(self, 'editor_or_tester', None)
+ editor_or_tester = getattr(self, "editor_or_tester", None)
if editor_or_tester is None:
- editor_or_tester = user.profile.id in self.editor_ids or user.profile.id in self.tester_ids
+ editor_or_tester = (
+ user.profile.id in self.editor_ids or user.profile.id in self.tester_ids
+ )
if editor_or_tester:
return False
- completed_contest = getattr(self, 'completed_contest', None)
+ completed_contest = getattr(self, "completed_contest", None)
if completed_contest is None:
completed_contest = self.has_completed_contest(user)
@@ -406,7 +623,9 @@ def is_live_joinable_by(self, user):
return False
if self.limit_join_organizations:
- return self.join_organizations.filter(id__in=user.profile.organizations.all()).exists()
+ return self.join_organizations.filter(
+ id__in=user.profile.organizations.all()
+ ).exists()
return True
# Also skips access check
@@ -418,7 +637,9 @@ def is_spectatable_by(self, user):
return True
if self.limit_join_organizations:
- return self.join_organizations.filter(id__in=user.profile.organizations.all()).exists()
+ return self.join_organizations.filter(
+ id__in=user.profile.organizations.all()
+ ).exists()
return True
def is_accessible_by(self, user):
@@ -431,11 +652,14 @@ def is_accessible_by(self, user):
def is_editable_by(self, user):
# If the user can edit all contests
- if user.has_perm('judge.edit_all_contest'):
+ if user.has_perm("judge.edit_all_contest"):
return True
# If the user is a contest organizer or curator
- if user.has_perm('judge.edit_own_contest') and user.profile.id in self.editor_ids:
+ if (
+ user.has_perm("judge.edit_own_contest")
+ and user.profile.id in self.editor_ids
+ ):
return True
return False
@@ -443,20 +667,40 @@ def is_editable_by(self, user):
@classmethod
def get_visible_contests(cls, user):
if not user.is_authenticated:
- return cls.objects.filter(is_visible=True, is_organization_private=False, is_private=False) \
- .defer('description').distinct()
+ return (
+ cls.objects.filter(
+ is_visible=True, is_organization_private=False, is_private=False
+ )
+ .defer("description")
+ .distinct()
+ )
- queryset = cls.objects.defer('description')
- if not (user.has_perm('judge.see_private_contest') or user.has_perm('judge.edit_all_contest')):
- org_check = (Q(organizations__in=user.profile.organizations.all()) |
- Q(classes__in=user.profile.classes.all()))
+ queryset = cls.objects.defer("description")
+ if not (
+ user.has_perm("judge.see_private_contest")
+ or user.has_perm("judge.edit_all_contest")
+ ):
+ org_check = Q(organizations__in=user.profile.organizations.all()) | Q(
+ classes__in=user.profile.classes.all()
+ )
q = Q(is_visible=True)
q &= (
- Q(view_contest_scoreboard=user.profile) |
- Q(is_organization_private=False, is_private=False) |
- Q(is_organization_private=False, is_private=True, private_contestants=user.profile) |
- (Q(is_organization_private=True, is_private=False) & org_check) |
- (Q(is_organization_private=True, is_private=True, private_contestants=user.profile) & org_check)
+ Q(view_contest_scoreboard=user.profile)
+ | Q(is_organization_private=False, is_private=False)
+ | Q(
+ is_organization_private=False,
+ is_private=True,
+ private_contestants=user.profile,
+ )
+ | (Q(is_organization_private=True, is_private=False) & org_check)
+ | (
+ Q(
+ is_organization_private=True,
+ is_private=True,
+ private_contestants=user.profile,
+ )
+ & org_check
+ )
)
q |= Q(authors=user.profile)
@@ -468,45 +712,68 @@ def get_visible_contests(cls, user):
def rate(self):
with transaction.atomic():
- Rating.objects.filter(contest__end_time__range=(self.end_time, self._now)).delete()
+ Rating.objects.filter(
+ contest__end_time__range=(self.end_time, self._now)
+ ).delete()
for contest in Contest.objects.filter(
- is_rated=True, end_time__range=(self.end_time, self._now),
- ).order_by('end_time'):
+ is_rated=True,
+ end_time__range=(self.end_time, self._now),
+ ).order_by("end_time"):
rate_contest(contest)
class Meta:
permissions = (
- ('see_private_contest', _('See private contests')),
- ('edit_own_contest', _('Edit own contests')),
- ('edit_all_contest', _('Edit all contests')),
- ('clone_contest', _('Clone contest')),
- ('moss_contest', _('MOSS contest')),
- ('contest_rating', _('Rate contests')),
- ('contest_access_code', _('Contest access codes')),
- ('create_private_contest', _('Create private contests')),
- ('change_contest_visibility', _('Change contest visibility')),
- ('contest_problem_label', _('Edit contest problem label script')),
- ('lock_contest', _('Change lock status of contest')),
+ ("see_private_contest", _("See private contests")),
+ ("edit_own_contest", _("Edit own contests")),
+ ("edit_all_contest", _("Edit all contests")),
+ ("clone_contest", _("Clone contest")),
+ ("moss_contest", _("MOSS contest")),
+ ("contest_rating", _("Rate contests")),
+ ("contest_access_code", _("Contest access codes")),
+ ("create_private_contest", _("Create private contests")),
+ ("change_contest_visibility", _("Change contest visibility")),
+ ("contest_problem_label", _("Edit contest problem label script")),
+ ("lock_contest", _("Change lock status of contest")),
)
- verbose_name = _('contest')
- verbose_name_plural = _('contests')
+ verbose_name = _("contest")
+ verbose_name_plural = _("contests")
class ContestParticipation(models.Model):
LIVE = 0
SPECTATE = -1
- contest = models.ForeignKey(Contest, verbose_name=_('associated contest'), related_name='users', on_delete=CASCADE)
- user = models.ForeignKey(Profile, verbose_name=_('user'), related_name='contest_history', on_delete=CASCADE)
- real_start = models.DateTimeField(verbose_name=_('start time'), default=timezone.now, db_column='start')
- score = models.FloatField(verbose_name=_('score'), default=0, db_index=True)
- cumtime = models.PositiveIntegerField(verbose_name=_('cumulative time'), default=0)
- is_disqualified = models.BooleanField(verbose_name=_('is disqualified'), default=False,
- help_text=_('Whether this participation is disqualified.'))
- tiebreaker = models.FloatField(verbose_name=_('tie-breaking field'), default=0.0)
- virtual = models.IntegerField(verbose_name=_('virtual participation id'), default=LIVE,
- help_text=_('0 means non-virtual, otherwise the n-th virtual participation.'))
- format_data = JSONField(verbose_name=_('contest format specific data'), null=True, blank=True)
+ contest = models.ForeignKey(
+ Contest,
+ verbose_name=_("associated contest"),
+ related_name="users",
+ on_delete=CASCADE,
+ )
+ user = models.ForeignKey(
+ Profile,
+ verbose_name=_("user"),
+ related_name="contest_history",
+ on_delete=CASCADE,
+ )
+ real_start = models.DateTimeField(
+ verbose_name=_("start time"), default=timezone.now, db_column="start"
+ )
+ score = models.FloatField(verbose_name=_("score"), default=0, db_index=True)
+ cumtime = models.PositiveIntegerField(verbose_name=_("cumulative time"), default=0)
+ is_disqualified = models.BooleanField(
+ verbose_name=_("is disqualified"),
+ default=False,
+ help_text=_("Whether this participation is disqualified."),
+ )
+ tiebreaker = models.FloatField(verbose_name=_("tie-breaking field"), default=0.0)
+ virtual = models.IntegerField(
+ verbose_name=_("virtual participation id"),
+ default=LIVE,
+ help_text=_("0 means non-virtual, otherwise the n-th virtual participation."),
+ )
+ format_data = JSONField(
+ verbose_name=_("contest format specific data"), null=True, blank=True
+ )
def recompute_results(self):
with transaction.atomic():
@@ -515,7 +782,8 @@ def recompute_results(self):
self.score = -9999
self.cumtime = 0
self.tiebreaker = 0
- self.save(update_fields=['score', 'cumtime', 'tiebreaker'])
+ self.save(update_fields=["score", "cumtime", "tiebreaker"])
+
recompute_results.alters_data = True
def set_disqualified(self, disqualified):
@@ -529,6 +797,7 @@ def set_disqualified(self, disqualified):
self.contest.banned_users.add(self.user)
else:
self.contest.banned_users.remove(self.user)
+
set_disqualified.alters_data = True
@property
@@ -542,7 +811,11 @@ def spectate(self):
@cached_property
def start(self):
contest = self.contest
- return contest.start_time if contest.time_limit is None and (self.live or self.spectate) else self.real_start
+ return (
+ contest.start_time
+ if contest.time_limit is None and (self.live or self.spectate)
+ else self.real_start
+ )
@cached_property
def end_time(self):
@@ -554,8 +827,11 @@ def end_time(self):
return self.real_start + contest.time_limit
else:
return self.real_start + (contest.end_time - contest.start_time)
- return contest.end_time if contest.time_limit is None else \
- min(self.real_start + contest.time_limit, contest.end_time)
+ return (
+ contest.end_time
+ if contest.time_limit is None
+ else min(self.real_start + contest.time_limit, contest.end_time)
+ )
@cached_property
def _now(self):
@@ -574,92 +850,148 @@ def time_remaining(self):
def __str__(self):
if self.spectate:
- return _('%(user)s spectating in %(contest)s') % {'user': self.user.username, 'contest': self.contest.name}
+ return _("%(user)s spectating in %(contest)s") % {
+ "user": self.user.username,
+ "contest": self.contest.name,
+ }
if self.virtual:
- return _('%(user)s in %(contest)s, v%(id)d') % {
- 'user': self.user.username, 'contest': self.contest.name, 'id': self.virtual,
+ return _("%(user)s in %(contest)s, v%(id)d") % {
+ "user": self.user.username,
+ "contest": self.contest.name,
+ "id": self.virtual,
}
- return _('%(user)s in %(contest)s') % {'user': self.user.username, 'contest': self.contest.name}
+ return _("%(user)s in %(contest)s") % {
+ "user": self.user.username,
+ "contest": self.contest.name,
+ }
class Meta:
- verbose_name = _('contest participation')
- verbose_name_plural = _('contest participations')
+ verbose_name = _("contest participation")
+ verbose_name_plural = _("contest participations")
- unique_together = ('contest', 'user', 'virtual')
+ unique_together = ("contest", "user", "virtual")
class ContestProblem(models.Model):
- problem = models.ForeignKey(Problem, verbose_name=_('problem'), related_name='contests', on_delete=CASCADE)
- contest = models.ForeignKey(Contest, verbose_name=_('contest'), related_name='contest_problems', on_delete=CASCADE)
- points = models.IntegerField(verbose_name=_('points'))
- partial = models.BooleanField(default=True, verbose_name=_('partial'))
- is_pretested = models.BooleanField(default=False, verbose_name=_('is pretested'))
- order = models.PositiveIntegerField(db_index=True, verbose_name=_('order'))
- output_prefix_override = models.IntegerField(verbose_name=_('output prefix length override'),
- default=0, null=True, blank=True)
- max_submissions = models.IntegerField(verbose_name=_('max submissions'),
- help_text=_('Maximum number of submissions for this problem, '
- 'or leave blank for no limit.'),
- default=None, null=True, blank=True,
- validators=[MinValueOrNoneValidator(1, _('Why include a problem you '
- "can't submit to?"))])
+ problem = models.ForeignKey(
+ Problem, verbose_name=_("problem"), related_name="contests", on_delete=CASCADE
+ )
+ contest = models.ForeignKey(
+ Contest,
+ verbose_name=_("contest"),
+ related_name="contest_problems",
+ on_delete=CASCADE,
+ )
+ points = models.IntegerField(verbose_name=_("points"))
+ partial = models.BooleanField(default=True, verbose_name=_("partial"))
+ is_pretested = models.BooleanField(default=False, verbose_name=_("is pretested"))
+ order = models.PositiveIntegerField(db_index=True, verbose_name=_("order"))
+ output_prefix_override = models.IntegerField(
+ verbose_name=_("output prefix length override"),
+ default=0,
+ null=True,
+ blank=True,
+ )
+ max_submissions = models.IntegerField(
+ verbose_name=_("max submissions"),
+ help_text=_(
+ "Maximum number of submissions for this problem, "
+ "or leave blank for no limit."
+ ),
+ default=None,
+ null=True,
+ blank=True,
+ validators=[
+ MinValueOrNoneValidator(
+ 1, _("Why include a problem you " "can't submit to?")
+ )
+ ],
+ )
class Meta:
- unique_together = ('problem', 'contest')
- verbose_name = _('contest problem')
- verbose_name_plural = _('contest problems')
- ordering = ('order',)
+ unique_together = ("problem", "contest")
+ verbose_name = _("contest problem")
+ verbose_name_plural = _("contest problems")
+ ordering = ("order",)
class ContestSubmission(models.Model):
- submission = models.OneToOneField(Submission, verbose_name=_('submission'),
- related_name='contest', on_delete=CASCADE)
- problem = models.ForeignKey(ContestProblem, verbose_name=_('problem'), on_delete=CASCADE,
- related_name='submissions', related_query_name='submission')
- participation = models.ForeignKey(ContestParticipation, verbose_name=_('participation'), on_delete=CASCADE,
- related_name='submissions', related_query_name='submission')
- points = models.FloatField(default=0.0, verbose_name=_('points'))
- is_pretest = models.BooleanField(verbose_name=_('is pretested'),
- help_text=_('Whether this submission was ran only on pretests.'),
- default=False)
+ submission = models.OneToOneField(
+ Submission,
+ verbose_name=_("submission"),
+ related_name="contest",
+ on_delete=CASCADE,
+ )
+ problem = models.ForeignKey(
+ ContestProblem,
+ verbose_name=_("problem"),
+ on_delete=CASCADE,
+ related_name="submissions",
+ related_query_name="submission",
+ )
+ participation = models.ForeignKey(
+ ContestParticipation,
+ verbose_name=_("participation"),
+ on_delete=CASCADE,
+ related_name="submissions",
+ related_query_name="submission",
+ )
+ points = models.FloatField(default=0.0, verbose_name=_("points"))
+ is_pretest = models.BooleanField(
+ verbose_name=_("is pretested"),
+ help_text=_("Whether this submission was ran only on pretests."),
+ default=False,
+ )
class Meta:
- verbose_name = _('contest submission')
- verbose_name_plural = _('contest submissions')
+ verbose_name = _("contest submission")
+ verbose_name_plural = _("contest submissions")
class Rating(models.Model):
- user = models.ForeignKey(Profile, verbose_name=_('user'), related_name='ratings', on_delete=CASCADE)
- contest = models.ForeignKey(Contest, verbose_name=_('contest'), related_name='ratings', on_delete=CASCADE)
- participation = models.OneToOneField(ContestParticipation, verbose_name=_('participation'),
- related_name='rating', on_delete=CASCADE)
- rank = models.IntegerField(verbose_name=_('rank'))
- rating = models.IntegerField(verbose_name=_('rating'))
- mean = models.FloatField(verbose_name=_('raw rating'))
- performance = models.FloatField(verbose_name=_('contest performance'))
- last_rated = models.DateTimeField(db_index=True, verbose_name=_('last rated'))
+ user = models.ForeignKey(
+ Profile, verbose_name=_("user"), related_name="ratings", on_delete=CASCADE
+ )
+ contest = models.ForeignKey(
+ Contest, verbose_name=_("contest"), related_name="ratings", on_delete=CASCADE
+ )
+ participation = models.OneToOneField(
+ ContestParticipation,
+ verbose_name=_("participation"),
+ related_name="rating",
+ on_delete=CASCADE,
+ )
+ rank = models.IntegerField(verbose_name=_("rank"))
+ rating = models.IntegerField(verbose_name=_("rating"))
+ mean = models.FloatField(verbose_name=_("raw rating"))
+ performance = models.FloatField(verbose_name=_("contest performance"))
+ last_rated = models.DateTimeField(db_index=True, verbose_name=_("last rated"))
class Meta:
- unique_together = ('user', 'contest')
- verbose_name = _('contest rating')
- verbose_name_plural = _('contest ratings')
+ unique_together = ("user", "contest")
+ verbose_name = _("contest rating")
+ verbose_name_plural = _("contest ratings")
class ContestMoss(models.Model):
LANG_MAPPING = [
- ('C', MOSS_LANG_C),
- ('C++', MOSS_LANG_CC),
- ('Java', MOSS_LANG_JAVA),
- ('Python', MOSS_LANG_PYTHON),
+ ("C", MOSS_LANG_C),
+ ("C++", MOSS_LANG_CC),
+ ("Java", MOSS_LANG_JAVA),
+ ("Python", MOSS_LANG_PYTHON),
]
- contest = models.ForeignKey(Contest, verbose_name=_('contest'), related_name='moss', on_delete=CASCADE)
- problem = models.ForeignKey(Problem, verbose_name=_('problem'), related_name='moss', on_delete=CASCADE)
+ contest = models.ForeignKey(
+ Contest, verbose_name=_("contest"), related_name="moss", on_delete=CASCADE
+ )
+ problem = models.ForeignKey(
+ Problem, verbose_name=_("problem"), related_name="moss", on_delete=CASCADE
+ )
language = models.CharField(max_length=10)
submission_count = models.PositiveIntegerField(default=0)
url = models.URLField(null=True, blank=True)
class Meta:
- unique_together = ('contest', 'problem', 'language')
- verbose_name = _('contest moss result')
- verbose_name_plural = _('contest moss results')
+ unique_together = ("contest", "problem", "language")
+ verbose_name = _("contest moss result")
+ verbose_name_plural = _("contest moss results")
diff --git a/judge/models/interface.py b/judge/models/interface.py
index 565900744a..628aba9200 100644
--- a/judge/models/interface.py
+++ b/judge/models/interface.py
@@ -10,43 +10,51 @@
from judge.models.profile import Profile
-__all__ = ['MiscConfig', 'validate_regex', 'NavigationBar', 'BlogPost']
+__all__ = ["MiscConfig", "validate_regex", "NavigationBar", "BlogPost"]
class MiscConfig(models.Model):
- key = models.CharField(max_length=30, verbose_name=_('key'), db_index=True)
- value = models.TextField(verbose_name=_('value'), blank=True)
+ key = models.CharField(max_length=30, verbose_name=_("key"), db_index=True)
+ value = models.TextField(verbose_name=_("value"), blank=True)
def __str__(self):
return self.key
class Meta:
- verbose_name = _('configuration item')
- verbose_name_plural = _('miscellaneous configuration')
+ verbose_name = _("configuration item")
+ verbose_name_plural = _("miscellaneous configuration")
def validate_regex(regex):
try:
re.compile(regex, re.VERBOSE)
except re.error as e:
- raise ValidationError('Invalid regex: %s' % e.message)
+ raise ValidationError("Invalid regex: %s" % e.message)
class NavigationBar(MPTTModel):
class Meta:
- verbose_name = _('navigation item')
- verbose_name_plural = _('navigation bar')
+ verbose_name = _("navigation item")
+ verbose_name_plural = _("navigation bar")
class MPTTMeta:
- order_insertion_by = ['order']
-
- order = models.PositiveIntegerField(db_index=True, verbose_name=_('order'))
- key = models.CharField(max_length=10, unique=True, verbose_name=_('identifier'))
- label = models.CharField(max_length=20, verbose_name=_('label'))
- path = models.CharField(max_length=255, verbose_name=_('link path'))
- regex = models.TextField(verbose_name=_('highlight regex'), validators=[validate_regex])
- parent = TreeForeignKey('self', verbose_name=_('parent item'), null=True, blank=True,
- related_name='children', on_delete=models.CASCADE)
+ order_insertion_by = ["order"]
+
+ order = models.PositiveIntegerField(db_index=True, verbose_name=_("order"))
+ key = models.CharField(max_length=10, unique=True, verbose_name=_("identifier"))
+ label = models.CharField(max_length=20, verbose_name=_("label"))
+ path = models.CharField(max_length=255, verbose_name=_("link path"))
+ regex = models.TextField(
+ verbose_name=_("highlight regex"), validators=[validate_regex]
+ )
+ parent = TreeForeignKey(
+ "self",
+ verbose_name=_("parent item"),
+ null=True,
+ blank=True,
+ related_name="children",
+ on_delete=models.CASCADE,
+ )
def __str__(self):
return self.label
@@ -63,21 +71,23 @@ def pattern(self, cache={}):
class BlogPost(models.Model):
- title = models.CharField(verbose_name=_('post title'), max_length=100)
- authors = models.ManyToManyField(Profile, verbose_name=_('authors'), blank=True)
- slug = models.SlugField(verbose_name=_('slug'))
- visible = models.BooleanField(verbose_name=_('public visibility'), default=False)
- sticky = models.BooleanField(verbose_name=_('sticky'), default=False)
- publish_on = models.DateTimeField(verbose_name=_('publish after'))
- content = models.TextField(verbose_name=_('post content'))
- summary = models.TextField(verbose_name=_('post summary'), blank=True)
- og_image = models.CharField(verbose_name=_('OpenGraph image'), default='', max_length=150, blank=True)
+ title = models.CharField(verbose_name=_("post title"), max_length=100)
+ authors = models.ManyToManyField(Profile, verbose_name=_("authors"), blank=True)
+ slug = models.SlugField(verbose_name=_("slug"))
+ visible = models.BooleanField(verbose_name=_("public visibility"), default=False)
+ sticky = models.BooleanField(verbose_name=_("sticky"), default=False)
+ publish_on = models.DateTimeField(verbose_name=_("publish after"))
+ content = models.TextField(verbose_name=_("post content"))
+ summary = models.TextField(verbose_name=_("post summary"), blank=True)
+ og_image = models.CharField(
+ verbose_name=_("OpenGraph image"), default="", max_length=150, blank=True
+ )
def __str__(self):
return self.title
def get_absolute_url(self):
- return reverse('blog_post', args=(self.id, self.slug))
+ return reverse("blog_post", args=(self.id, self.slug))
def can_see(self, user):
if self.visible and self.publish_on <= timezone.now():
@@ -87,14 +97,17 @@ def can_see(self, user):
def is_editable_by(self, user):
if not user.is_authenticated:
return False
- if user.has_perm('judge.edit_all_post'):
+ if user.has_perm("judge.edit_all_post"):
return True
- return user.has_perm('judge.change_blogpost') and self.authors.filter(id=user.profile.id).exists()
+ return (
+ user.has_perm("judge.change_blogpost")
+ and self.authors.filter(id=user.profile.id).exists()
+ )
class Meta:
permissions = (
- ('edit_all_post', _('Edit all posts')),
- ('change_post_visibility', _('Edit post visibility')),
+ ("edit_all_post", _("Edit all posts")),
+ ("change_post_visibility", _("Edit post visibility")),
)
- verbose_name = _('blog post')
- verbose_name_plural = _('blog posts')
+ verbose_name = _("blog post")
+ verbose_name_plural = _("blog posts")
diff --git a/judge/models/problem.py b/judge/models/problem.py
index aa436724fb..95b9048f86 100644
--- a/judge/models/problem.py
+++ b/judge/models/problem.py
@@ -19,79 +19,120 @@
from judge.models.runtime import Language
from judge.user_translations import gettext as user_gettext
-__all__ = ['ProblemGroup', 'ProblemType', 'Problem', 'ProblemTranslation', 'ProblemClarification', 'License',
- 'Solution', 'SubmissionSourceAccess', 'TranslatedProblemQuerySet']
+__all__ = [
+ "ProblemGroup",
+ "ProblemType",
+ "Problem",
+ "ProblemTranslation",
+ "ProblemClarification",
+ "License",
+ "Solution",
+ "SubmissionSourceAccess",
+ "TranslatedProblemQuerySet",
+]
def disallowed_characters_validator(text):
- common_disallowed_characters = set(text) & settings.DMOJ_PROBLEM_STATEMENT_DISALLOWED_CHARACTERS
+ common_disallowed_characters = (
+ set(text) & settings.DMOJ_PROBLEM_STATEMENT_DISALLOWED_CHARACTERS
+ )
if common_disallowed_characters:
- raise ValidationError(_('Disallowed characters: %(value)s'),
- params={'value': ''.join(common_disallowed_characters)})
+ raise ValidationError(
+ _("Disallowed characters: %(value)s"),
+ params={"value": "".join(common_disallowed_characters)},
+ )
class ProblemType(models.Model):
- name = models.CharField(max_length=20, verbose_name=_('problem category ID'), unique=True)
- full_name = models.CharField(max_length=100, verbose_name=_('problem category name'))
+ name = models.CharField(
+ max_length=20, verbose_name=_("problem category ID"), unique=True
+ )
+ full_name = models.CharField(
+ max_length=100, verbose_name=_("problem category name")
+ )
def __str__(self):
return self.full_name
class Meta:
- ordering = ['full_name']
- verbose_name = _('problem type')
- verbose_name_plural = _('problem types')
+ ordering = ["full_name"]
+ verbose_name = _("problem type")
+ verbose_name_plural = _("problem types")
class ProblemGroup(models.Model):
- name = models.CharField(max_length=20, verbose_name=_('problem group ID'), unique=True)
- full_name = models.CharField(max_length=100, verbose_name=_('problem group name'))
+ name = models.CharField(
+ max_length=20, verbose_name=_("problem group ID"), unique=True
+ )
+ full_name = models.CharField(max_length=100, verbose_name=_("problem group name"))
def __str__(self):
return self.full_name
class Meta:
- ordering = ['full_name']
- verbose_name = _('problem group')
- verbose_name_plural = _('problem groups')
+ ordering = ["full_name"]
+ verbose_name = _("problem group")
+ verbose_name_plural = _("problem groups")
class License(models.Model):
- key = models.CharField(max_length=20, unique=True, verbose_name=_('key'),
- validators=[RegexValidator(r'^[-\w.]+$', r'License key must be ^[-\w.]+$')])
- link = models.CharField(max_length=256, verbose_name=_('link'))
- name = models.CharField(max_length=256, verbose_name=_('full name'))
- display = models.CharField(max_length=256, blank=True, verbose_name=_('short name'),
- help_text=_('Displayed on pages under this license.'))
- icon = models.CharField(max_length=256, blank=True, verbose_name=_('icon'), help_text=_('URL to the icon.'))
- text = models.TextField(verbose_name=_('license text'))
+ key = models.CharField(
+ max_length=20,
+ unique=True,
+ verbose_name=_("key"),
+ validators=[RegexValidator(r"^[-\w.]+$", r"License key must be ^[-\w.]+$")],
+ )
+ link = models.CharField(max_length=256, verbose_name=_("link"))
+ name = models.CharField(max_length=256, verbose_name=_("full name"))
+ display = models.CharField(
+ max_length=256,
+ blank=True,
+ verbose_name=_("short name"),
+ help_text=_("Displayed on pages under this license."),
+ )
+ icon = models.CharField(
+ max_length=256,
+ blank=True,
+ verbose_name=_("icon"),
+ help_text=_("URL to the icon."),
+ )
+ text = models.TextField(verbose_name=_("license text"))
def __str__(self):
return self.name
def get_absolute_url(self):
- return reverse('license', args=(self.key,))
+ return reverse("license", args=(self.key,))
class Meta:
- verbose_name = _('license')
- verbose_name_plural = _('licenses')
+ verbose_name = _("license")
+ verbose_name_plural = _("licenses")
class TranslatedProblemQuerySet(SearchQuerySet):
def __init__(self, **kwargs):
- super(TranslatedProblemQuerySet, self).__init__(('code', 'name', 'description'), **kwargs)
+ super(TranslatedProblemQuerySet, self).__init__(
+ ("code", "name", "description"), **kwargs
+ )
def add_i18n_name(self, language):
- return self.annotate(i18n_translation=FilteredRelation(
- 'translations', condition=Q(translations__language=language),
- )).annotate(i18n_name=Coalesce(F('i18n_translation__name'), F('name'), output_field=models.CharField()))
+ return self.annotate(
+ i18n_translation=FilteredRelation(
+ "translations",
+ condition=Q(translations__language=language),
+ )
+ ).annotate(
+ i18n_name=Coalesce(
+ F("i18n_translation__name"), F("name"), output_field=models.CharField()
+ )
+ )
class SubmissionSourceAccess:
- ALWAYS = 'A'
- SOLVED = 'S'
- ONLY_OWN = 'O'
- FOLLOW = 'F'
+ ALWAYS = "A"
+ SOLVED = "S"
+ ONLY_OWN = "O"
+ FOLLOW = "F"
class VotePermission(IntEnum):
@@ -108,77 +149,181 @@ def can_vote(self):
class Problem(models.Model):
SUBMISSION_SOURCE_ACCESS = (
- (SubmissionSourceAccess.FOLLOW, _('Follow global setting')),
- (SubmissionSourceAccess.ALWAYS, _('Always visible')),
- (SubmissionSourceAccess.SOLVED, _('Visible if problem solved')),
- (SubmissionSourceAccess.ONLY_OWN, _('Only own submissions')),
- )
-
- code = models.CharField(max_length=20, verbose_name=_('problem code'), unique=True,
- validators=[RegexValidator('^[a-z0-9]+$', _('Problem code must be ^[a-z0-9]+$'))],
- help_text=_('A short, unique code for the problem, used in the URL after /problem/'))
- name = models.CharField(max_length=100, verbose_name=_('problem name'), db_index=True,
- help_text=_('The full name of the problem, as shown in the problem list.'),
- validators=[disallowed_characters_validator])
- description = models.TextField(verbose_name=_('problem body'), validators=[disallowed_characters_validator])
- authors = models.ManyToManyField(Profile, verbose_name=_('creators'), blank=True, related_name='authored_problems',
- help_text=_('These users will be able to edit the problem, '
- 'and be listed as authors.'))
- curators = models.ManyToManyField(Profile, verbose_name=_('curators'), blank=True, related_name='curated_problems',
- help_text=_('These users will be able to edit the problem, '
- 'but not be listed as authors.'))
- testers = models.ManyToManyField(Profile, verbose_name=_('testers'), blank=True, related_name='tested_problems',
- help_text=_(
- 'These users will be able to view the private problem, but not edit it.'))
- types = models.ManyToManyField(ProblemType, verbose_name=_('problem types'),
- help_text=_("The type of problem, as shown on the problem's page."))
- group = models.ForeignKey(ProblemGroup, verbose_name=_('problem group'), on_delete=CASCADE,
- help_text=_('The group of problem, shown under Category in the problem list.'))
- time_limit = models.FloatField(verbose_name=_('time limit'),
- help_text=_('The time limit for this problem, in seconds. '
- 'Fractional seconds (e.g. 1.5) are supported.'),
- validators=[MinValueValidator(settings.DMOJ_PROBLEM_MIN_TIME_LIMIT),
- MaxValueValidator(settings.DMOJ_PROBLEM_MAX_TIME_LIMIT)])
- memory_limit = models.PositiveIntegerField(verbose_name=_('memory limit'),
- help_text=_('The memory limit for this problem, in kilobytes '
- '(e.g. 256mb = 262144 kilobytes).'),
- validators=[MinValueValidator(settings.DMOJ_PROBLEM_MIN_MEMORY_LIMIT),
- MaxValueValidator(settings.DMOJ_PROBLEM_MAX_MEMORY_LIMIT)])
- short_circuit = models.BooleanField(verbose_name=_('short circuit'), default=False)
- points = models.FloatField(verbose_name=_('points'),
- help_text=_('Points awarded for problem completion. '
- "Points are displayed with a 'p' suffix if partial."),
- validators=[MinValueValidator(settings.DMOJ_PROBLEM_MIN_PROBLEM_POINTS)])
- partial = models.BooleanField(verbose_name=_('allows partial points'), default=False)
- allowed_languages = models.ManyToManyField(Language, verbose_name=_('allowed languages'),
- help_text=_('List of allowed submission languages.'))
- is_public = models.BooleanField(verbose_name=_('publicly visible'), db_index=True, default=False)
- is_manually_managed = models.BooleanField(verbose_name=_('manually managed'), db_index=True, default=False,
- help_text=_('Whether judges should be allowed to manage data or not.'))
- date = models.DateTimeField(verbose_name=_('date of publishing'), null=True, blank=True, db_index=True,
- help_text=_(
- "Doesn't have the magic ability to auto-publish due to backward compatibility."))
- banned_users = models.ManyToManyField(Profile, verbose_name=_('personae non gratae'), blank=True,
- help_text=_('Bans the selected users from submitting to this problem.'))
- license = models.ForeignKey(License, null=True, blank=True, on_delete=SET_NULL, verbose_name=_('license'),
- help_text=_('The license under which this problem is published.'))
- og_image = models.CharField(verbose_name=_('OpenGraph image'), max_length=150, blank=True)
- summary = models.TextField(blank=True, verbose_name=_('problem summary'),
- help_text=_('Plain-text, shown in meta description tag, e.g. for social media.'))
- user_count = models.IntegerField(verbose_name=_('number of users'), default=0,
- help_text=_('The number of users who solved the problem.'))
- ac_rate = models.FloatField(verbose_name=_('solve rate'), default=0)
- is_full_markup = models.BooleanField(verbose_name=_('allow full markdown access'), default=False)
- submission_source_visibility_mode = models.CharField(verbose_name=_('submission source visibility'), max_length=1,
- default=SubmissionSourceAccess.FOLLOW,
- choices=SUBMISSION_SOURCE_ACCESS)
+ (SubmissionSourceAccess.FOLLOW, _("Follow global setting")),
+ (SubmissionSourceAccess.ALWAYS, _("Always visible")),
+ (SubmissionSourceAccess.SOLVED, _("Visible if problem solved")),
+ (SubmissionSourceAccess.ONLY_OWN, _("Only own submissions")),
+ )
+
+ code = models.CharField(
+ max_length=20,
+ verbose_name=_("problem code"),
+ unique=True,
+ validators=[
+ RegexValidator("^[a-z0-9]+$", _("Problem code must be ^[a-z0-9]+$"))
+ ],
+ help_text=_(
+ "A short, unique code for the problem, used in the URL after /problem/"
+ ),
+ )
+ name = models.CharField(
+ max_length=100,
+ verbose_name=_("problem name"),
+ db_index=True,
+ help_text=_("The full name of the problem, as shown in the problem list."),
+ validators=[disallowed_characters_validator],
+ )
+ description = models.TextField(
+ verbose_name=_("problem body"), validators=[disallowed_characters_validator]
+ )
+ authors = models.ManyToManyField(
+ Profile,
+ verbose_name=_("creators"),
+ blank=True,
+ related_name="authored_problems",
+ help_text=_(
+ "These users will be able to edit the problem, " "and be listed as authors."
+ ),
+ )
+ curators = models.ManyToManyField(
+ Profile,
+ verbose_name=_("curators"),
+ blank=True,
+ related_name="curated_problems",
+ help_text=_(
+ "These users will be able to edit the problem, "
+ "but not be listed as authors."
+ ),
+ )
+ testers = models.ManyToManyField(
+ Profile,
+ verbose_name=_("testers"),
+ blank=True,
+ related_name="tested_problems",
+ help_text=_(
+ "These users will be able to view the private problem, but not edit it."
+ ),
+ )
+ types = models.ManyToManyField(
+ ProblemType,
+ verbose_name=_("problem types"),
+ help_text=_("The type of problem, as shown on the problem's page."),
+ )
+ group = models.ForeignKey(
+ ProblemGroup,
+ verbose_name=_("problem group"),
+ on_delete=CASCADE,
+ help_text=_("The group of problem, shown under Category in the problem list."),
+ )
+ time_limit = models.FloatField(
+ verbose_name=_("time limit"),
+ help_text=_(
+ "The time limit for this problem, in seconds. "
+ "Fractional seconds (e.g. 1.5) are supported."
+ ),
+ validators=[
+ MinValueValidator(settings.DMOJ_PROBLEM_MIN_TIME_LIMIT),
+ MaxValueValidator(settings.DMOJ_PROBLEM_MAX_TIME_LIMIT),
+ ],
+ )
+ memory_limit = models.PositiveIntegerField(
+ verbose_name=_("memory limit"),
+ help_text=_(
+ "The memory limit for this problem, in kilobytes "
+ "(e.g. 256mb = 262144 kilobytes)."
+ ),
+ validators=[
+ MinValueValidator(settings.DMOJ_PROBLEM_MIN_MEMORY_LIMIT),
+ MaxValueValidator(settings.DMOJ_PROBLEM_MAX_MEMORY_LIMIT),
+ ],
+ )
+ short_circuit = models.BooleanField(verbose_name=_("short circuit"), default=False)
+ points = models.FloatField(
+ verbose_name=_("points"),
+ help_text=_(
+ "Points awarded for problem completion. "
+ "Points are displayed with a 'p' suffix if partial."
+ ),
+ validators=[MinValueValidator(settings.DMOJ_PROBLEM_MIN_PROBLEM_POINTS)],
+ )
+ partial = models.BooleanField(
+ verbose_name=_("allows partial points"), default=False
+ )
+ allowed_languages = models.ManyToManyField(
+ Language,
+ verbose_name=_("allowed languages"),
+ help_text=_("List of allowed submission languages."),
+ )
+ is_public = models.BooleanField(
+ verbose_name=_("publicly visible"), db_index=True, default=False
+ )
+ is_manually_managed = models.BooleanField(
+ verbose_name=_("manually managed"),
+ db_index=True,
+ default=False,
+ help_text=_("Whether judges should be allowed to manage data or not."),
+ )
+ date = models.DateTimeField(
+ verbose_name=_("date of publishing"),
+ null=True,
+ blank=True,
+ db_index=True,
+ help_text=_(
+ "Doesn't have the magic ability to auto-publish due to backward compatibility."
+ ),
+ )
+ banned_users = models.ManyToManyField(
+ Profile,
+ verbose_name=_("personae non gratae"),
+ blank=True,
+ help_text=_("Bans the selected users from submitting to this problem."),
+ )
+ license = models.ForeignKey(
+ License,
+ null=True,
+ blank=True,
+ on_delete=SET_NULL,
+ verbose_name=_("license"),
+ help_text=_("The license under which this problem is published."),
+ )
+ og_image = models.CharField(
+ verbose_name=_("OpenGraph image"), max_length=150, blank=True
+ )
+ summary = models.TextField(
+ blank=True,
+ verbose_name=_("problem summary"),
+ help_text=_(
+ "Plain-text, shown in meta description tag, e.g. for social media."
+ ),
+ )
+ user_count = models.IntegerField(
+ verbose_name=_("number of users"),
+ default=0,
+ help_text=_("The number of users who solved the problem."),
+ )
+ ac_rate = models.FloatField(verbose_name=_("solve rate"), default=0)
+ is_full_markup = models.BooleanField(
+ verbose_name=_("allow full markdown access"), default=False
+ )
+ submission_source_visibility_mode = models.CharField(
+ verbose_name=_("submission source visibility"),
+ max_length=1,
+ default=SubmissionSourceAccess.FOLLOW,
+ choices=SUBMISSION_SOURCE_ACCESS,
+ )
objects = TranslatedProblemQuerySet.as_manager()
- tickets = GenericRelation('Ticket')
+ tickets = GenericRelation("Ticket")
- organizations = models.ManyToManyField(Organization, blank=True, verbose_name=_('organizations'),
- help_text=_('If private, only these organizations may see the problem.'))
- is_organization_private = models.BooleanField(verbose_name=_('private to organizations'), default=False)
+ organizations = models.ManyToManyField(
+ Organization,
+ blank=True,
+ verbose_name=_("organizations"),
+ help_text=_("If private, only these organizations may see the problem."),
+ )
+ is_organization_private = models.BooleanField(
+ verbose_name=_("private to organizations"), default=False
+ )
def __init__(self, *args, **kwargs):
super(Problem, self).__init__(*args, **kwargs)
@@ -188,24 +333,37 @@ def __init__(self, *args, **kwargs):
@cached_property
def types_list(self):
- return list(map(user_gettext, map(attrgetter('full_name'), self.types.all())))
+ return list(map(user_gettext, map(attrgetter("full_name"), self.types.all())))
def languages_list(self):
- return self.allowed_languages.values_list('common_name', flat=True).distinct().order_by('common_name')
+ return (
+ self.allowed_languages.values_list("common_name", flat=True)
+ .distinct()
+ .order_by("common_name")
+ )
def is_editor(self, profile):
- return (self.authors.filter(id=profile.id) | self.curators.filter(id=profile.id)).exists()
+ return (
+ self.authors.filter(id=profile.id) | self.curators.filter(id=profile.id)
+ ).exists()
def is_editable_by(self, user):
if not user.is_authenticated:
return False
- if not user.has_perm('judge.edit_own_problem'):
+ if not user.has_perm("judge.edit_own_problem"):
return False
- if user.has_perm('judge.edit_all_problem') or user.has_perm('judge.edit_public_problem') and self.is_public:
+ if (
+ user.has_perm("judge.edit_all_problem")
+ or user.has_perm("judge.edit_public_problem")
+ and self.is_public
+ ):
return True
if user.profile.id in self.editor_ids:
return True
- if self.is_organization_private and self.organizations.filter(admins=user.profile).exists():
+ if (
+ self.is_organization_private
+ and self.organizations.filter(admins=user.profile).exists()
+ ):
return True
return False
@@ -216,7 +374,10 @@ def is_accessible_by(self, user, skip_contest_problem_check=False):
current = user.profile.current_contest_id
if current is not None:
from judge.models import ContestProblem
- if ContestProblem.objects.filter(problem_id=self.id, contest__users__id=current).exists():
+
+ if ContestProblem.objects.filter(
+ problem_id=self.id, contest__users__id=current
+ ).exists():
return True
# Problem is public.
@@ -226,19 +387,20 @@ def is_accessible_by(self, user, skip_contest_problem_check=False):
return True
# If the user can see all organization private problems.
- if user.has_perm('judge.see_organization_problem'):
+ if user.has_perm("judge.see_organization_problem"):
return True
# If the user is in the organization.
- if user.is_authenticated and \
- self.organizations.filter(id__in=user.profile.organizations.all()):
+ if user.is_authenticated and self.organizations.filter(
+ id__in=user.profile.organizations.all()
+ ):
return True
if not user.is_authenticated:
return False
# If the user can view all problems.
- if user.has_perm('judge.see_private_problem'):
+ if user.has_perm("judge.see_private_problem"):
return True
# If the user can edit the problem.
@@ -253,7 +415,11 @@ def is_accessible_by(self, user, skip_contest_problem_check=False):
return False
def is_subs_manageable_by(self, user):
- return user.is_staff and user.has_perm('judge.rejudge_submission') and self.is_editable_by(user)
+ return (
+ user.is_staff
+ and user.has_perm("judge.rejudge_submission")
+ and self.is_editable_by(user)
+ )
@classmethod
def get_visible_problems(cls, user):
@@ -270,25 +436,33 @@ def get_visible_problems(cls, user):
# - is_public problems
# - not is_organization_private or in organization or `judge.see_organization_problem`
# - author or curator or tester
- queryset = cls.objects.defer('description')
+ queryset = cls.objects.defer("description")
- edit_own_problem = user.has_perm('judge.edit_own_problem')
- edit_public_problem = edit_own_problem and user.has_perm('judge.edit_public_problem')
- edit_all_problem = edit_own_problem and user.has_perm('judge.edit_all_problem')
+ edit_own_problem = user.has_perm("judge.edit_own_problem")
+ edit_public_problem = edit_own_problem and user.has_perm(
+ "judge.edit_public_problem"
+ )
+ edit_all_problem = edit_own_problem and user.has_perm("judge.edit_all_problem")
- if not (user.has_perm('judge.see_private_problem') or edit_all_problem):
+ if not (user.has_perm("judge.see_private_problem") or edit_all_problem):
q = Q(is_public=True)
- if not (user.has_perm('judge.see_organization_problem') or edit_public_problem):
+ if not (
+ user.has_perm("judge.see_organization_problem") or edit_public_problem
+ ):
# Either not organization private or in the organization.
q &= Q(is_organization_private=False) | cls.organization_filter_q(
# Avoids needlessly joining Organization
- Profile.organizations.through.objects.filter(profile=user.profile).values('organization_id'),
+ Profile.organizations.through.objects.filter(
+ profile=user.profile
+ ).values("organization_id"),
)
if edit_own_problem:
q |= cls.organization_filter_q(
# Avoids needlessly joining Organization
- Organization.admins.through.objects.filter(profile=user.profile).values('organization_id'),
+ Organization.admins.through.objects.filter(
+ profile=user.profile
+ ).values("organization_id"),
)
# Authors, curators, and testers should always have access.
@@ -301,32 +475,52 @@ def get_visible_problems(cls, user):
def q_add_author_curator_tester(cls, q, profile):
# This is way faster than the obvious |= Q(authors=profile) et al. because we are not doing
# joins and forcing the user to clean it up with .distinct().
- q |= Exists(Problem.authors.through.objects.filter(problem=OuterRef('pk'), profile=profile))
- q |= Exists(Problem.curators.through.objects.filter(problem=OuterRef('pk'), profile=profile))
- q |= Exists(Problem.testers.through.objects.filter(problem=OuterRef('pk'), profile=profile))
+ q |= Exists(
+ Problem.authors.through.objects.filter(
+ problem=OuterRef("pk"), profile=profile
+ )
+ )
+ q |= Exists(
+ Problem.curators.through.objects.filter(
+ problem=OuterRef("pk"), profile=profile
+ )
+ )
+ q |= Exists(
+ Problem.testers.through.objects.filter(
+ problem=OuterRef("pk"), profile=profile
+ )
+ )
return q
@classmethod
def organization_filter_q(cls, queryset):
q = Q(is_organization_private=True)
- q &= Exists(Problem.organizations.through.objects.filter(problem=OuterRef('pk'), organization__in=queryset))
+ q &= Exists(
+ Problem.organizations.through.objects.filter(
+ problem=OuterRef("pk"), organization__in=queryset
+ )
+ )
return q
@classmethod
def get_public_problems(cls):
- return cls.objects.filter(is_public=True, is_organization_private=False).defer('description')
+ return cls.objects.filter(is_public=True, is_organization_private=False).defer(
+ "description"
+ )
@classmethod
def get_editable_problems(cls, user):
- if not user.has_perm('judge.edit_own_problem'):
+ if not user.has_perm("judge.edit_own_problem"):
return cls.objects.none()
- if user.has_perm('judge.edit_all_problem'):
+ if user.has_perm("judge.edit_all_problem"):
return cls.objects.all()
q = Q(authors=user.profile) | Q(curators=user.profile)
- q |= Q(is_organization_private=True, organizations__in=user.profile.admin_of.all())
+ q |= Q(
+ is_organization_private=True, organizations__in=user.profile.admin_of.all()
+ )
- if user.has_perm('judge.edit_public_problem'):
+ if user.has_perm("judge.edit_public_problem"):
q |= Q(is_public=True)
return cls.objects.filter(q)
@@ -335,35 +529,46 @@ def __str__(self):
return self.name
def get_absolute_url(self):
- return reverse('problem_detail', args=(self.code,))
+ return reverse("problem_detail", args=(self.code,))
@cached_property
def author_ids(self):
- return Problem.authors.through.objects.filter(problem=self).values_list('profile_id', flat=True)
+ return Problem.authors.through.objects.filter(problem=self).values_list(
+ "profile_id", flat=True
+ )
@cached_property
def editor_ids(self):
return self.author_ids.union(
- Problem.curators.through.objects.filter(problem=self).values_list('profile_id', flat=True))
+ Problem.curators.through.objects.filter(problem=self).values_list(
+ "profile_id", flat=True
+ )
+ )
@cached_property
def tester_ids(self):
- return Problem.testers.through.objects.filter(problem=self).values_list('profile_id', flat=True)
+ return Problem.testers.through.objects.filter(problem=self).values_list(
+ "profile_id", flat=True
+ )
@cached_property
def usable_common_names(self):
- return set(self.usable_languages.values_list('common_name', flat=True))
+ return set(self.usable_languages.values_list("common_name", flat=True))
@property
def usable_languages(self):
- return self.allowed_languages.filter(judges__in=self.judges.filter(online=True)).distinct()
+ return self.allowed_languages.filter(
+ judges__in=self.judges.filter(online=True)
+ ).distinct()
def translated_name(self, language):
if language in self._translated_name_cache:
return self._translated_name_cache[language]
# Hits database despite prefetch_related.
try:
- name = self.translations.filter(language=language).values_list('name', flat=True)[0]
+ name = self.translations.filter(language=language).values_list(
+ "name", flat=True
+ )[0]
except IndexError:
name = self.name
self._translated_name_cache[language] = name
@@ -387,16 +592,16 @@ def clarifications(self):
def submission_source_visibility(self):
if self.submission_source_visibility_mode == SubmissionSourceAccess.FOLLOW:
return {
- 'all': SubmissionSourceAccess.ALWAYS,
- 'all-solved': SubmissionSourceAccess.SOLVED,
- 'only-own': SubmissionSourceAccess.ONLY_OWN,
+ "all": SubmissionSourceAccess.ALWAYS,
+ "all-solved": SubmissionSourceAccess.SOLVED,
+ "only-own": SubmissionSourceAccess.ONLY_OWN,
}[settings.DMOJ_SUBMISSION_SOURCE_VISIBILITY]
return self.submission_source_visibility_mode
def update_stats(self):
all_queryset = self.submission_set.filter(user__is_unlisted=False)
- ac_queryset = all_queryset.filter(points__gte=self.points, result='AC')
- self.user_count = ac_queryset.values('user').distinct().count()
+ ac_queryset = all_queryset.filter(points__gte=self.points, result="AC")
+ self.user_count = ac_queryset.values("user").distinct().count()
submissions = all_queryset.count()
if submissions:
self.ac_rate = 100.0 * ac_queryset.count() / submissions
@@ -408,9 +613,13 @@ def update_stats(self):
def _get_limits(self, key):
global_limit = getattr(self, key)
- limits = {limit['language_id']: (limit['language__name'], limit[key])
- for limit in self.language_limits.values('language_id', 'language__name', key)
- if limit[key] != global_limit}
+ limits = {
+ limit["language_id"]: (limit["language__name"], limit[key])
+ for limit in self.language_limits.values(
+ "language_id", "language__name", key
+ )
+ if limit[key] != global_limit
+ }
limit_ids = set(limits.keys())
common = []
@@ -430,27 +639,27 @@ def _get_limits(self, key):
@property
def language_time_limit(self):
- key = 'problem_tls:%d' % self.id
+ key = "problem_tls:%d" % self.id
result = cache.get(key)
if result is not None:
return result
- result = self._get_limits('time_limit')
+ result = self._get_limits("time_limit")
cache.set(key, result)
return result
@property
def language_memory_limit(self):
- key = 'problem_mls:%d' % self.id
+ key = "problem_mls:%d" % self.id
result = cache.get(key)
if result is not None:
return result
- result = self._get_limits('memory_limit')
+ result = self._get_limits("memory_limit")
cache.set(key, result)
return result
@property
def markdown_style(self):
- return 'problem-full' if self.is_full_markup else 'problem'
+ return "problem-full" if self.is_full_markup else "problem"
def save(self, *args, **kwargs):
super(Problem, self).save(*args, **kwargs)
@@ -466,7 +675,9 @@ def save(self, *args, **kwargs):
def is_solved_by(self, user):
# Return true if a full AC submission to the problem from the user exists.
- return self.submission_set.filter(user=user.profile, result='AC', points__gte=F('problem__points')).exists()
+ return self.submission_set.filter(
+ user=user.profile, result="AC", points__gte=F("problem__points")
+ ).exists()
def vote_permission_for_user(self, user):
if not user.is_authenticated:
@@ -492,114 +703,173 @@ def vote_permission_for_user(self, user):
class Meta:
permissions = (
- ('see_private_problem', _('See hidden problems')),
- ('edit_own_problem', _('Edit own problems')),
- ('edit_all_problem', _('Edit all problems')),
- ('edit_public_problem', _('Edit all public problems')),
- ('problem_full_markup', _('Edit problems with full markup')),
- ('clone_problem', _('Clone problem')),
- ('change_public_visibility', _('Change is_public field')),
- ('change_manually_managed', _('Change is_manually_managed field')),
- ('see_organization_problem', _('See organization-private problems')),
+ ("see_private_problem", _("See hidden problems")),
+ ("edit_own_problem", _("Edit own problems")),
+ ("edit_all_problem", _("Edit all problems")),
+ ("edit_public_problem", _("Edit all public problems")),
+ ("problem_full_markup", _("Edit problems with full markup")),
+ ("clone_problem", _("Clone problem")),
+ ("change_public_visibility", _("Change is_public field")),
+ ("change_manually_managed", _("Change is_manually_managed field")),
+ ("see_organization_problem", _("See organization-private problems")),
)
- verbose_name = _('problem')
- verbose_name_plural = _('problems')
+ verbose_name = _("problem")
+ verbose_name_plural = _("problems")
class ProblemTranslation(models.Model):
- problem = models.ForeignKey(Problem, verbose_name=_('problem'), related_name='translations', on_delete=CASCADE)
- language = models.CharField(verbose_name=_('language'), max_length=7, choices=settings.LANGUAGES)
- name = models.CharField(verbose_name=_('translated name'), max_length=100, db_index=True)
- description = models.TextField(verbose_name=_('translated description'),
- validators=[disallowed_characters_validator])
+ problem = models.ForeignKey(
+ Problem,
+ verbose_name=_("problem"),
+ related_name="translations",
+ on_delete=CASCADE,
+ )
+ language = models.CharField(
+ verbose_name=_("language"), max_length=7, choices=settings.LANGUAGES
+ )
+ name = models.CharField(
+ verbose_name=_("translated name"), max_length=100, db_index=True
+ )
+ description = models.TextField(
+ verbose_name=_("translated description"),
+ validators=[disallowed_characters_validator],
+ )
class Meta:
- unique_together = ('problem', 'language')
- verbose_name = _('problem translation')
- verbose_name_plural = _('problem translations')
+ unique_together = ("problem", "language")
+ verbose_name = _("problem translation")
+ verbose_name_plural = _("problem translations")
class ProblemClarification(models.Model):
- problem = models.ForeignKey(Problem, verbose_name=_('clarified problem'), on_delete=CASCADE)
- description = models.TextField(verbose_name=_('clarification body'), validators=[disallowed_characters_validator])
- date = models.DateTimeField(verbose_name=_('clarification timestamp'), auto_now_add=True)
+ problem = models.ForeignKey(
+ Problem, verbose_name=_("clarified problem"), on_delete=CASCADE
+ )
+ description = models.TextField(
+ verbose_name=_("clarification body"),
+ validators=[disallowed_characters_validator],
+ )
+ date = models.DateTimeField(
+ verbose_name=_("clarification timestamp"), auto_now_add=True
+ )
class Meta:
- verbose_name = _('problem clarification')
- verbose_name_plural = _('problem clarifications')
+ verbose_name = _("problem clarification")
+ verbose_name_plural = _("problem clarifications")
class LanguageLimit(models.Model):
- problem = models.ForeignKey(Problem, verbose_name=_('problem'), related_name='language_limits', on_delete=CASCADE)
- language = models.ForeignKey(Language, verbose_name=_('language'), on_delete=CASCADE)
- time_limit = models.FloatField(verbose_name=_('time limit'),
- validators=[MinValueValidator(settings.DMOJ_PROBLEM_MIN_TIME_LIMIT),
- MaxValueValidator(settings.DMOJ_PROBLEM_MAX_TIME_LIMIT)])
- memory_limit = models.IntegerField(verbose_name=_('memory limit'),
- validators=[MinValueValidator(settings.DMOJ_PROBLEM_MIN_MEMORY_LIMIT),
- MaxValueValidator(settings.DMOJ_PROBLEM_MAX_MEMORY_LIMIT)])
+ problem = models.ForeignKey(
+ Problem,
+ verbose_name=_("problem"),
+ related_name="language_limits",
+ on_delete=CASCADE,
+ )
+ language = models.ForeignKey(
+ Language, verbose_name=_("language"), on_delete=CASCADE
+ )
+ time_limit = models.FloatField(
+ verbose_name=_("time limit"),
+ validators=[
+ MinValueValidator(settings.DMOJ_PROBLEM_MIN_TIME_LIMIT),
+ MaxValueValidator(settings.DMOJ_PROBLEM_MAX_TIME_LIMIT),
+ ],
+ )
+ memory_limit = models.IntegerField(
+ verbose_name=_("memory limit"),
+ validators=[
+ MinValueValidator(settings.DMOJ_PROBLEM_MIN_MEMORY_LIMIT),
+ MaxValueValidator(settings.DMOJ_PROBLEM_MAX_MEMORY_LIMIT),
+ ],
+ )
class Meta:
- unique_together = ('problem', 'language')
- verbose_name = _('language-specific resource limit')
- verbose_name_plural = _('language-specific resource limits')
+ unique_together = ("problem", "language")
+ verbose_name = _("language-specific resource limit")
+ verbose_name_plural = _("language-specific resource limits")
class Solution(models.Model):
- problem = models.OneToOneField(Problem, on_delete=CASCADE, verbose_name=_('associated problem'),
- blank=True, related_name='solution')
- is_public = models.BooleanField(verbose_name=_('public visibility'), default=False)
- publish_on = models.DateTimeField(verbose_name=_('publish date'))
- authors = models.ManyToManyField(Profile, verbose_name=_('authors'), blank=True)
- content = models.TextField(verbose_name=_('editorial content'), validators=[disallowed_characters_validator])
+ problem = models.OneToOneField(
+ Problem,
+ on_delete=CASCADE,
+ verbose_name=_("associated problem"),
+ blank=True,
+ related_name="solution",
+ )
+ is_public = models.BooleanField(verbose_name=_("public visibility"), default=False)
+ publish_on = models.DateTimeField(verbose_name=_("publish date"))
+ authors = models.ManyToManyField(Profile, verbose_name=_("authors"), blank=True)
+ content = models.TextField(
+ verbose_name=_("editorial content"),
+ validators=[disallowed_characters_validator],
+ )
def get_absolute_url(self):
problem = self.problem
if problem is None:
- return reverse('home')
+ return reverse("home")
else:
- return reverse('problem_editorial', args=[problem.code])
+ return reverse("problem_editorial", args=[problem.code])
def __str__(self):
- return _('Editorial for %s') % self.problem.name
+ return _("Editorial for %s") % self.problem.name
def is_accessible_by(self, user):
if self.is_public and self.publish_on < timezone.now():
return True
- if user.has_perm('judge.see_private_solution'):
+ if user.has_perm("judge.see_private_solution"):
return True
if self.problem.is_editable_by(user):
return True
return False
class Meta:
- permissions = (
- ('see_private_solution', _('See hidden solutions')),
- )
- verbose_name = _('solution')
- verbose_name_plural = _('solutions')
+ permissions = (("see_private_solution", _("See hidden solutions")),)
+ verbose_name = _("solution")
+ verbose_name_plural = _("solutions")
class ProblemPointsVote(models.Model):
points = models.IntegerField(
- verbose_name=_('proposed points'),
- help_text=_('The amount of points the voter thinks this problem deserves.'),
+ verbose_name=_("proposed points"),
+ help_text=_("The amount of points the voter thinks this problem deserves."),
validators=[
MinValueValidator(settings.DMOJ_PROBLEM_MIN_USER_POINTS_VOTE),
MaxValueValidator(settings.DMOJ_PROBLEM_MAX_USER_POINTS_VOTE),
],
)
- voter = models.ForeignKey(Profile, verbose_name=_('voter'), related_name='problem_points_votes', on_delete=CASCADE)
- problem = models.ForeignKey(Problem, verbose_name=_('problem'), related_name='problem_points_votes',
- on_delete=CASCADE)
- vote_time = models.DateTimeField(verbose_name=_('vote time'), help_text=_('The time this vote was cast.'),
- auto_now_add=True)
- note = models.TextField(verbose_name=_('note'), help_text=_('Justification for problem point value.'),
- max_length=8192, blank=True, default='')
+ voter = models.ForeignKey(
+ Profile,
+ verbose_name=_("voter"),
+ related_name="problem_points_votes",
+ on_delete=CASCADE,
+ )
+ problem = models.ForeignKey(
+ Problem,
+ verbose_name=_("problem"),
+ related_name="problem_points_votes",
+ on_delete=CASCADE,
+ )
+ vote_time = models.DateTimeField(
+ verbose_name=_("vote time"),
+ help_text=_("The time this vote was cast."),
+ auto_now_add=True,
+ )
+ note = models.TextField(
+ verbose_name=_("note"),
+ help_text=_("Justification for problem point value."),
+ max_length=8192,
+ blank=True,
+ default="",
+ )
class Meta:
- verbose_name = _('problem vote')
- verbose_name_plural = _('problem votes')
+ verbose_name = _("problem vote")
+ verbose_name_plural = _("problem votes")
def __str__(self):
- return _('Points vote by %(voter)s for %(problem)s') % {'voter': self.voter, 'problem': self.problem}
+ return _("Points vote by %(voter)s for %(problem)s") % {
+ "voter": self.voter,
+ "problem": self.problem,
+ }
diff --git a/judge/models/problem_data.py b/judge/models/problem_data.py
index a00016a510..bdf05480d6 100644
--- a/judge/models/problem_data.py
+++ b/judge/models/problem_data.py
@@ -6,7 +6,13 @@
from judge.utils.problem_data import ProblemDataStorage
-__all__ = ['problem_data_storage', 'problem_directory_file', 'ProblemData', 'ProblemTestCase', 'CHECKERS']
+__all__ = [
+ "problem_data_storage",
+ "problem_directory_file",
+ "ProblemData",
+ "ProblemTestCase",
+ "CHECKERS",
+]
problem_data_storage = ProblemDataStorage()
@@ -20,32 +26,61 @@ def problem_directory_file(data, filename):
CHECKERS = (
- ('standard', _('Standard')),
- ('floats', _('Floats')),
- ('floatsabs', _('Floats (absolute)')),
- ('floatsrel', _('Floats (relative)')),
- ('rstripped', _('Non-trailing spaces')),
- ('sorted', _('Sorted')),
- ('identical', _('Byte identical')),
- ('linecount', _('Line-by-line')),
+ ("standard", _("Standard")),
+ ("floats", _("Floats")),
+ ("floatsabs", _("Floats (absolute)")),
+ ("floatsrel", _("Floats (relative)")),
+ ("rstripped", _("Non-trailing spaces")),
+ ("sorted", _("Sorted")),
+ ("identical", _("Byte identical")),
+ ("linecount", _("Line-by-line")),
)
class ProblemData(models.Model):
- problem = models.OneToOneField('Problem', verbose_name=_('problem'), related_name='data_files',
- on_delete=models.CASCADE)
- zipfile = models.FileField(verbose_name=_('data zip file'), storage=problem_data_storage, null=True, blank=True,
- upload_to=problem_directory_file)
- generator = models.FileField(verbose_name=_('generator file'), storage=problem_data_storage, null=True, blank=True,
- upload_to=problem_directory_file)
- output_prefix = models.IntegerField(verbose_name=_('output prefix length'), blank=True, null=True)
- output_limit = models.IntegerField(verbose_name=_('output limit length'), blank=True, null=True)
- feedback = models.TextField(verbose_name=_('init.yml generation feedback'), blank=True)
- checker = models.CharField(max_length=10, verbose_name=_('checker'), choices=CHECKERS, blank=True)
- unicode = models.BooleanField(verbose_name=_('enable unicode'), null=True, blank=True)
- nobigmath = models.BooleanField(verbose_name=_('disable bigInteger / bigDecimal'), null=True, blank=True)
- checker_args = models.TextField(verbose_name=_('checker arguments'), blank=True,
- help_text=_('Checker arguments as a JSON object.'))
+ problem = models.OneToOneField(
+ "Problem",
+ verbose_name=_("problem"),
+ related_name="data_files",
+ on_delete=models.CASCADE,
+ )
+ zipfile = models.FileField(
+ verbose_name=_("data zip file"),
+ storage=problem_data_storage,
+ null=True,
+ blank=True,
+ upload_to=problem_directory_file,
+ )
+ generator = models.FileField(
+ verbose_name=_("generator file"),
+ storage=problem_data_storage,
+ null=True,
+ blank=True,
+ upload_to=problem_directory_file,
+ )
+ output_prefix = models.IntegerField(
+ verbose_name=_("output prefix length"), blank=True, null=True
+ )
+ output_limit = models.IntegerField(
+ verbose_name=_("output limit length"), blank=True, null=True
+ )
+ feedback = models.TextField(
+ verbose_name=_("init.yml generation feedback"), blank=True
+ )
+ checker = models.CharField(
+ max_length=10, verbose_name=_("checker"), choices=CHECKERS, blank=True
+ )
+ unicode = models.BooleanField(
+ verbose_name=_("enable unicode"), null=True, blank=True
+ )
+ nobigmath = models.BooleanField(
+ verbose_name=_("disable bigInteger / bigDecimal"), null=True, blank=True
+ )
+ checker_args = models.TextField(
+ verbose_name=_("checker arguments"),
+ blank=True,
+ help_text=_("Checker arguments as a JSON object."),
+ )
__original_zipfile = None
@@ -59,7 +94,7 @@ def save(self, *args, **kwargs):
return super(ProblemData, self).save(*args, **kwargs)
def has_yml(self):
- return problem_data_storage.exists('%s/init.yml' % self.problem.code)
+ return problem_data_storage.exists("%s/init.yml" % self.problem.code)
def _update_code(self, original, new):
try:
@@ -72,25 +107,48 @@ def _update_code(self, original, new):
if self.generator:
self.generator.name = _problem_directory_file(new, self.generator.name)
self.save()
+
_update_code.alters_data = True
class ProblemTestCase(models.Model):
- dataset = models.ForeignKey('Problem', verbose_name=_('problem data set'), related_name='cases',
- on_delete=models.CASCADE)
- order = models.IntegerField(verbose_name=_('case position'))
- type = models.CharField(max_length=1, verbose_name=_('case type'),
- choices=(('C', _('Normal case')),
- ('S', _('Batch start')),
- ('E', _('Batch end'))),
- default='C')
- input_file = models.CharField(max_length=100, verbose_name=_('input file name'), blank=True)
- output_file = models.CharField(max_length=100, verbose_name=_('output file name'), blank=True)
- generator_args = models.TextField(verbose_name=_('generator arguments'), blank=True)
- points = models.IntegerField(verbose_name=_('point value'), blank=True, null=True)
- is_pretest = models.BooleanField(verbose_name=_('case is pretest?'))
- output_prefix = models.IntegerField(verbose_name=_('output prefix length'), blank=True, null=True)
- output_limit = models.IntegerField(verbose_name=_('output limit length'), blank=True, null=True)
- checker = models.CharField(max_length=10, verbose_name=_('checker'), choices=CHECKERS, blank=True)
- checker_args = models.TextField(verbose_name=_('checker arguments'), blank=True,
- help_text=_('Checker arguments as a JSON object.'))
+ dataset = models.ForeignKey(
+ "Problem",
+ verbose_name=_("problem data set"),
+ related_name="cases",
+ on_delete=models.CASCADE,
+ )
+ order = models.IntegerField(verbose_name=_("case position"))
+ type = models.CharField(
+ max_length=1,
+ verbose_name=_("case type"),
+ choices=(
+ ("C", _("Normal case")),
+ ("S", _("Batch start")),
+ ("E", _("Batch end")),
+ ),
+ default="C",
+ )
+ input_file = models.CharField(
+ max_length=100, verbose_name=_("input file name"), blank=True
+ )
+ output_file = models.CharField(
+ max_length=100, verbose_name=_("output file name"), blank=True
+ )
+ generator_args = models.TextField(verbose_name=_("generator arguments"), blank=True)
+ points = models.IntegerField(verbose_name=_("point value"), blank=True, null=True)
+ is_pretest = models.BooleanField(verbose_name=_("case is pretest?"))
+ output_prefix = models.IntegerField(
+ verbose_name=_("output prefix length"), blank=True, null=True
+ )
+ output_limit = models.IntegerField(
+ verbose_name=_("output limit length"), blank=True, null=True
+ )
+ checker = models.CharField(
+ max_length=10, verbose_name=_("checker"), choices=CHECKERS, blank=True
+ )
+ checker_args = models.TextField(
+ verbose_name=_("checker arguments"),
+ blank=True,
+ help_text=_("Checker arguments as a JSON object."),
+ )
diff --git a/judge/models/profile.py b/judge/models/profile.py
index 859c30d47f..81c23d1a9c 100644
--- a/judge/models/profile.py
+++ b/judge/models/profile.py
@@ -28,7 +28,13 @@
from judge.ratings import rating_class
from judge.utils.two_factor import webauthn_decode
-__all__ = ['Class', 'Organization', 'Profile', 'OrganizationRequest', 'WebAuthnCredential']
+__all__ = [
+ "Class",
+ "Organization",
+ "Profile",
+ "OrganizationRequest",
+ "WebAuthnCredential",
+]
class EncryptedNullCharField(EncryptedCharField):
@@ -39,32 +45,71 @@ def get_prep_value(self, value):
class Organization(models.Model):
- name = models.CharField(max_length=128, verbose_name=_('organization title'))
- slug = models.SlugField(max_length=128, verbose_name=_('organization slug'),
- help_text=_('Organization name shown in URLs.'))
- short_name = models.CharField(max_length=20, verbose_name=_('short name'),
- help_text=_('Displayed beside user name during contests.'))
- about = models.TextField(verbose_name=_('organization description'))
- admins = models.ManyToManyField('Profile', verbose_name=_('administrators'), related_name='admin_of',
- help_text=_('Those who can edit this organization.'))
- creation_date = models.DateTimeField(verbose_name=_('creation date'), auto_now_add=True)
- is_open = models.BooleanField(verbose_name=_('is open organization?'),
- help_text=_('Allow joining organization.'), default=True)
- slots = models.IntegerField(verbose_name=_('maximum size'), null=True, blank=True,
- help_text=_('Maximum amount of users in this organization, '
- 'only applicable to private organizations.'))
- access_code = models.CharField(max_length=7, help_text=_('Student access code.'),
- verbose_name=_('access code'), null=True, blank=True)
- logo_override_image = models.CharField(verbose_name=_('logo override image'), default='', max_length=150,
- blank=True,
- help_text=_('This image will replace the default site logo for users '
- 'viewing the organization.'))
- class_required = models.BooleanField(verbose_name=_('class membership required'), default=False,
- help_text=_('Whether members are compelled to select a class when joining.'))
+ name = models.CharField(max_length=128, verbose_name=_("organization title"))
+ slug = models.SlugField(
+ max_length=128,
+ verbose_name=_("organization slug"),
+ help_text=_("Organization name shown in URLs."),
+ )
+ short_name = models.CharField(
+ max_length=20,
+ verbose_name=_("short name"),
+ help_text=_("Displayed beside user name during contests."),
+ )
+ about = models.TextField(verbose_name=_("organization description"))
+ admins = models.ManyToManyField(
+ "Profile",
+ verbose_name=_("administrators"),
+ related_name="admin_of",
+ help_text=_("Those who can edit this organization."),
+ )
+ creation_date = models.DateTimeField(
+ verbose_name=_("creation date"), auto_now_add=True
+ )
+ is_open = models.BooleanField(
+ verbose_name=_("is open organization?"),
+ help_text=_("Allow joining organization."),
+ default=True,
+ )
+ slots = models.IntegerField(
+ verbose_name=_("maximum size"),
+ null=True,
+ blank=True,
+ help_text=_(
+ "Maximum amount of users in this organization, "
+ "only applicable to private organizations."
+ ),
+ )
+ access_code = models.CharField(
+ max_length=7,
+ help_text=_("Student access code."),
+ verbose_name=_("access code"),
+ null=True,
+ blank=True,
+ )
+ logo_override_image = models.CharField(
+ verbose_name=_("logo override image"),
+ default="",
+ max_length=150,
+ blank=True,
+ help_text=_(
+ "This image will replace the default site logo for users "
+ "viewing the organization."
+ ),
+ )
+ class_required = models.BooleanField(
+ verbose_name=_("class membership required"),
+ default=False,
+ help_text=_("Whether members are compelled to select a class when joining."),
+ )
def clean(self):
if self.class_required and self.is_open:
- raise ValidationError(_('Class membership cannot be enforced when organization has open enrollment.'))
+ raise ValidationError(
+ _(
+ "Class membership cannot be enforced when organization has open enrollment."
+ )
+ )
def __contains__(self, item):
if isinstance(item, int):
@@ -72,16 +117,18 @@ def __contains__(self, item):
elif isinstance(item, Profile):
return self.members.filter(id=item.id).exists()
else:
- raise TypeError('Organization membership test must be Profile or primary key.')
+ raise TypeError(
+ "Organization membership test must be Profile or primary key."
+ )
def __str__(self):
return self.name
def get_absolute_url(self):
- return reverse('organization_home', args=(self.id, self.slug))
+ return reverse("organization_home", args=(self.id, self.slug))
def get_users_url(self):
- return reverse('organization_users', args=(self.id, self.slug))
+ return reverse("organization_users", args=(self.id, self.slug))
def can_review_all_requests(self, profile):
return self.admins.filter(id=profile.id).exists()
@@ -90,123 +137,240 @@ def can_review_class_requests(self, profile):
return self.classes.filter(admins__id=profile.id).exists()
class Meta:
- ordering = ['name']
+ ordering = ["name"]
permissions = (
- ('organization_admin', _('Administer organizations')),
- ('edit_all_organization', _('Edit all organizations')),
+ ("organization_admin", _("Administer organizations")),
+ ("edit_all_organization", _("Edit all organizations")),
)
- verbose_name = _('organization')
- verbose_name_plural = _('organizations')
+ verbose_name = _("organization")
+ verbose_name_plural = _("organizations")
class Class(models.Model):
- organization = models.ForeignKey(Organization, on_delete=models.CASCADE, verbose_name=_('organization'),
- help_text=_('The organization that this class belongs to.'),
- related_name='classes', related_query_name='class')
- name = models.CharField(max_length=128, verbose_name=_('class name'), unique=True)
- slug = models.SlugField(max_length=128, verbose_name=_('class slug'), help_text=_('Class name shown in URLs.'))
- description = models.TextField(verbose_name=_('class description'), blank=True)
- is_active = models.BooleanField(verbose_name=_('is class active'), default=True)
- access_code = models.CharField(max_length=7, verbose_name=_('access code'), null=True, blank=True,
- help_text=_('Student access code.'))
- admins = models.ManyToManyField('Profile', verbose_name=_('administrators'), related_name='class_admin_of',
- help_text=_('Those who can approve membership to this class.'))
- members = models.ManyToManyField('Profile', verbose_name=_('members'), blank=True,
- related_name='classes', related_query_name='class')
+ organization = models.ForeignKey(
+ Organization,
+ on_delete=models.CASCADE,
+ verbose_name=_("organization"),
+ help_text=_("The organization that this class belongs to."),
+ related_name="classes",
+ related_query_name="class",
+ )
+ name = models.CharField(max_length=128, verbose_name=_("class name"), unique=True)
+ slug = models.SlugField(
+ max_length=128,
+ verbose_name=_("class slug"),
+ help_text=_("Class name shown in URLs."),
+ )
+ description = models.TextField(verbose_name=_("class description"), blank=True)
+ is_active = models.BooleanField(verbose_name=_("is class active"), default=True)
+ access_code = models.CharField(
+ max_length=7,
+ verbose_name=_("access code"),
+ null=True,
+ blank=True,
+ help_text=_("Student access code."),
+ )
+ admins = models.ManyToManyField(
+ "Profile",
+ verbose_name=_("administrators"),
+ related_name="class_admin_of",
+ help_text=_("Those who can approve membership to this class."),
+ )
+ members = models.ManyToManyField(
+ "Profile",
+ verbose_name=_("members"),
+ blank=True,
+ related_name="classes",
+ related_query_name="class",
+ )
@classmethod
def get_visible_classes(cls, user):
if not user.is_authenticated:
return cls.objects.none()
- if user.has_perm('judge.edit_all_organization'):
+ if user.has_perm("judge.edit_all_organization"):
return cls.objects.all()
- return cls.objects.filter(contest__organizations__admins=user.profile) | cls.objects.filter(admins=user.profile)
+ return cls.objects.filter(
+ contest__organizations__admins=user.profile
+ ) | cls.objects.filter(admins=user.profile)
def __str__(self):
- return _('%(class)s in %(organization)s') % {'class': self.name, 'organization': self.organization.name}
+ return _("%(class)s in %(organization)s") % {
+ "class": self.name,
+ "organization": self.organization.name,
+ }
def get_absolute_url(self):
- return reverse('class_home', args=self._url_args)
+ return reverse("class_home", args=self._url_args)
def get_join_url(self):
- return reverse('class_join', args=self._url_args)
+ return reverse("class_join", args=self._url_args)
@cached_property
def _url_args(self):
return self.organization.id, self.organization.slug, self.id, self.slug
class Meta:
- ordering = ['organization', 'name']
- verbose_name = _('class')
- verbose_name_plural = _('classes')
- constraints = [UniqueConstraint(fields=['name'], condition=Q(is_active=True), name='unique_active_name')]
+ ordering = ["organization", "name"]
+ verbose_name = _("class")
+ verbose_name_plural = _("classes")
+ constraints = [
+ UniqueConstraint(
+ fields=["name"], condition=Q(is_active=True), name="unique_active_name"
+ )
+ ]
class Profile(models.Model):
- user = models.OneToOneField(User, verbose_name=_('user associated'), on_delete=models.CASCADE)
- about = models.TextField(verbose_name=_('self-description'), null=True, blank=True)
- timezone = models.CharField(max_length=50, verbose_name=_('time zone'), choices=TIMEZONE,
- default=settings.DEFAULT_USER_TIME_ZONE)
- language = models.ForeignKey('Language', verbose_name=_('preferred language'), on_delete=models.SET_DEFAULT,
- default=Language.get_default_language_pk)
+ user = models.OneToOneField(
+ User, verbose_name=_("user associated"), on_delete=models.CASCADE
+ )
+ about = models.TextField(verbose_name=_("self-description"), null=True, blank=True)
+ timezone = models.CharField(
+ max_length=50,
+ verbose_name=_("time zone"),
+ choices=TIMEZONE,
+ default=settings.DEFAULT_USER_TIME_ZONE,
+ )
+ language = models.ForeignKey(
+ "Language",
+ verbose_name=_("preferred language"),
+ on_delete=models.SET_DEFAULT,
+ default=Language.get_default_language_pk,
+ )
points = models.FloatField(default=0)
performance_points = models.FloatField(default=0)
problem_count = models.IntegerField(default=0)
- ace_theme = models.CharField(max_length=30, verbose_name=_('Ace theme'), choices=ACE_THEMES, default='auto')
- site_theme = models.CharField(max_length=10, verbose_name=_('site theme'), choices=SITE_THEMES, default='auto')
- last_access = models.DateTimeField(verbose_name=_('last access time'), default=now)
- ip = models.GenericIPAddressField(verbose_name=_('last IP'), blank=True, null=True)
- organizations = SortedManyToManyField(Organization, verbose_name=_('organization'), blank=True,
- related_name='members', related_query_name='member')
- display_rank = models.CharField(max_length=10, default='user', verbose_name=_('display rank'),
- choices=(
- ('user', _('Normal User')),
- ('setter', _('Problem Setter')),
- ('admin', _('Admin'))))
- mute = models.BooleanField(verbose_name=_('comment mute'), help_text=_('Some users are at their best when silent.'),
- default=False)
- is_unlisted = models.BooleanField(verbose_name=_('unlisted user'), help_text=_('User will not be ranked.'),
- default=False)
+ ace_theme = models.CharField(
+ max_length=30, verbose_name=_("Ace theme"), choices=ACE_THEMES, default="auto"
+ )
+ site_theme = models.CharField(
+ max_length=10, verbose_name=_("site theme"), choices=SITE_THEMES, default="auto"
+ )
+ last_access = models.DateTimeField(verbose_name=_("last access time"), default=now)
+ ip = models.GenericIPAddressField(verbose_name=_("last IP"), blank=True, null=True)
+ organizations = SortedManyToManyField(
+ Organization,
+ verbose_name=_("organization"),
+ blank=True,
+ related_name="members",
+ related_query_name="member",
+ )
+ display_rank = models.CharField(
+ max_length=10,
+ default="user",
+ verbose_name=_("display rank"),
+ choices=(
+ ("user", _("Normal User")),
+ ("setter", _("Problem Setter")),
+ ("admin", _("Admin")),
+ ),
+ )
+ mute = models.BooleanField(
+ verbose_name=_("comment mute"),
+ help_text=_("Some users are at their best when silent."),
+ default=False,
+ )
+ is_unlisted = models.BooleanField(
+ verbose_name=_("unlisted user"),
+ help_text=_("User will not be ranked."),
+ default=False,
+ )
is_banned_from_problem_voting = models.BooleanField(
- verbose_name=_('banned from voting on problem point values'),
+ verbose_name=_("banned from voting on problem point values"),
help_text=_("User will not be able to vote on problems' point values."),
default=False,
)
rating = models.IntegerField(null=True, default=None)
- user_script = models.TextField(verbose_name=_('user script'), default='', blank=True, max_length=65536,
- help_text=_('User-defined JavaScript for site customization.'))
- current_contest = models.OneToOneField('ContestParticipation', verbose_name=_('current contest'),
- null=True, blank=True, related_name='+', on_delete=models.SET_NULL)
- math_engine = models.CharField(verbose_name=_('math engine'), choices=MATH_ENGINES_CHOICES, max_length=4,
- default=settings.MATHOID_DEFAULT_TYPE,
- help_text=_('The rendering engine used to render math.'))
- is_totp_enabled = models.BooleanField(verbose_name=_('TOTP 2FA enabled'), default=False,
- help_text=_('Check to enable TOTP-based two-factor authentication.'))
- is_webauthn_enabled = models.BooleanField(verbose_name=_('WebAuthn 2FA enabled'), default=False,
- help_text=_('Check to enable WebAuthn-based two-factor authentication.'))
- totp_key = EncryptedNullCharField(max_length=32, null=True, blank=True, verbose_name=_('TOTP key'),
- help_text=_('32-character Base32-encoded key for TOTP.'),
- validators=[RegexValidator('^$|^[A-Z2-7]{32}$',
- _('TOTP key must be empty or Base32.'))])
- scratch_codes = EncryptedNullCharField(max_length=255, null=True, blank=True, verbose_name=_('scratch codes'),
- help_text=_('JSON array of 16-character Base32-encoded codes '
- 'for scratch codes.'),
- validators=[
- RegexValidator(r'^(\[\])?$|^\[("[A-Z0-9]{16}", *)*"[A-Z0-9]{16}"\]$',
- _('Scratch codes must be empty or a JSON array of '
- '16-character Base32 codes.'))])
- last_totp_timecode = models.IntegerField(verbose_name=_('last TOTP timecode'), default=0)
- api_token = models.CharField(max_length=64, null=True, verbose_name=_('API token'),
- help_text=_('64-character hex-encoded API access token.'),
- validators=[RegexValidator('^[a-f0-9]{64}$',
- _('API token must be None or hexadecimal'))])
- notes = models.TextField(verbose_name=_('internal notes'), null=True, blank=True,
- help_text=_('Notes for administrators regarding this user.'))
- data_last_downloaded = models.DateTimeField(verbose_name=_('last data download time'), null=True, blank=True)
- username_display_override = models.CharField(max_length=100, blank=True, verbose_name=_('display name override'),
- help_text=_('Name displayed in place of username.'))
+ user_script = models.TextField(
+ verbose_name=_("user script"),
+ default="",
+ blank=True,
+ max_length=65536,
+ help_text=_("User-defined JavaScript for site customization."),
+ )
+ current_contest = models.OneToOneField(
+ "ContestParticipation",
+ verbose_name=_("current contest"),
+ null=True,
+ blank=True,
+ related_name="+",
+ on_delete=models.SET_NULL,
+ )
+ math_engine = models.CharField(
+ verbose_name=_("math engine"),
+ choices=MATH_ENGINES_CHOICES,
+ max_length=4,
+ default=settings.MATHOID_DEFAULT_TYPE,
+ help_text=_("The rendering engine used to render math."),
+ )
+ is_totp_enabled = models.BooleanField(
+ verbose_name=_("TOTP 2FA enabled"),
+ default=False,
+ help_text=_("Check to enable TOTP-based two-factor authentication."),
+ )
+ is_webauthn_enabled = models.BooleanField(
+ verbose_name=_("WebAuthn 2FA enabled"),
+ default=False,
+ help_text=_("Check to enable WebAuthn-based two-factor authentication."),
+ )
+ totp_key = EncryptedNullCharField(
+ max_length=32,
+ null=True,
+ blank=True,
+ verbose_name=_("TOTP key"),
+ help_text=_("32-character Base32-encoded key for TOTP."),
+ validators=[
+ RegexValidator("^$|^[A-Z2-7]{32}$", _("TOTP key must be empty or Base32."))
+ ],
+ )
+ scratch_codes = EncryptedNullCharField(
+ max_length=255,
+ null=True,
+ blank=True,
+ verbose_name=_("scratch codes"),
+ help_text=_(
+ "JSON array of 16-character Base32-encoded codes " "for scratch codes."
+ ),
+ validators=[
+ RegexValidator(
+ r'^(\[\])?$|^\[("[A-Z0-9]{16}", *)*"[A-Z0-9]{16}"\]$',
+ _(
+ "Scratch codes must be empty or a JSON array of "
+ "16-character Base32 codes."
+ ),
+ )
+ ],
+ )
+ last_totp_timecode = models.IntegerField(
+ verbose_name=_("last TOTP timecode"), default=0
+ )
+ api_token = models.CharField(
+ max_length=64,
+ null=True,
+ verbose_name=_("API token"),
+ help_text=_("64-character hex-encoded API access token."),
+ validators=[
+ RegexValidator("^[a-f0-9]{64}$", _("API token must be None or hexadecimal"))
+ ],
+ )
+ notes = models.TextField(
+ verbose_name=_("internal notes"),
+ null=True,
+ blank=True,
+ help_text=_("Notes for administrators regarding this user."),
+ )
+ data_last_downloaded = models.DateTimeField(
+ verbose_name=_("last data download time"), null=True, blank=True
+ )
+ username_display_override = models.CharField(
+ max_length=100,
+ blank=True,
+ verbose_name=_("display name override"),
+ help_text=_("Name displayed in place of username."),
+ )
@cached_property
def organization(self):
@@ -224,15 +388,17 @@ def display_name(self):
@cached_property
def has_any_solves(self):
- return self.submission_set.filter(result='AC', case_points__gte=F('case_total')).exists()
+ return self.submission_set.filter(
+ result="AC", case_points__gte=F("case_total")
+ ).exists()
@cached_property
def resolved_ace_theme(self):
- if self.ace_theme != 'auto':
+ if self.ace_theme != "auto":
return self.ace_theme
- if not self.user.has_perm('judge.test_site'):
- return settings.DMOJ_THEME_DEFAULT_ACE_THEME.get('light')
- if self.site_theme != 'auto':
+ if not self.user.has_perm("judge.test_site"):
+ return settings.DMOJ_THEME_DEFAULT_ACE_THEME.get("light")
+ if self.site_theme != "auto":
return settings.DMOJ_THEME_DEFAULT_ACE_THEME.get(self.site_theme)
# This must be resolved client-side using prefers-color-scheme.
return None
@@ -241,45 +407,66 @@ def resolved_ace_theme(self):
def calculate_points(self, table=_pp_table):
from judge.models import Problem
+
public_problems = Problem.get_public_problems()
data = (
- public_problems.filter(submission__user=self, submission__points__isnull=False)
- .annotate(max_points=Max('submission__points')).order_by('-max_points')
- .values_list('max_points', flat=True).filter(max_points__gt=0)
+ public_problems.filter(
+ submission__user=self, submission__points__isnull=False
+ )
+ .annotate(max_points=Max("submission__points"))
+ .order_by("-max_points")
+ .values_list("max_points", flat=True)
+ .filter(max_points__gt=0)
)
bonus_function = settings.DMOJ_PP_BONUS_FUNCTION
points = sum(data)
entries = min(len(data), len(table))
problems = (
- public_problems.filter(submission__user=self, submission__result='AC',
- submission__case_points__gte=F('submission__case_total'))
- .values('id').distinct().count()
+ public_problems.filter(
+ submission__user=self,
+ submission__result="AC",
+ submission__case_points__gte=F("submission__case_total"),
+ )
+ .values("id")
+ .distinct()
+ .count()
)
pp = sum(map(mul, table[:entries], data[:entries])) + bonus_function(problems)
- if self.points != points or problems != self.problem_count or self.performance_points != pp:
+ if (
+ self.points != points
+ or problems != self.problem_count
+ or self.performance_points != pp
+ ):
self.points = points
self.problem_count = problems
self.performance_points = pp
- self.save(update_fields=['points', 'problem_count', 'performance_points'])
+ self.save(update_fields=["points", "problem_count", "performance_points"])
return points
calculate_points.alters_data = True
def generate_api_token(self):
secret = secrets.token_bytes(32)
- self.api_token = hmac.new(force_bytes(settings.SECRET_KEY), msg=secret, digestmod='sha256').hexdigest()
- self.save(update_fields=['api_token'])
- token = base64.urlsafe_b64encode(struct.pack('>I32s', self.user.id, secret))
- return token.decode('utf-8')
+ self.api_token = hmac.new(
+ force_bytes(settings.SECRET_KEY), msg=secret, digestmod="sha256"
+ ).hexdigest()
+ self.save(update_fields=["api_token"])
+ token = base64.urlsafe_b64encode(struct.pack(">I32s", self.user.id, secret))
+ return token.decode("utf-8")
generate_api_token.alters_data = True
def generate_scratch_codes(self):
def generate_scratch_code():
- return ''.join(secrets.choice('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567') for _ in range(16))
- codes = [generate_scratch_code() for _ in range(settings.DMOJ_SCRATCH_CODES_COUNT)]
+ return "".join(
+ secrets.choice("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567") for _ in range(16)
+ )
+
+ codes = [
+ generate_scratch_code() for _ in range(settings.DMOJ_SCRATCH_CODES_COUNT)
+ ]
self.scratch_codes = json.dumps(codes)
- self.save(update_fields=['scratch_codes'])
+ self.save(update_fields=["scratch_codes"])
return codes
generate_scratch_codes.alters_data = True
@@ -292,7 +479,9 @@ def remove_contest(self):
def update_contest(self):
contest = self.current_contest
- if contest is not None and (contest.ended or not contest.contest.is_accessible_by(self.user)):
+ if contest is not None and (
+ contest.ended or not contest.contest.is_accessible_by(self.user)
+ ):
self.remove_contest()
update_contest.alters_data = True
@@ -300,26 +489,36 @@ def update_contest(self):
def check_totp_code(self, code):
totp = pyotp.TOTP(self.totp_key)
now_timecode = totp.timecode(timezone.now())
- min_timecode = max(self.last_totp_timecode + 1, now_timecode - settings.DMOJ_TOTP_TOLERANCE_HALF_MINUTES)
- for timecode in range(min_timecode, now_timecode + settings.DMOJ_TOTP_TOLERANCE_HALF_MINUTES + 1):
+ min_timecode = max(
+ self.last_totp_timecode + 1,
+ now_timecode - settings.DMOJ_TOTP_TOLERANCE_HALF_MINUTES,
+ )
+ for timecode in range(
+ min_timecode, now_timecode + settings.DMOJ_TOTP_TOLERANCE_HALF_MINUTES + 1
+ ):
if strings_equal(code, totp.generate_otp(timecode)):
self.last_totp_timecode = timecode
- self.save(update_fields=['last_totp_timecode'])
+ self.save(update_fields=["last_totp_timecode"])
return True
return False
check_totp_code.alters_data = True
def get_absolute_url(self):
- return reverse('user_page', args=(self.user.username,))
+ return reverse("user_page", args=(self.user.username,))
def __str__(self):
return self.user.username
@classmethod
- def get_user_css_class(cls, display_rank, rating, rating_colors=settings.DMOJ_RATING_COLORS):
+ def get_user_css_class(
+ cls, display_rank, rating, rating_colors=settings.DMOJ_RATING_COLORS
+ ):
if rating_colors:
- return 'rating %s %s' % (rating_class(rating) if rating is not None else 'rate-none', display_rank)
+ return "rating %s %s" % (
+ rating_class(rating) if rating is not None else "rate-none",
+ display_rank,
+ )
return display_rank
@cached_property
@@ -328,30 +527,40 @@ def css_class(self):
@cached_property
def webauthn_id(self):
- return hmac.new(force_bytes(settings.SECRET_KEY), msg=b'webauthn:%d' % (self.id,), digestmod='sha256').digest()
+ return hmac.new(
+ force_bytes(settings.SECRET_KEY),
+ msg=b"webauthn:%d" % (self.id,),
+ digestmod="sha256",
+ ).digest()
class Meta:
permissions = (
- ('test_site', _('Shows in-progress development stuff')),
- ('totp', _('Edit TOTP settings')),
+ ("test_site", _("Shows in-progress development stuff")),
+ ("totp", _("Edit TOTP settings")),
)
- verbose_name = _('user profile')
- verbose_name_plural = _('user profiles')
+ verbose_name = _("user profile")
+ verbose_name_plural = _("user profiles")
indexes = [
- models.Index(fields=('is_unlisted', '-performance_points')),
- models.Index(fields=('is_unlisted', '-rating')),
- models.Index(fields=('is_unlisted', '-problem_count')),
+ models.Index(fields=("is_unlisted", "-performance_points")),
+ models.Index(fields=("is_unlisted", "-rating")),
+ models.Index(fields=("is_unlisted", "-problem_count")),
]
class WebAuthnCredential(models.Model):
- user = models.ForeignKey(Profile, verbose_name=_('user'), related_name='webauthn_credentials',
- on_delete=models.CASCADE)
- name = models.CharField(verbose_name=_('device name'), max_length=100)
- cred_id = models.CharField(verbose_name=_('credential ID'), max_length=255, unique=True)
- public_key = models.TextField(verbose_name=_('public key'))
- counter = models.BigIntegerField(verbose_name=_('sign counter'))
+ user = models.ForeignKey(
+ Profile,
+ verbose_name=_("user"),
+ related_name="webauthn_credentials",
+ on_delete=models.CASCADE,
+ )
+ name = models.CharField(verbose_name=_("device name"), max_length=100)
+ cred_id = models.CharField(
+ verbose_name=_("credential ID"), max_length=255, unique=True
+ )
+ public_key = models.TextField(verbose_name=_("public key"))
+ counter = models.BigIntegerField(verbose_name=_("sign counter"))
@cached_property
def webauthn_user(self):
@@ -369,32 +578,50 @@ def webauthn_user(self):
)
def __str__(self):
- return _('WebAuthn credential: %(name)s') % {'name': self.name}
+ return _("WebAuthn credential: %(name)s") % {"name": self.name}
class Meta:
- verbose_name = _('WebAuthn credential')
- verbose_name_plural = _('WebAuthn credentials')
+ verbose_name = _("WebAuthn credential")
+ verbose_name_plural = _("WebAuthn credentials")
class OrganizationRequest(models.Model):
- user = models.ForeignKey(Profile, verbose_name=_('user'), related_name='requests', on_delete=models.CASCADE)
- organization = models.ForeignKey(Organization, verbose_name=_('organization'), related_name='requests',
- on_delete=models.CASCADE)
- time = models.DateTimeField(verbose_name=_('request time'), auto_now_add=True)
- state = models.CharField(max_length=1, verbose_name=_('state'), choices=(
- ('P', _('Pending')),
- ('A', _('Approved')),
- ('R', _('Rejected')),
- ))
- request_class = models.ForeignKey(Class, verbose_name=_('class'), on_delete=models.CASCADE, null=True, blank=True)
- reason = models.TextField(verbose_name=_('reason'))
+ user = models.ForeignKey(
+ Profile,
+ verbose_name=_("user"),
+ related_name="requests",
+ on_delete=models.CASCADE,
+ )
+ organization = models.ForeignKey(
+ Organization,
+ verbose_name=_("organization"),
+ related_name="requests",
+ on_delete=models.CASCADE,
+ )
+ time = models.DateTimeField(verbose_name=_("request time"), auto_now_add=True)
+ state = models.CharField(
+ max_length=1,
+ verbose_name=_("state"),
+ choices=(
+ ("P", _("Pending")),
+ ("A", _("Approved")),
+ ("R", _("Rejected")),
+ ),
+ )
+ request_class = models.ForeignKey(
+ Class, verbose_name=_("class"), on_delete=models.CASCADE, null=True, blank=True
+ )
+ reason = models.TextField(verbose_name=_("reason"))
def clean(self):
if self.organization.class_required and self.request_class is None:
- raise ValidationError('Organization requires a class to be specified')
- if self.request_class and self.organization_id != self.request_class.organization_id:
- raise ValidationError('Class must be part of the organization')
+ raise ValidationError("Organization requires a class to be specified")
+ if (
+ self.request_class
+ and self.organization_id != self.request_class.organization_id
+ ):
+ raise ValidationError("Class must be part of the organization")
class Meta:
- verbose_name = _('organization join request')
- verbose_name_plural = _('organization join requests')
+ verbose_name = _("organization join request")
+ verbose_name_plural = _("organization join requests")
diff --git a/judge/models/runtime.py b/judge/models/runtime.py
index 313cbe79ea..c97bf2db5b 100644
--- a/judge/models/runtime.py
+++ b/judge/models/runtime.py
@@ -12,38 +12,82 @@
from judge.judgeapi import disconnect_judge, update_disable_judge
-__all__ = ['Language', 'RuntimeVersion', 'Judge']
+__all__ = ["Language", "RuntimeVersion", "Judge"]
class Language(models.Model):
- key = models.CharField(max_length=6, verbose_name=_('short identifier'),
- help_text=_('The identifier for this language; the same as its executor id for judges.'),
- unique=True)
- name = models.CharField(max_length=20, verbose_name=_('long name'),
- help_text=_('Longer name for the language, e.g. "Python 2" or "C++11".'))
- short_name = models.CharField(max_length=10, verbose_name=_('short name'),
- help_text=_('More readable, but short, name to display publicly; e.g. "PY2" or '
- '"C++11". If left blank, it will default to the '
- 'short identifier.'),
- null=True, blank=True)
- common_name = models.CharField(max_length=10, verbose_name=_('common name'),
- help_text=_('Common name for the language. For example, the common name for C++03, '
- 'C++11, and C++14 would be "C++".'))
- ace = models.CharField(max_length=20, verbose_name=_('ace mode name'),
- help_text=_('Language ID for Ace.js editor highlighting, appended to "mode-" to determine '
- 'the Ace JavaScript file to use, e.g., "python".'))
- pygments = models.CharField(max_length=20, verbose_name=_('pygments name'),
- help_text=_('Language ID for Pygments highlighting in source windows.'))
- template = models.TextField(verbose_name=_('code template'),
- help_text=_('Code template to display in submission editor.'), blank=True)
- info = models.CharField(max_length=50, verbose_name=_('runtime info override'), blank=True,
- help_text=_("Do not set this unless you know what you're doing! It will override the "
- 'usually more specific, judge-provided runtime info!'))
- description = models.TextField(verbose_name=_('language description'),
- help_text=_('Use this field to inform users of quirks with your environment, '
- 'additional restrictions, etc.'), blank=True)
- extension = models.CharField(max_length=10, verbose_name=_('extension'),
- help_text=_('The extension of source files, e.g., "py" or "cpp".'))
+ key = models.CharField(
+ max_length=6,
+ verbose_name=_("short identifier"),
+ help_text=_(
+ "The identifier for this language; the same as its executor id for judges."
+ ),
+ unique=True,
+ )
+ name = models.CharField(
+ max_length=20,
+ verbose_name=_("long name"),
+ help_text=_('Longer name for the language, e.g. "Python 2" or "C++11".'),
+ )
+ short_name = models.CharField(
+ max_length=10,
+ verbose_name=_("short name"),
+ help_text=_(
+ 'More readable, but short, name to display publicly; e.g. "PY2" or '
+ '"C++11". If left blank, it will default to the '
+ "short identifier."
+ ),
+ null=True,
+ blank=True,
+ )
+ common_name = models.CharField(
+ max_length=10,
+ verbose_name=_("common name"),
+ help_text=_(
+ "Common name for the language. For example, the common name for C++03, "
+ 'C++11, and C++14 would be "C++".'
+ ),
+ )
+ ace = models.CharField(
+ max_length=20,
+ verbose_name=_("ace mode name"),
+ help_text=_(
+ 'Language ID for Ace.js editor highlighting, appended to "mode-" to determine '
+ 'the Ace JavaScript file to use, e.g., "python".'
+ ),
+ )
+ pygments = models.CharField(
+ max_length=20,
+ verbose_name=_("pygments name"),
+ help_text=_("Language ID for Pygments highlighting in source windows."),
+ )
+ template = models.TextField(
+ verbose_name=_("code template"),
+ help_text=_("Code template to display in submission editor."),
+ blank=True,
+ )
+ info = models.CharField(
+ max_length=50,
+ verbose_name=_("runtime info override"),
+ blank=True,
+ help_text=_(
+ "Do not set this unless you know what you're doing! It will override the "
+ "usually more specific, judge-provided runtime info!"
+ ),
+ )
+ description = models.TextField(
+ verbose_name=_("language description"),
+ help_text=_(
+ "Use this field to inform users of quirks with your environment, "
+ "additional restrictions, etc."
+ ),
+ blank=True,
+ )
+ extension = models.CharField(
+ max_length=10,
+ verbose_name=_("extension"),
+ help_text=_('The extension of source files, e.g., "py" or "cpp".'),
+ )
def runtime_versions(self):
runtimes = OrderedDict()
@@ -52,25 +96,29 @@ def runtime_versions(self):
id = runtime.name
if id not in runtimes:
runtimes[id] = set()
- if not runtime.version: # empty str == error determining version on judge side
+ if (
+ not runtime.version
+ ): # empty str == error determining version on judge side
continue
runtimes[id].add(runtime.version)
lang_versions = []
for id, version_list in runtimes.items():
- lang_versions.append((id, sorted(version_list, key=lambda a: tuple(map(int, a.split('.'))))))
+ lang_versions.append(
+ (id, sorted(version_list, key=lambda a: tuple(map(int, a.split(".")))))
+ )
return lang_versions
@classmethod
def get_common_name_map(cls):
- result = cache.get('lang:cn_map')
+ result = cache.get("lang:cn_map")
if result is not None:
return result
result = defaultdict(set)
- for id, cn in Language.objects.values_list('id', 'common_name'):
+ for id, cn in Language.objects.values_list("id", "common_name"):
result[cn].add(id)
result = {id: cns for id, cns in result.items() if len(cns) > 1}
- cache.set('lang:cn_map', result, 86400)
+ cache.set("lang:cn_map", result, 86400)
return result
@cached_property
@@ -83,17 +131,19 @@ def __str__(self):
@cached_property
def display_name(self):
if self.info:
- return '%s (%s)' % (self.name, self.info)
+ return "%s (%s)" % (self.name, self.info)
else:
return self.name
@classmethod
def get_python3(cls):
# We really need a default language, and this app is in Python 3
- return Language.objects.get_or_create(key='PY3', defaults={'name': 'Python 3'})[0]
+ return Language.objects.get_or_create(key="PY3", defaults={"name": "Python 3"})[
+ 0
+ ]
def get_absolute_url(self):
- return reverse('runtime_list') + '#' + self.key
+ return reverse("runtime_list") + "#" + self.key
@classmethod
def get_default_language(cls):
@@ -107,39 +157,75 @@ def get_default_language_pk(cls):
return cls.get_default_language().pk
class Meta:
- ordering = ['key']
- verbose_name = _('language')
- verbose_name_plural = _('languages')
+ ordering = ["key"]
+ verbose_name = _("language")
+ verbose_name_plural = _("languages")
class RuntimeVersion(models.Model):
- language = models.ForeignKey(Language, verbose_name=_('language to which this runtime belongs'), on_delete=CASCADE)
- judge = models.ForeignKey('Judge', verbose_name=_('judge on which this runtime exists'), on_delete=CASCADE)
- name = models.CharField(max_length=64, verbose_name=_('runtime name'))
- version = models.CharField(max_length=64, verbose_name=_('runtime version'), blank=True)
- priority = models.IntegerField(verbose_name=_('order in which to display this runtime'), default=0)
+ language = models.ForeignKey(
+ Language,
+ verbose_name=_("language to which this runtime belongs"),
+ on_delete=CASCADE,
+ )
+ judge = models.ForeignKey(
+ "Judge", verbose_name=_("judge on which this runtime exists"), on_delete=CASCADE
+ )
+ name = models.CharField(max_length=64, verbose_name=_("runtime name"))
+ version = models.CharField(
+ max_length=64, verbose_name=_("runtime version"), blank=True
+ )
+ priority = models.IntegerField(
+ verbose_name=_("order in which to display this runtime"), default=0
+ )
class Judge(models.Model):
- name = models.CharField(max_length=50, verbose_name=_('judge name'), help_text=_('Server name, hostname-style.'),
- unique=True)
- created = models.DateTimeField(auto_now_add=True, verbose_name=_('time of creation'))
- auth_key = models.CharField(max_length=100, help_text=_('A key to authenticate this judge.'),
- verbose_name=_('authentication key'))
- is_blocked = models.BooleanField(verbose_name=_('block judge'), default=False,
- help_text=_('Whether this judge should be blocked from connecting, '
- 'even if its key is correct.'))
- is_disabled = models.BooleanField(verbose_name=_('disable judge'), default=False,
- help_text=_('Whether this judge should be removed from judging queue.'))
- online = models.BooleanField(verbose_name=_('judge online status'), default=False)
- start_time = models.DateTimeField(verbose_name=_('judge start time'), null=True)
- ping = models.FloatField(verbose_name=_('response time'), null=True)
- load = models.FloatField(verbose_name=_('system load'), null=True,
- help_text=_('Load for the last minute, divided by processors to be fair.'))
- description = models.TextField(blank=True, verbose_name=_('description'))
- last_ip = models.GenericIPAddressField(verbose_name=_('last connected IP'), blank=True, null=True)
- problems = models.ManyToManyField('Problem', verbose_name=_('problems'), related_name='judges')
- runtimes = models.ManyToManyField(Language, verbose_name=_('judges'), related_name='judges')
+ name = models.CharField(
+ max_length=50,
+ verbose_name=_("judge name"),
+ help_text=_("Server name, hostname-style."),
+ unique=True,
+ )
+ created = models.DateTimeField(
+ auto_now_add=True, verbose_name=_("time of creation")
+ )
+ auth_key = models.CharField(
+ max_length=100,
+ help_text=_("A key to authenticate this judge."),
+ verbose_name=_("authentication key"),
+ )
+ is_blocked = models.BooleanField(
+ verbose_name=_("block judge"),
+ default=False,
+ help_text=_(
+ "Whether this judge should be blocked from connecting, "
+ "even if its key is correct."
+ ),
+ )
+ is_disabled = models.BooleanField(
+ verbose_name=_("disable judge"),
+ default=False,
+ help_text=_("Whether this judge should be removed from judging queue."),
+ )
+ online = models.BooleanField(verbose_name=_("judge online status"), default=False)
+ start_time = models.DateTimeField(verbose_name=_("judge start time"), null=True)
+ ping = models.FloatField(verbose_name=_("response time"), null=True)
+ load = models.FloatField(
+ verbose_name=_("system load"),
+ null=True,
+ help_text=_("Load for the last minute, divided by processors to be fair."),
+ )
+ description = models.TextField(blank=True, verbose_name=_("description"))
+ last_ip = models.GenericIPAddressField(
+ verbose_name=_("last connected IP"), blank=True, null=True
+ )
+ problems = models.ManyToManyField(
+ "Problem", verbose_name=_("problems"), related_name="judges"
+ )
+ runtimes = models.ManyToManyField(
+ Language, verbose_name=_("judges"), related_name="judges"
+ )
def __str__(self):
return self.name
@@ -152,30 +238,32 @@ def disconnect(self, force=False):
def toggle_disabled(self):
self.is_disabled = not self.is_disabled
update_disable_judge(self)
- self.save(update_fields=['is_disabled'])
+ self.save(update_fields=["is_disabled"])
toggle_disabled.alters_data = True
@classmethod
def runtime_versions(cls):
- qs = (RuntimeVersion.objects.filter(judge__online=True)
- .values('judge__name', 'language__key', 'language__name', 'version', 'name')
- .order_by('language__key', 'priority'))
+ qs = (
+ RuntimeVersion.objects.filter(judge__online=True)
+ .values("judge__name", "language__key", "language__name", "version", "name")
+ .order_by("language__key", "priority")
+ )
ret = defaultdict(OrderedDict)
for data in qs:
- judge = data['judge__name']
- key = data['language__key']
+ judge = data["judge__name"]
+ key = data["language__key"]
if key not in ret:
- ret[judge][key] = {'name': data['language__name'], 'runtime': []}
- ret[judge][key]['runtime'].append((data['name'], (data['version'],)))
+ ret[judge][key] = {"name": data["language__name"], "runtime": []}
+ ret[judge][key]["runtime"].append((data["name"], (data["version"],)))
return {judge: list(data.items()) for judge, data in ret.items()}
@cached_property
def uptime(self):
- return timezone.now() - self.start_time if self.online else 'N/A'
+ return timezone.now() - self.start_time if self.online else "N/A"
@cached_property
def ping_ms(self):
@@ -183,9 +271,9 @@ def ping_ms(self):
@cached_property
def runtime_list(self):
- return map(attrgetter('name'), self.runtimes.all())
+ return map(attrgetter("name"), self.runtimes.all())
class Meta:
- ordering = ['name']
- verbose_name = _('judge')
- verbose_name_plural = _('judges')
+ ordering = ["name"]
+ verbose_name = _("judge")
+ verbose_name_plural = _("judges")
diff --git a/judge/models/submission.py b/judge/models/submission.py
index 5aa6c67659..c8cd0861ed 100644
--- a/judge/models/submission.py
+++ b/judge/models/submission.py
@@ -16,93 +16,137 @@
from judge.models.runtime import Language
from judge.utils.unicode import utf8bytes
-__all__ = ['SUBMISSION_RESULT', 'Submission', 'SubmissionSource', 'SubmissionTestCase']
+__all__ = ["SUBMISSION_RESULT", "Submission", "SubmissionSource", "SubmissionTestCase"]
SUBMISSION_RESULT = (
- ('AC', _('Accepted')),
- ('WA', _('Wrong Answer')),
- ('TLE', _('Time Limit Exceeded')),
- ('MLE', _('Memory Limit Exceeded')),
- ('OLE', _('Output Limit Exceeded')),
- ('IR', _('Invalid Return')),
- ('RTE', _('Runtime Error')),
- ('CE', _('Compile Error')),
- ('IE', _('Internal Error')),
- ('SC', _('Short Circuited')),
- ('AB', _('Aborted')),
+ ("AC", _("Accepted")),
+ ("WA", _("Wrong Answer")),
+ ("TLE", _("Time Limit Exceeded")),
+ ("MLE", _("Memory Limit Exceeded")),
+ ("OLE", _("Output Limit Exceeded")),
+ ("IR", _("Invalid Return")),
+ ("RTE", _("Runtime Error")),
+ ("CE", _("Compile Error")),
+ ("IE", _("Internal Error")),
+ ("SC", _("Short Circuited")),
+ ("AB", _("Aborted")),
)
-@revisions.register(follow=['test_cases'])
+@revisions.register(follow=["test_cases"])
class Submission(models.Model):
STATUS = (
- ('QU', _('Queued')),
- ('P', _('Processing')),
- ('G', _('Grading')),
- ('D', _('Completed')),
- ('IE', _('Internal Error')),
- ('CE', _('Compile Error')),
- ('AB', _('Aborted')),
+ ("QU", _("Queued")),
+ ("P", _("Processing")),
+ ("G", _("Grading")),
+ ("D", _("Completed")),
+ ("IE", _("Internal Error")),
+ ("CE", _("Compile Error")),
+ ("AB", _("Aborted")),
)
- IN_PROGRESS_GRADING_STATUS = ('QU', 'P', 'G')
+ IN_PROGRESS_GRADING_STATUS = ("QU", "P", "G")
RESULT = SUBMISSION_RESULT
USER_DISPLAY_CODES = {
- 'AC': _('Accepted'),
- 'WA': _('Wrong Answer'),
- 'SC': _('Short Circuited'),
- 'TLE': _('Time Limit Exceeded'),
- 'MLE': _('Memory Limit Exceeded'),
- 'OLE': _('Output Limit Exceeded'),
- 'IR': _('Invalid Return'),
- 'RTE': _('Runtime Error'),
- 'CE': _('Compile Error'),
- 'IE': _('Internal Error (judging server error)'),
- 'QU': _('Queued'),
- 'P': _('Processing'),
- 'G': _('Grading'),
- 'D': _('Completed'),
- 'AB': _('Aborted'),
+ "AC": _("Accepted"),
+ "WA": _("Wrong Answer"),
+ "SC": _("Short Circuited"),
+ "TLE": _("Time Limit Exceeded"),
+ "MLE": _("Memory Limit Exceeded"),
+ "OLE": _("Output Limit Exceeded"),
+ "IR": _("Invalid Return"),
+ "RTE": _("Runtime Error"),
+ "CE": _("Compile Error"),
+ "IE": _("Internal Error (judging server error)"),
+ "QU": _("Queued"),
+ "P": _("Processing"),
+ "G": _("Grading"),
+ "D": _("Completed"),
+ "AB": _("Aborted"),
}
- user = models.ForeignKey(Profile, verbose_name=_('user'), on_delete=models.CASCADE, db_index=False)
- problem = models.ForeignKey(Problem, verbose_name=_('problem'), on_delete=models.CASCADE, db_index=False)
- date = models.DateTimeField(verbose_name=_('submission time'), auto_now_add=True, db_index=True)
- time = models.FloatField(verbose_name=_('execution time'), null=True)
- memory = models.FloatField(verbose_name=_('memory usage'), null=True)
- points = models.FloatField(verbose_name=_('points granted'), null=True)
- language = models.ForeignKey(Language, verbose_name=_('submission language'),
- on_delete=models.CASCADE, db_index=False)
- status = models.CharField(verbose_name=_('status'), max_length=2, choices=STATUS, default='QU', db_index=True)
- result = models.CharField(verbose_name=_('result'), max_length=3, choices=SUBMISSION_RESULT,
- default=None, null=True, blank=True)
- error = models.TextField(verbose_name=_('compile errors'), null=True, blank=True)
+ user = models.ForeignKey(
+ Profile, verbose_name=_("user"), on_delete=models.CASCADE, db_index=False
+ )
+ problem = models.ForeignKey(
+ Problem, verbose_name=_("problem"), on_delete=models.CASCADE, db_index=False
+ )
+ date = models.DateTimeField(
+ verbose_name=_("submission time"), auto_now_add=True, db_index=True
+ )
+ time = models.FloatField(verbose_name=_("execution time"), null=True)
+ memory = models.FloatField(verbose_name=_("memory usage"), null=True)
+ points = models.FloatField(verbose_name=_("points granted"), null=True)
+ language = models.ForeignKey(
+ Language,
+ verbose_name=_("submission language"),
+ on_delete=models.CASCADE,
+ db_index=False,
+ )
+ status = models.CharField(
+ verbose_name=_("status"),
+ max_length=2,
+ choices=STATUS,
+ default="QU",
+ db_index=True,
+ )
+ result = models.CharField(
+ verbose_name=_("result"),
+ max_length=3,
+ choices=SUBMISSION_RESULT,
+ default=None,
+ null=True,
+ blank=True,
+ )
+ error = models.TextField(verbose_name=_("compile errors"), null=True, blank=True)
current_testcase = models.IntegerField(default=0)
- batch = models.BooleanField(verbose_name=_('batched cases'), default=False)
- case_points = models.FloatField(verbose_name=_('test case points'), default=0)
- case_total = models.FloatField(verbose_name=_('test case total points'), default=0)
- judged_on = models.ForeignKey('Judge', verbose_name=_('judged on'), null=True, blank=True,
- on_delete=models.SET_NULL)
- judged_date = models.DateTimeField(verbose_name=_('submission judge time'), default=None, null=True)
- rejudged_date = models.DateTimeField(verbose_name=_('last rejudge date by admin'), null=True, blank=True)
- is_pretested = models.BooleanField(verbose_name=_('was ran on pretests only'), default=False)
- contest_object = models.ForeignKey('Contest', verbose_name=_('contest'), null=True, blank=True,
- on_delete=models.SET_NULL, related_name='+', db_index=False)
- locked_after = models.DateTimeField(verbose_name=_('submission lock'), null=True, blank=True)
+ batch = models.BooleanField(verbose_name=_("batched cases"), default=False)
+ case_points = models.FloatField(verbose_name=_("test case points"), default=0)
+ case_total = models.FloatField(verbose_name=_("test case total points"), default=0)
+ judged_on = models.ForeignKey(
+ "Judge",
+ verbose_name=_("judged on"),
+ null=True,
+ blank=True,
+ on_delete=models.SET_NULL,
+ )
+ judged_date = models.DateTimeField(
+ verbose_name=_("submission judge time"), default=None, null=True
+ )
+ rejudged_date = models.DateTimeField(
+ verbose_name=_("last rejudge date by admin"), null=True, blank=True
+ )
+ is_pretested = models.BooleanField(
+ verbose_name=_("was ran on pretests only"), default=False
+ )
+ contest_object = models.ForeignKey(
+ "Contest",
+ verbose_name=_("contest"),
+ null=True,
+ blank=True,
+ on_delete=models.SET_NULL,
+ related_name="+",
+ db_index=False,
+ )
+ locked_after = models.DateTimeField(
+ verbose_name=_("submission lock"), null=True, blank=True
+ )
@classmethod
def result_class_from_code(cls, result, case_points, case_total):
- if result == 'AC':
+ if result == "AC":
if case_points == case_total:
- return 'AC'
- return '_AC'
+ return "AC"
+ return "_AC"
return result
@property
def result_class(self):
# This exists to save all these conditionals from being executed (slowly) in each row.html template
- if self.status in ('IE', 'CE'):
+ if self.status in ("IE", "CE"):
return self.status
- return Submission.result_class_from_code(self.result, self.case_points, self.case_total)
+ return Submission.result_class_from_code(
+ self.result, self.case_points, self.case_total
+ )
@property
def memory_bytes(self):
@@ -114,19 +158,21 @@ def short_status(self):
@property
def long_status(self):
- return Submission.USER_DISPLAY_CODES.get(self.short_status, '')
+ return Submission.USER_DISPLAY_CODES.get(self.short_status, "")
@cached_property
def is_locked(self):
return self.locked_after is not None and self.locked_after < timezone.now()
- def judge(self, *args, rejudge=False, force_judge=False, rejudge_user=None, **kwargs):
+ def judge(
+ self, *args, rejudge=False, force_judge=False, rejudge_user=None, **kwargs
+ ):
if force_judge or not self.is_locked:
if rejudge:
with revisions.create_revision(manage_manually=True):
if rejudge_user:
revisions.set_user(rejudge_user)
- revisions.set_comment('Rejudged')
+ revisions.set_comment("Rejudged")
revisions.add_to_revision(self)
judge_submission(self, *args, rejudge=rejudge, **kwargs)
@@ -144,26 +190,35 @@ def can_see_detail(self, user):
source_visibility = self.problem.submission_source_visibility
if self.problem.is_editable_by(user):
return True
- elif user.has_perm('judge.view_all_submission'):
+ elif user.has_perm("judge.view_all_submission"):
return True
elif self.user_id == profile.id:
return True
elif source_visibility == SubmissionSourceAccess.ALWAYS:
return True
- elif source_visibility == SubmissionSourceAccess.SOLVED and \
- (self.problem.is_public or self.problem.testers.filter(id=profile.id).exists()) and \
- self.problem.is_solved_by(user):
+ elif (
+ source_visibility == SubmissionSourceAccess.SOLVED
+ and (
+ self.problem.is_public
+ or self.problem.testers.filter(id=profile.id).exists()
+ )
+ and self.problem.is_solved_by(user)
+ ):
return True
- elif source_visibility == SubmissionSourceAccess.ONLY_OWN and \
- self.problem.testers.filter(id=profile.id).exists():
+ elif (
+ source_visibility == SubmissionSourceAccess.ONLY_OWN
+ and self.problem.testers.filter(id=profile.id).exists()
+ ):
return True
contest = self.contest_object
# If user is an author or curator of the contest the submission was made in, or they can see in-contest subs
if contest is not None and (
- user.profile.id in contest.editor_ids or
- contest.view_contest_submissions.filter(id=user.profile.id).exists() or
- (contest.tester_see_submissions and user.profile.id in contest.tester_ids)
+ user.profile.id in contest.editor_ids
+ or contest.view_contest_submissions.filter(id=user.profile.id).exists()
+ or (
+ contest.tester_see_submissions and user.profile.id in contest.tester_ids
+ )
):
return True
@@ -176,8 +231,12 @@ def update_contest(self):
return
contest_problem = contest.problem
- contest.points = round(self.case_points / self.case_total * contest_problem.points
- if self.case_total > 0 else 0, 3)
+ contest.points = round(
+ self.case_points / self.case_total * contest_problem.points
+ if self.case_total > 0
+ else 0,
+ 3,
+ )
if not contest_problem.partial and contest.points != contest_problem.points:
contest.points = 0
contest.save()
@@ -187,20 +246,22 @@ def update_contest(self):
@property
def is_graded(self):
- return self.status not in ('QU', 'P', 'G')
+ return self.status not in ("QU", "P", "G")
@cached_property
def contest_key(self):
- if hasattr(self, 'contest'):
+ if hasattr(self, "contest"):
return self.contest_object.key
def __str__(self):
- return _('Submission %(id)d of %(problem)s by %(user)s') % {
- 'id': self.id, 'problem': self.problem, 'user': self.user.user.username,
+ return _("Submission %(id)d of %(problem)s by %(user)s") % {
+ "id": self.id,
+ "problem": self.problem,
+ "user": self.user.user.username,
}
def get_absolute_url(self):
- return reverse('submission_status', args=(self.id,))
+ return reverse("submission_status", args=(self.id,))
@cached_property
def contest_or_none(self):
@@ -211,8 +272,14 @@ def contest_or_none(self):
@classmethod
def get_id_secret(cls, sub_id):
- return (hmac.new(utf8bytes(settings.EVENT_DAEMON_SUBMISSION_KEY), b'%d' % sub_id, hashlib.sha512)
- .hexdigest()[:16] + '%08x' % sub_id)
+ return (
+ hmac.new(
+ utf8bytes(settings.EVENT_DAEMON_SUBMISSION_KEY),
+ b"%d" % sub_id,
+ hashlib.sha512,
+ ).hexdigest()[:16]
+ + "%08x" % sub_id
+ )
@cached_property
def id_secret(self):
@@ -220,85 +287,96 @@ def id_secret(self):
class Meta:
permissions = (
- ('abort_any_submission', _('Abort any submission')),
- ('rejudge_submission', _('Rejudge the submission')),
- ('rejudge_submission_lot', _('Rejudge a lot of submissions')),
- ('spam_submission', _('Submit without limit')),
- ('view_all_submission', _('View all submission')),
- ('resubmit_other', _("Resubmit others' submission")),
- ('lock_submission', _('Change lock status of submission')),
+ ("abort_any_submission", _("Abort any submission")),
+ ("rejudge_submission", _("Rejudge the submission")),
+ ("rejudge_submission_lot", _("Rejudge a lot of submissions")),
+ ("spam_submission", _("Submit without limit")),
+ ("view_all_submission", _("View all submission")),
+ ("resubmit_other", _("Resubmit others' submission")),
+ ("lock_submission", _("Change lock status of submission")),
)
- verbose_name = _('submission')
- verbose_name_plural = _('submissions')
+ verbose_name = _("submission")
+ verbose_name_plural = _("submissions")
indexes = [
# For problem submission rankings
- models.Index(fields=['problem', 'user', '-points', '-time']),
-
+ models.Index(fields=["problem", "user", "-points", "-time"]),
# For contest problem submission rankings
- models.Index(fields=['contest_object', 'problem', 'user', '-points', '-time']),
-
+ models.Index(
+ fields=["contest_object", "problem", "user", "-points", "-time"]
+ ),
# For main submission list filtering by some combination of result and language
- models.Index(fields=['result', '-id']),
- models.Index(fields=['result', 'language', '-id']),
- models.Index(fields=['language', '-id']),
-
+ models.Index(fields=["result", "-id"]),
+ models.Index(fields=["result", "language", "-id"]),
+ models.Index(fields=["language", "-id"]),
# For filtered main submission list result charts
- models.Index(fields=['result', 'problem']),
- models.Index(fields=['language', 'problem', 'result']),
-
+ models.Index(fields=["result", "problem"]),
+ models.Index(fields=["language", "problem", "result"]),
# For problem submissions result chart
- models.Index(fields=['problem', 'result']),
-
+ models.Index(fields=["problem", "result"]),
# For user_attempted_ids and own problem submissions result chart
- models.Index(fields=['user', 'problem', 'result']),
-
+ models.Index(fields=["user", "problem", "result"]),
# For user_completed_ids
- models.Index(fields=['user', 'result']),
+ models.Index(fields=["user", "result"]),
]
class SubmissionSource(models.Model):
- submission = models.OneToOneField(Submission, on_delete=models.CASCADE, verbose_name=_('associated submission'),
- related_name='source')
- source = models.TextField(verbose_name=_('source code'), max_length=65536)
+ submission = models.OneToOneField(
+ Submission,
+ on_delete=models.CASCADE,
+ verbose_name=_("associated submission"),
+ related_name="source",
+ )
+ source = models.TextField(verbose_name=_("source code"), max_length=65536)
def __str__(self):
- return _('Source of %(submission)s') % {'submission': self.submission}
+ return _("Source of %(submission)s") % {"submission": self.submission}
class Meta:
- verbose_name = _('submission source')
- verbose_name_plural = _('submission sources')
+ verbose_name = _("submission source")
+ verbose_name_plural = _("submission sources")
@revisions.register()
class SubmissionTestCase(models.Model):
RESULT = SUBMISSION_RESULT
- submission = models.ForeignKey(Submission, verbose_name=_('associated submission'), db_index=False,
- related_name='test_cases', on_delete=models.CASCADE)
- case = models.IntegerField(verbose_name=_('test case ID'))
- status = models.CharField(max_length=3, verbose_name=_('status flag'), choices=SUBMISSION_RESULT)
- time = models.FloatField(verbose_name=_('execution time'), null=True)
- memory = models.FloatField(verbose_name=_('memory usage'), null=True)
- points = models.FloatField(verbose_name=_('points granted'), null=True)
- total = models.FloatField(verbose_name=_('points possible'), null=True)
- batch = models.IntegerField(verbose_name=_('batch number'), null=True)
- feedback = models.CharField(max_length=50, verbose_name=_('judging feedback'), blank=True)
- extended_feedback = models.TextField(verbose_name=_('extended judging feedback'), blank=True)
- output = models.TextField(verbose_name=_('program output'), blank=True)
+ submission = models.ForeignKey(
+ Submission,
+ verbose_name=_("associated submission"),
+ db_index=False,
+ related_name="test_cases",
+ on_delete=models.CASCADE,
+ )
+ case = models.IntegerField(verbose_name=_("test case ID"))
+ status = models.CharField(
+ max_length=3, verbose_name=_("status flag"), choices=SUBMISSION_RESULT
+ )
+ time = models.FloatField(verbose_name=_("execution time"), null=True)
+ memory = models.FloatField(verbose_name=_("memory usage"), null=True)
+ points = models.FloatField(verbose_name=_("points granted"), null=True)
+ total = models.FloatField(verbose_name=_("points possible"), null=True)
+ batch = models.IntegerField(verbose_name=_("batch number"), null=True)
+ feedback = models.CharField(
+ max_length=50, verbose_name=_("judging feedback"), blank=True
+ )
+ extended_feedback = models.TextField(
+ verbose_name=_("extended judging feedback"), blank=True
+ )
+ output = models.TextField(verbose_name=_("program output"), blank=True)
@property
def long_status(self):
- return Submission.USER_DISPLAY_CODES.get(self.status, '')
+ return Submission.USER_DISPLAY_CODES.get(self.status, "")
@property
def result_class(self):
- if self.status in ('IE', 'CE'):
+ if self.status in ("IE", "CE"):
return self.status
return Submission.result_class_from_code(self.status, self.points, self.total)
class Meta:
- unique_together = ('submission', 'case')
- verbose_name = _('submission test case')
- verbose_name_plural = _('submission test cases')
+ unique_together = ("submission", "case")
+ verbose_name = _("submission test case")
+ verbose_name_plural = _("submission test cases")
diff --git a/judge/models/tests/test_blogpost.py b/judge/models/tests/test_blogpost.py
index 1beadde289..07423f610d 100644
--- a/judge/models/tests/test_blogpost.py
+++ b/judge/models/tests/test_blogpost.py
@@ -7,26 +7,28 @@ class BlogPostTestCase(CommonDataMixin, TestCase):
@classmethod
def setUpTestData(self):
super().setUpTestData()
- self.users.update({
- 'staff_blogpost_edit_own': create_user(
- username='staff_blogpost_edit_own',
- is_staff=True,
- user_permissions=('change_blogpost',),
- ),
- 'staff_blogpost_edit_all': create_user(
- username='staff_blogpost_edit_all',
- is_staff=True,
- user_permissions=('change_blogpost', 'edit_all_post'),
- ),
- })
+ self.users.update(
+ {
+ "staff_blogpost_edit_own": create_user(
+ username="staff_blogpost_edit_own",
+ is_staff=True,
+ user_permissions=("change_blogpost",),
+ ),
+ "staff_blogpost_edit_all": create_user(
+ username="staff_blogpost_edit_all",
+ is_staff=True,
+ user_permissions=("change_blogpost", "edit_all_post"),
+ ),
+ }
+ )
self.basic_blogpost = create_blogpost(
- title='basic',
- authors=('staff_blogpost_edit_own',),
+ title="basic",
+ authors=("staff_blogpost_edit_own",),
)
self.visible_blogpost = create_blogpost(
- title='visible',
+ title="visible",
visible=True,
)
@@ -35,46 +37,46 @@ def test_basic_blogpost(self):
def test_basic_blogpost_methods(self):
data = {
- 'superuser': {
- 'can_see': self.assertTrue,
- 'is_editable_by': self.assertTrue,
+ "superuser": {
+ "can_see": self.assertTrue,
+ "is_editable_by": self.assertTrue,
},
- 'staff_blogpost_edit_own': {
- 'can_see': self.assertTrue,
- 'is_editable_by': self.assertTrue,
+ "staff_blogpost_edit_own": {
+ "can_see": self.assertTrue,
+ "is_editable_by": self.assertTrue,
},
- 'staff_blogpost_edit_all': {
- 'can_see': self.assertTrue,
- 'is_editable_by': self.assertTrue,
+ "staff_blogpost_edit_all": {
+ "can_see": self.assertTrue,
+ "is_editable_by": self.assertTrue,
},
- 'normal': {
- 'can_see': self.assertFalse,
- 'is_editable_by': self.assertFalse,
+ "normal": {
+ "can_see": self.assertFalse,
+ "is_editable_by": self.assertFalse,
},
- 'anonymous': {
- 'can_see': self.assertFalse,
- 'is_editable_by': self.assertFalse,
+ "anonymous": {
+ "can_see": self.assertFalse,
+ "is_editable_by": self.assertFalse,
},
}
self._test_object_methods_with_users(self.basic_blogpost, data)
def test_visible_blogpost_methods(self):
data = {
- 'superuser': {
- 'can_see': self.assertTrue,
- 'is_editable_by': self.assertTrue,
+ "superuser": {
+ "can_see": self.assertTrue,
+ "is_editable_by": self.assertTrue,
},
- 'staff_blogpost_edit_own': {
- 'can_see': self.assertTrue,
- 'is_editable_by': self.assertFalse,
+ "staff_blogpost_edit_own": {
+ "can_see": self.assertTrue,
+ "is_editable_by": self.assertFalse,
},
- 'normal': {
- 'can_see': self.assertTrue,
- 'is_editable_by': self.assertFalse,
+ "normal": {
+ "can_see": self.assertTrue,
+ "is_editable_by": self.assertFalse,
},
- 'anonymous': {
- 'can_see': self.assertTrue,
- 'is_editable_by': self.assertFalse,
+ "anonymous": {
+ "can_see": self.assertTrue,
+ "is_editable_by": self.assertFalse,
},
}
self._test_object_methods_with_users(self.visible_blogpost, data)
diff --git a/judge/models/tests/test_contest.py b/judge/models/tests/test_contest.py
index 5be197e22b..74a2139c1b 100644
--- a/judge/models/tests/test_contest.py
+++ b/judge/models/tests/test_contest.py
@@ -4,69 +4,78 @@
from judge.models import Contest, ContestParticipation, ContestTag
from judge.models.contest import MinValueOrNoneValidator
-from judge.models.tests.util import CommonDataMixin, create_contest, create_contest_participation, create_user
+from judge.models.tests.util import (
+ CommonDataMixin,
+ create_contest,
+ create_contest_participation,
+ create_user,
+)
class ContestTestCase(CommonDataMixin, TestCase):
@classmethod
def setUpTestData(self):
super().setUpTestData()
- self.users.update({
- 'staff_contest_edit_own': create_user(
- username='staff_contest_edit_own',
- is_staff=True,
- user_permissions=('edit_own_contest',),
- ),
- 'staff_contest_see_all': create_user(
- username='staff_contest_see_all',
- user_permissions=('see_private_contest',),
- ),
- 'staff_contest_edit_all': create_user(
- username='staff_contest_edit_all',
- is_staff=True,
- user_permissions=('edit_own_contest', 'edit_all_contest'),
- ),
- 'normal_during_window': create_user(
- username='normal_during_window',
- ),
- 'normal_after_window': create_user(
- username='normal_after_window',
- ),
- 'normal_before_window': create_user(
- username='normal_before_window',
- ),
- 'non_staff_author': create_user(
- username='non_staff_author',
- is_staff=False,
- ),
- 'non_staff_tester': create_user(
- username='non_staff_tester',
- is_staff=False,
- ),
- 'normal_open_org': create_user(
- username='normal_open_org',
- is_staff=False,
- ),
- 'non_staff_spectator': create_user(
- username='non_staff_spectator',
- is_staff=False,
- ),
- })
-
- self.users['normal_open_org'].profile.organizations.add(self.organizations['open'])
+ self.users.update(
+ {
+ "staff_contest_edit_own": create_user(
+ username="staff_contest_edit_own",
+ is_staff=True,
+ user_permissions=("edit_own_contest",),
+ ),
+ "staff_contest_see_all": create_user(
+ username="staff_contest_see_all",
+ user_permissions=("see_private_contest",),
+ ),
+ "staff_contest_edit_all": create_user(
+ username="staff_contest_edit_all",
+ is_staff=True,
+ user_permissions=("edit_own_contest", "edit_all_contest"),
+ ),
+ "normal_during_window": create_user(
+ username="normal_during_window",
+ ),
+ "normal_after_window": create_user(
+ username="normal_after_window",
+ ),
+ "normal_before_window": create_user(
+ username="normal_before_window",
+ ),
+ "non_staff_author": create_user(
+ username="non_staff_author",
+ is_staff=False,
+ ),
+ "non_staff_tester": create_user(
+ username="non_staff_tester",
+ is_staff=False,
+ ),
+ "normal_open_org": create_user(
+ username="normal_open_org",
+ is_staff=False,
+ ),
+ "non_staff_spectator": create_user(
+ username="non_staff_spectator",
+ is_staff=False,
+ ),
+ }
+ )
+
+ self.users["normal_open_org"].profile.organizations.add(
+ self.organizations["open"]
+ )
_now = timezone.now()
self.basic_contest = create_contest(
- key='basic',
+ key="basic",
start_time=_now - timezone.timedelta(days=1),
end_time=_now + timezone.timedelta(days=100),
- authors=('superuser', 'staff_contest_edit_own'),
- testers=('non_staff_tester',),
+ authors=("superuser", "staff_contest_edit_own"),
+ testers=("non_staff_tester",),
)
self.hidden_scoreboard_contest = create_contest(
- key='hidden_scoreboard',
+ key="hidden_scoreboard",
start_time=_now - timezone.timedelta(days=1),
end_time=_now + timezone.timedelta(days=100),
is_visible=True,
@@ -76,220 +85,228 @@ def setUpTestData(self):
return tostring(math.floor(n))
end
""",
- spectators=('non_staff_spectator',),
+ spectators=("non_staff_spectator",),
)
self.hidden_scoreboard_non_staff_author = create_contest(
- key='non_staff_author',
+ key="non_staff_author",
start_time=_now - timezone.timedelta(days=1),
end_time=_now + timezone.timedelta(days=100),
is_visible=True,
scoreboard_visibility=Contest.SCOREBOARD_AFTER_CONTEST,
- authors=('non_staff_author',),
- curators=('staff_contest_edit_own',),
+ authors=("non_staff_author",),
+ curators=("staff_contest_edit_own",),
)
self.contest_hidden_scoreboard_contest = create_contest(
- key='contest_scoreboard',
+ key="contest_scoreboard",
start_time=_now - timezone.timedelta(days=10),
end_time=_now + timezone.timedelta(days=100),
time_limit=timezone.timedelta(days=1),
is_visible=True,
scoreboard_visibility=Contest.SCOREBOARD_AFTER_CONTEST,
- testers=('non_staff_tester',),
+ testers=("non_staff_tester",),
)
self.particip_hidden_scoreboard_contest = create_contest(
- key='particip_scoreboard',
+ key="particip_scoreboard",
start_time=_now - timezone.timedelta(days=10),
end_time=_now + timezone.timedelta(days=100),
time_limit=timezone.timedelta(days=1),
is_visible=True,
scoreboard_visibility=Contest.SCOREBOARD_AFTER_PARTICIPATION,
- testers=('non_staff_tester',),
+ testers=("non_staff_tester",),
)
self.visible_scoreboard_contest = create_contest(
- key='visible_scoreboard',
+ key="visible_scoreboard",
start_time=_now - timezone.timedelta(days=10),
end_time=_now + timezone.timedelta(days=100),
time_limit=timezone.timedelta(days=1),
is_visible=True,
scoreboard_visibility=Contest.SCOREBOARD_VISIBLE,
- testers=('non_staff_tester',),
+ testers=("non_staff_tester",),
)
self.full_hidden_scoreboard_contest = create_contest(
- key='full_hidden_board',
+ key="full_hidden_board",
start_time=_now - timezone.timedelta(days=100),
end_time=_now - timezone.timedelta(days=1),
time_limit=timezone.timedelta(days=1),
is_visible=True,
scoreboard_visibility=Contest.SCOREBOARD_HIDDEN,
- authors=('non_staff_author',),
- curators=('staff_contest_edit_own',),
- testers=('non_staff_tester',),
- spectators=('non_staff_spectator',),
+ authors=("non_staff_author",),
+ curators=("staff_contest_edit_own",),
+ testers=("non_staff_tester",),
+ spectators=("non_staff_spectator",),
)
self.future_contest = create_contest(
- key='future_contest',
+ key="future_contest",
start_time=_now + timezone.timedelta(days=3),
end_time=_now + timezone.timedelta(days=5),
time_limit=timezone.timedelta(days=1),
is_visible=True,
scoreboard_visibility=Contest.SCOREBOARD_AFTER_CONTEST,
- authors=('non_staff_author',),
- curators=('staff_contest_edit_own',),
- testers=('non_staff_tester',),
- spectators=('non_staff_spectator',),
+ authors=("non_staff_author",),
+ curators=("staff_contest_edit_own",),
+ testers=("non_staff_tester",),
+ spectators=("non_staff_spectator",),
)
self.tester_see_scoreboard_contest = create_contest(
- key='tester_see_board',
+ key="tester_see_board",
start_time=_now - timezone.timedelta(days=100),
end_time=_now + timezone.timedelta(days=100),
time_limit=timezone.timedelta(days=1),
is_visible=True,
scoreboard_visibility=Contest.SCOREBOARD_HIDDEN,
- testers=('non_staff_tester',),
+ testers=("non_staff_tester",),
tester_see_scoreboard=True,
)
self.public_limit_organization_join_contest = create_contest(
- key='limit_org_join',
+ key="limit_org_join",
start_time=_now - timezone.timedelta(days=2),
end_time=_now + timezone.timedelta(days=2),
time_limit=timezone.timedelta(days=1),
is_visible=True,
limit_join_organizations=True,
- join_organizations=('open',),
+ join_organizations=("open",),
scoreboard_visibility=Contest.SCOREBOARD_VISIBLE,
- authors=('non_staff_author',),
- curators=('staff_contest_edit_own',),
- testers=('non_staff_tester',),
+ authors=("non_staff_author",),
+ curators=("staff_contest_edit_own",),
+ testers=("non_staff_tester",),
)
- for contest_key in ('contest_scoreboard', 'particip_scoreboard', 'visible_scoreboard'):
+ for contest_key in (
+ "contest_scoreboard",
+ "particip_scoreboard",
+ "visible_scoreboard",
+ ):
create_contest_participation(
contest=contest_key,
- user='normal_during_window',
+ user="normal_during_window",
real_start=_now - timezone.timedelta(hours=1),
virtual=ContestParticipation.LIVE,
)
create_contest_participation(
contest=contest_key,
- user='normal_after_window',
+ user="normal_after_window",
real_start=_now - timezone.timedelta(days=3),
virtual=ContestParticipation.LIVE,
)
create_contest_participation(
- contest='particip_scoreboard',
- user='normal',
+ contest="particip_scoreboard",
+ user="normal",
real_start=_now - timezone.timedelta(days=3),
virtual=ContestParticipation.LIVE,
)
create_contest_participation(
- contest='particip_scoreboard',
- user='normal',
+ contest="particip_scoreboard",
+ user="normal",
real_start=_now + timezone.timedelta(days=101),
virtual=ContestParticipation.SPECTATE,
)
create_contest_participation(
- contest='full_hidden_board',
- user='normal_after_window',
+ contest="full_hidden_board",
+ user="normal_after_window",
real_start=_now - timezone.timedelta(days=5),
virtual=ContestParticipation.LIVE,
)
- self.users['normal'].profile.current_contest = create_contest_participation(
- contest='hidden_scoreboard',
- user='normal',
+ self.users["normal"].profile.current_contest = create_contest_participation(
+ contest="hidden_scoreboard",
+ user="normal",
)
- self.users['normal'].profile.save()
+ self.users["normal"].profile.save()
self.hidden_scoreboard_contest.update_user_count()
self.private_contest = create_contest(
- key='private',
+ key="private",
start_time=_now - timezone.timedelta(days=5),
end_time=_now - timezone.timedelta(days=3),
is_visible=True,
is_private=True,
is_organization_private=True,
- private_contestants=('staff_contest_edit_own',),
- testers=('non_staff_tester',),
+ private_contestants=("staff_contest_edit_own",),
+ testers=("non_staff_tester",),
)
self.organization_private_contest = create_contest(
- key='organization_private',
+ key="organization_private",
start_time=_now - timezone.timedelta(days=5),
end_time=_now + timezone.timedelta(days=6),
is_visible=True,
is_organization_private=True,
- organizations=('open',),
- view_contest_scoreboard=('normal',),
- testers=('non_staff_tester',),
+ organizations=("open",),
+ view_contest_scoreboard=("normal",),
+ testers=("non_staff_tester",),
)
self.future_organization_private_contest = create_contest(
- key='future_org_private',
+ key="future_org_private",
start_time=_now + timezone.timedelta(days=3),
end_time=_now + timezone.timedelta(days=6),
is_visible=True,
is_organization_private=True,
- organizations=('open',),
- view_contest_scoreboard=('normal',),
- testers=('non_staff_tester',),
+ organizations=("open",),
+ view_contest_scoreboard=("normal",),
+ testers=("non_staff_tester",),
)
self.private_user_contest = create_contest(
- key='private_user',
+ key="private_user",
start_time=_now - timezone.timedelta(days=3),
end_time=_now + timezone.timedelta(days=6),
is_visible=True,
is_private=True,
- testers=('non_staff_tester',),
+ testers=("non_staff_tester",),
)
self.non_visible_contest = create_contest(
- key='non_visible_contest',
+ key="non_visible_contest",
start_time=_now - timezone.timedelta(days=3),
end_time=_now + timezone.timedelta(days=6),
is_visible=False,
)
self.non_visible_contest_with_tester = create_contest(
- key='non_visible_w_tester',
+ key="non_visible_w_tester",
start_time=_now - timezone.timedelta(days=3),
end_time=_now + timezone.timedelta(days=6),
is_visible=False,
- testers=('non_staff_tester',),
+ testers=("non_staff_tester",),
)
def setUp(self):
- self.users['normal'].profile.refresh_from_db()
+ self.users["normal"].profile.refresh_from_db()
def test_basic_contest(self):
self.assertTrue(self.basic_contest.show_scoreboard)
- self.assertEqual(self.basic_contest.contest_window_length, timezone.timedelta(days=101))
+ self.assertEqual(
+ self.basic_contest.contest_window_length, timezone.timedelta(days=101)
+ )
self.assertIsInstance(self.basic_contest._now, timezone.datetime)
self.assertTrue(self.basic_contest.started)
self.assertIsNone(self.basic_contest.time_before_start)
self.assertIsInstance(self.basic_contest.time_before_end, timezone.timedelta)
self.assertFalse(self.basic_contest.ended)
self.assertEqual(str(self.basic_contest), self.basic_contest.name)
- self.assertEqual(self.basic_contest.get_label_for_problem(0), '1')
+ self.assertEqual(self.basic_contest.get_label_for_problem(0), "1")
def test_hidden_scoreboard_contest(self):
self.assertFalse(self.hidden_scoreboard_contest.show_scoreboard)
for i in range(3):
with self.subTest(contest_problem_index=i):
- self.assertEqual(self.hidden_scoreboard_contest.get_label_for_problem(i), str(i))
+ self.assertEqual(
+ self.hidden_scoreboard_contest.get_label_for_problem(i), str(i)
+ )
self.assertEqual(self.hidden_scoreboard_contest.user_count, 1)
def test_private_contest(self):
@@ -302,347 +319,362 @@ def test_organization_private_contest(self):
self.assertTrue(self.organization_private_contest.show_scoreboard)
self.assertFalse(self.organization_private_contest.ended)
self.assertIsNone(self.organization_private_contest.time_before_start)
- self.assertIsInstance(self.organization_private_contest.time_before_end, timezone.timedelta)
+ self.assertIsInstance(
+ self.organization_private_contest.time_before_end, timezone.timedelta
+ )
def test_future_organization_private_contest(self):
self.assertFalse(self.future_organization_private_contest.started)
self.assertFalse(self.future_organization_private_contest.show_scoreboard)
self.assertFalse(self.future_organization_private_contest.ended)
- self.assertIsInstance(self.future_organization_private_contest.time_before_start, timezone.timedelta)
- self.assertIsInstance(self.future_organization_private_contest.time_before_end, timezone.timedelta)
+ self.assertIsInstance(
+ self.future_organization_private_contest.time_before_start,
+ timezone.timedelta,
+ )
+ self.assertIsInstance(
+ self.future_organization_private_contest.time_before_end, timezone.timedelta
+ )
def test_basic_contest_methods(self):
with self.assertRaises(Contest.Inaccessible):
- self.basic_contest.access_check(self.users['normal'])
+ self.basic_contest.access_check(self.users["normal"])
data = {
- 'superuser': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_live_joinable_by': self.assertFalse, # author
- 'is_spectatable_by': self.assertTrue,
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertTrue,
- 'is_in_contest': self.assertFalse,
- },
- 'staff_contest_edit_own': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_live_joinable_by': self.assertFalse, # author
- 'is_spectatable_by': self.assertTrue,
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertTrue,
- 'is_in_contest': self.assertFalse,
- },
- 'staff_contest_see_all': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_live_joinable_by': self.assertTrue,
- 'is_spectatable_by': self.assertTrue,
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertFalse,
- 'is_in_contest': self.assertFalse,
- },
- 'staff_contest_edit_all': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_live_joinable_by': self.assertTrue,
- 'is_spectatable_by': self.assertTrue,
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertTrue,
- 'is_in_contest': self.assertFalse,
- },
- 'normal': {
+ "superuser": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_live_joinable_by": self.assertFalse, # author
+ "is_spectatable_by": self.assertTrue,
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertTrue,
+ "is_in_contest": self.assertFalse,
+ },
+ "staff_contest_edit_own": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_live_joinable_by": self.assertFalse, # author
+ "is_spectatable_by": self.assertTrue,
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertTrue,
+ "is_in_contest": self.assertFalse,
+ },
+ "staff_contest_see_all": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_live_joinable_by": self.assertTrue,
+ "is_spectatable_by": self.assertTrue,
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertFalse,
+ "is_in_contest": self.assertFalse,
+ },
+ "staff_contest_edit_all": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_live_joinable_by": self.assertTrue,
+ "is_spectatable_by": self.assertTrue,
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertTrue,
+ "is_in_contest": self.assertFalse,
+ },
+ "normal": {
# scoreboard checks don't do accessibility checks
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_live_joinable_by': self.assertTrue,
- 'is_spectatable_by': self.assertTrue,
- 'is_accessible_by': self.assertFalse,
- 'is_editable_by': self.assertFalse,
- 'is_in_contest': self.assertFalse,
- },
- 'non_staff_tester': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_live_joinable_by': self.assertFalse,
- 'is_spectatable_by': self.assertTrue,
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertFalse,
- 'is_in_contest': self.assertFalse,
- },
- 'anonymous': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_live_joinable_by': self.assertFalse,
- 'is_spectatable_by': self.assertFalse,
- 'is_accessible_by': self.assertFalse,
- 'is_editable_by': self.assertFalse,
- 'is_in_contest': self.assertFalse,
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_live_joinable_by": self.assertTrue,
+ "is_spectatable_by": self.assertTrue,
+ "is_accessible_by": self.assertFalse,
+ "is_editable_by": self.assertFalse,
+ "is_in_contest": self.assertFalse,
+ },
+ "non_staff_tester": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_live_joinable_by": self.assertFalse,
+ "is_spectatable_by": self.assertTrue,
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertFalse,
+ "is_in_contest": self.assertFalse,
+ },
+ "anonymous": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_live_joinable_by": self.assertFalse,
+ "is_spectatable_by": self.assertFalse,
+ "is_accessible_by": self.assertFalse,
+ "is_editable_by": self.assertFalse,
+ "is_in_contest": self.assertFalse,
},
}
self._test_object_methods_with_users(self.basic_contest, data)
def test_hidden_scoreboard_contest_methods(self):
data = {
- 'staff_contest_edit_own': {
- 'can_see_own_scoreboard': self.assertFalse,
- 'can_see_full_scoreboard': self.assertFalse,
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertFalse,
- 'is_in_contest': self.assertFalse,
- },
- 'staff_contest_see_all': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertFalse,
- 'is_in_contest': self.assertFalse,
- },
- 'staff_contest_edit_all': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertTrue,
- 'is_in_contest': self.assertFalse,
- },
- 'non_staff_spectator': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertFalse,
- 'is_in_contest': self.assertFalse,
- },
- 'normal': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertFalse,
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertFalse,
- 'is_in_contest': self.assertTrue,
- },
- 'anonymous': {
- 'can_see_own_scoreboard': self.assertFalse,
- 'can_see_full_scoreboard': self.assertFalse,
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertFalse,
- 'is_in_contest': self.assertFalse,
+ "staff_contest_edit_own": {
+ "can_see_own_scoreboard": self.assertFalse,
+ "can_see_full_scoreboard": self.assertFalse,
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertFalse,
+ "is_in_contest": self.assertFalse,
+ },
+ "staff_contest_see_all": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertFalse,
+ "is_in_contest": self.assertFalse,
+ },
+ "staff_contest_edit_all": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertTrue,
+ "is_in_contest": self.assertFalse,
+ },
+ "non_staff_spectator": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertFalse,
+ "is_in_contest": self.assertFalse,
+ },
+ "normal": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertFalse,
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertFalse,
+ "is_in_contest": self.assertTrue,
+ },
+ "anonymous": {
+ "can_see_own_scoreboard": self.assertFalse,
+ "can_see_full_scoreboard": self.assertFalse,
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertFalse,
+ "is_in_contest": self.assertFalse,
},
}
self._test_object_methods_with_users(self.hidden_scoreboard_contest, data)
def test_contest_hidden_scoreboard_non_staff_author_contest_methods(self):
data = {
- 'staff_contest_edit_own': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertTrue,
- 'is_in_contest': self.assertFalse,
- },
- 'non_staff_author': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertFalse,
- 'is_in_contest': self.assertFalse,
+ "staff_contest_edit_own": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertTrue,
+ "is_in_contest": self.assertFalse,
+ },
+ "non_staff_author": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertFalse,
+ "is_in_contest": self.assertFalse,
},
}
- self._test_object_methods_with_users(self.hidden_scoreboard_non_staff_author, data)
+ self._test_object_methods_with_users(
+ self.hidden_scoreboard_non_staff_author, data
+ )
def test_contest_hidden_scoreboard_contest_methods(self):
data = {
- 'normal_before_window': {
- 'can_see_own_scoreboard': self.assertFalse,
- 'can_see_full_scoreboard': self.assertFalse,
- 'is_live_joinable_by': self.assertTrue,
- 'is_spectatable_by': self.assertTrue,
- 'has_completed_contest': self.assertFalse,
- },
- 'normal_during_window': {
- 'can_see_own_scoreboard': self.assertFalse,
- 'can_see_full_scoreboard': self.assertFalse,
- 'has_completed_contest': self.assertFalse,
- },
- 'normal_after_window': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertFalse,
- 'is_live_joinable_by': self.assertFalse,
- 'is_spectatable_by': self.assertTrue,
- 'has_completed_contest': self.assertTrue,
- },
- 'non_staff_tester': {
- 'can_see_own_scoreboard': self.assertFalse,
- 'can_see_full_scoreboard': self.assertFalse,
- 'is_live_joinable_by': self.assertFalse,
- 'is_spectatable_by': self.assertTrue,
- 'has_completed_contest': self.assertFalse,
+ "normal_before_window": {
+ "can_see_own_scoreboard": self.assertFalse,
+ "can_see_full_scoreboard": self.assertFalse,
+ "is_live_joinable_by": self.assertTrue,
+ "is_spectatable_by": self.assertTrue,
+ "has_completed_contest": self.assertFalse,
+ },
+ "normal_during_window": {
+ "can_see_own_scoreboard": self.assertFalse,
+ "can_see_full_scoreboard": self.assertFalse,
+ "has_completed_contest": self.assertFalse,
+ },
+ "normal_after_window": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertFalse,
+ "is_live_joinable_by": self.assertFalse,
+ "is_spectatable_by": self.assertTrue,
+ "has_completed_contest": self.assertTrue,
+ },
+ "non_staff_tester": {
+ "can_see_own_scoreboard": self.assertFalse,
+ "can_see_full_scoreboard": self.assertFalse,
+ "is_live_joinable_by": self.assertFalse,
+ "is_spectatable_by": self.assertTrue,
+ "has_completed_contest": self.assertFalse,
},
}
- self._test_object_methods_with_users(self.contest_hidden_scoreboard_contest, data)
+ self._test_object_methods_with_users(
+ self.contest_hidden_scoreboard_contest, data
+ )
def test_particip_hidden_scoreboard_contest_methods(self):
data = {
- 'normal_before_window': {
- 'can_see_own_scoreboard': self.assertFalse,
- 'can_see_full_scoreboard': self.assertFalse,
- 'has_completed_contest': self.assertFalse,
- },
- 'normal_during_window': {
- 'can_see_own_scoreboard': self.assertFalse,
- 'can_see_full_scoreboard': self.assertFalse,
- 'has_completed_contest': self.assertFalse,
- },
- 'normal_after_window': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'has_completed_contest': self.assertTrue,
- },
- 'normal': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'has_completed_contest': self.assertTrue,
- },
- 'non_staff_tester': {
- 'can_see_own_scoreboard': self.assertFalse,
- 'can_see_full_scoreboard': self.assertFalse,
- 'has_completed_contest': self.assertFalse,
+ "normal_before_window": {
+ "can_see_own_scoreboard": self.assertFalse,
+ "can_see_full_scoreboard": self.assertFalse,
+ "has_completed_contest": self.assertFalse,
+ },
+ "normal_during_window": {
+ "can_see_own_scoreboard": self.assertFalse,
+ "can_see_full_scoreboard": self.assertFalse,
+ "has_completed_contest": self.assertFalse,
+ },
+ "normal_after_window": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "has_completed_contest": self.assertTrue,
+ },
+ "normal": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "has_completed_contest": self.assertTrue,
+ },
+ "non_staff_tester": {
+ "can_see_own_scoreboard": self.assertFalse,
+ "can_see_full_scoreboard": self.assertFalse,
+ "has_completed_contest": self.assertFalse,
},
}
- self._test_object_methods_with_users(self.particip_hidden_scoreboard_contest, data)
+ self._test_object_methods_with_users(
+ self.particip_hidden_scoreboard_contest, data
+ )
def test_visible_scoreboard_contest_methods(self):
data = {
- 'normal_before_window': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'has_completed_contest': self.assertFalse,
- },
- 'normal_during_window': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'has_completed_contest': self.assertFalse,
- },
- 'normal_after_window': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'has_completed_contest': self.assertTrue,
- },
- 'non_staff_tester': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'has_completed_contest': self.assertFalse,
+ "normal_before_window": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "has_completed_contest": self.assertFalse,
+ },
+ "normal_during_window": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "has_completed_contest": self.assertFalse,
+ },
+ "normal_after_window": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "has_completed_contest": self.assertTrue,
+ },
+ "non_staff_tester": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "has_completed_contest": self.assertFalse,
},
}
self._test_object_methods_with_users(self.visible_scoreboard_contest, data)
def test_full_hidden_scoreboard_contest_methods(self):
data = {
- 'superuser': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
+ "superuser": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
},
- 'non_staff_tester': {
- 'can_see_own_scoreboard': self.assertFalse,
- 'can_see_full_scoreboard': self.assertFalse,
+ "non_staff_tester": {
+ "can_see_own_scoreboard": self.assertFalse,
+ "can_see_full_scoreboard": self.assertFalse,
},
- 'non_staff_author': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
+ "non_staff_author": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
},
- 'staff_contest_edit_own': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
+ "staff_contest_edit_own": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
},
- 'staff_contest_edit_all': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
+ "staff_contest_edit_all": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
},
- 'normal': {
- 'can_see_own_scoreboard': self.assertFalse,
- 'can_see_full_scoreboard': self.assertFalse,
+ "normal": {
+ "can_see_own_scoreboard": self.assertFalse,
+ "can_see_full_scoreboard": self.assertFalse,
},
- 'normal_after_window': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertFalse,
+ "normal_after_window": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertFalse,
},
}
self._test_object_methods_with_users(self.full_hidden_scoreboard_contest, data)
def test_tester_see_scoreboard_contest_methods(self):
data = {
- 'superuser': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
+ "superuser": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
},
- 'non_staff_tester': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
+ "non_staff_tester": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
},
- 'normal': {
- 'can_see_own_scoreboard': self.assertFalse,
- 'can_see_full_scoreboard': self.assertFalse,
+ "normal": {
+ "can_see_own_scoreboard": self.assertFalse,
+ "can_see_full_scoreboard": self.assertFalse,
},
}
self._test_object_methods_with_users(self.tester_see_scoreboard_contest, data)
def test_public_limit_organization_join_contest(self):
data = {
- 'non_staff_tester': {
- 'is_live_joinable_by': self.assertFalse,
- 'is_spectatable_by': self.assertTrue,
+ "non_staff_tester": {
+ "is_live_joinable_by": self.assertFalse,
+ "is_spectatable_by": self.assertTrue,
},
- 'non_staff_author': {
- 'is_live_joinable_by': self.assertFalse,
- 'is_spectatable_by': self.assertTrue,
+ "non_staff_author": {
+ "is_live_joinable_by": self.assertFalse,
+ "is_spectatable_by": self.assertTrue,
},
- 'staff_contest_edit_own': {
- 'is_live_joinable_by': self.assertFalse,
- 'is_spectatable_by': self.assertTrue, # curator
+ "staff_contest_edit_own": {
+ "is_live_joinable_by": self.assertFalse,
+ "is_spectatable_by": self.assertTrue, # curator
},
- 'staff_contest_edit_all': {
- 'is_live_joinable_by': self.assertFalse,
- 'is_spectatable_by': self.assertFalse,
+ "staff_contest_edit_all": {
+ "is_live_joinable_by": self.assertFalse,
+ "is_spectatable_by": self.assertFalse,
},
- 'normal': {
- 'is_live_joinable_by': self.assertFalse,
- 'is_spectatable_by': self.assertFalse,
+ "normal": {
+ "is_live_joinable_by": self.assertFalse,
+ "is_spectatable_by": self.assertFalse,
},
- 'normal_open_org': {
- 'is_live_joinable_by': self.assertTrue,
- 'is_spectatable_by': self.assertTrue,
+ "normal_open_org": {
+ "is_live_joinable_by": self.assertTrue,
+ "is_spectatable_by": self.assertTrue,
},
- 'normal_after_window': {
- 'is_live_joinable_by': self.assertFalse,
- 'is_spectatable_by': self.assertFalse, # not in org
+ "normal_after_window": {
+ "is_live_joinable_by": self.assertFalse,
+ "is_spectatable_by": self.assertFalse, # not in org
},
}
- self._test_object_methods_with_users(self.public_limit_organization_join_contest, data)
+ self._test_object_methods_with_users(
+ self.public_limit_organization_join_contest, data
+ )
def test_future_contest_methods(self):
data = {
- 'non_staff_spectator': {
- 'can_see_own_scoreboard': self.assertFalse,
- 'can_see_full_scoreboard': self.assertFalse,
+ "non_staff_spectator": {
+ "can_see_own_scoreboard": self.assertFalse,
+ "can_see_full_scoreboard": self.assertFalse,
},
- 'non_staff_tester': {
- 'can_see_own_scoreboard': self.assertFalse,
- 'can_see_full_scoreboard': self.assertFalse,
+ "non_staff_tester": {
+ "can_see_own_scoreboard": self.assertFalse,
+ "can_see_full_scoreboard": self.assertFalse,
},
- 'non_staff_author': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
+ "non_staff_author": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
},
- 'staff_contest_edit_own': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
+ "staff_contest_edit_own": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
},
- 'staff_contest_edit_all': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
+ "staff_contest_edit_all": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
},
- 'normal': {
- 'can_see_own_scoreboard': self.assertFalse,
- 'can_see_full_scoreboard': self.assertFalse,
+ "normal": {
+ "can_see_own_scoreboard": self.assertFalse,
+ "can_see_full_scoreboard": self.assertFalse,
},
}
self._test_object_methods_with_users(self.future_contest, data)
@@ -650,253 +682,257 @@ def test_future_contest_methods(self):
def test_private_contest_methods(self):
# User must be in org and in private user list
with self.assertRaises(Contest.PrivateContest):
- self.private_contest.access_check(self.users['normal_open_org'])
- self.private_contest.private_contestants.add(self.users['normal_open_org'].profile)
+ self.private_contest.access_check(self.users["normal_open_org"])
+ self.private_contest.private_contestants.add(
+ self.users["normal_open_org"].profile
+ )
with self.assertRaises(Contest.PrivateContest):
- self.private_contest.access_check(self.users['normal_open_org'])
- self.private_contest.organizations.add(self.organizations['open'])
+ self.private_contest.access_check(self.users["normal_open_org"])
+ self.private_contest.organizations.add(self.organizations["open"])
data = {
- 'normal_open_org': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_live_joinable_by': self.assertTrue,
- 'is_spectatable_by': self.assertTrue,
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertFalse,
- 'is_in_contest': self.assertFalse,
- },
- 'staff_contest_see_all': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertFalse,
- 'is_in_contest': self.assertFalse,
- },
- 'anonymous': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_accessible_by': self.assertFalse,
- 'is_editable_by': self.assertFalse,
- 'is_in_contest': self.assertFalse,
- },
- 'non_staff_tester': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertFalse,
- 'is_in_contest': self.assertFalse,
+ "normal_open_org": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_live_joinable_by": self.assertTrue,
+ "is_spectatable_by": self.assertTrue,
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertFalse,
+ "is_in_contest": self.assertFalse,
+ },
+ "staff_contest_see_all": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertFalse,
+ "is_in_contest": self.assertFalse,
+ },
+ "anonymous": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_accessible_by": self.assertFalse,
+ "is_editable_by": self.assertFalse,
+ "is_in_contest": self.assertFalse,
+ },
+ "non_staff_tester": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertFalse,
+ "is_in_contest": self.assertFalse,
},
}
self._test_object_methods_with_users(self.private_contest, data)
def test_organization_private_contest_methods(self):
data = {
- 'staff_contest_edit_own': {
+ "staff_contest_edit_own": {
# scoreboard checks don't do accessibility checks
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_accessible_by': self.assertFalse,
- 'is_editable_by': self.assertFalse,
- 'is_in_contest': self.assertFalse,
- },
- 'staff_contest_see_all': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertFalse,
- 'is_in_contest': self.assertFalse,
- },
- 'staff_contest_edit_all': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertTrue,
- 'is_in_contest': self.assertFalse,
- },
- 'normal': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_live_joinable_by': self.assertTrue,
- 'is_spectatable_by': self.assertTrue,
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertFalse,
- 'is_in_contest': self.assertFalse,
- },
- 'non_staff_tester': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertFalse,
- 'is_in_contest': self.assertFalse,
- },
- 'anonymous': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_accessible_by': self.assertFalse,
- 'is_editable_by': self.assertFalse,
- 'is_in_contest': self.assertFalse,
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_accessible_by": self.assertFalse,
+ "is_editable_by": self.assertFalse,
+ "is_in_contest": self.assertFalse,
+ },
+ "staff_contest_see_all": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertFalse,
+ "is_in_contest": self.assertFalse,
+ },
+ "staff_contest_edit_all": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertTrue,
+ "is_in_contest": self.assertFalse,
+ },
+ "normal": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_live_joinable_by": self.assertTrue,
+ "is_spectatable_by": self.assertTrue,
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertFalse,
+ "is_in_contest": self.assertFalse,
+ },
+ "non_staff_tester": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertFalse,
+ "is_in_contest": self.assertFalse,
+ },
+ "anonymous": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_accessible_by": self.assertFalse,
+ "is_editable_by": self.assertFalse,
+ "is_in_contest": self.assertFalse,
},
}
self._test_object_methods_with_users(self.organization_private_contest, data)
def test_future_organization_private_contest_methods(self):
data = {
- 'staff_contest_edit_own': {
- 'can_see_own_scoreboard': self.assertFalse,
- 'can_see_full_scoreboard': self.assertFalse,
- 'is_live_joinable_by': self.assertFalse,
- 'is_spectatable_by': self.assertTrue,
- 'is_accessible_by': self.assertFalse,
- 'is_editable_by': self.assertFalse,
- 'is_in_contest': self.assertFalse,
- },
- 'staff_contest_see_all': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_live_joinable_by': self.assertFalse,
- 'is_spectatable_by': self.assertTrue,
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertFalse,
- 'is_in_contest': self.assertFalse,
- },
- 'staff_contest_edit_all': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_live_joinable_by': self.assertFalse,
- 'is_spectatable_by': self.assertTrue,
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertTrue,
- 'is_in_contest': self.assertFalse,
- },
- 'normal': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_live_joinable_by': self.assertFalse,
- 'is_spectatable_by': self.assertTrue,
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertFalse,
- 'is_in_contest': self.assertFalse,
- },
- 'non_staff_tester': {
+ "staff_contest_edit_own": {
+ "can_see_own_scoreboard": self.assertFalse,
+ "can_see_full_scoreboard": self.assertFalse,
+ "is_live_joinable_by": self.assertFalse,
+ "is_spectatable_by": self.assertTrue,
+ "is_accessible_by": self.assertFalse,
+ "is_editable_by": self.assertFalse,
+ "is_in_contest": self.assertFalse,
+ },
+ "staff_contest_see_all": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_live_joinable_by": self.assertFalse,
+ "is_spectatable_by": self.assertTrue,
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertFalse,
+ "is_in_contest": self.assertFalse,
+ },
+ "staff_contest_edit_all": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_live_joinable_by": self.assertFalse,
+ "is_spectatable_by": self.assertTrue,
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertTrue,
+ "is_in_contest": self.assertFalse,
+ },
+ "normal": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_live_joinable_by": self.assertFalse,
+ "is_spectatable_by": self.assertTrue,
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertFalse,
+ "is_in_contest": self.assertFalse,
+ },
+ "non_staff_tester": {
# False because contest has not begun
- 'can_see_own_scoreboard': self.assertFalse,
- 'can_see_full_scoreboard': self.assertFalse,
- 'is_live_joinable_by': self.assertFalse,
- 'is_spectatable_by': self.assertTrue,
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertFalse,
- 'is_in_contest': self.assertFalse,
- },
- 'anonymous': {
+ "can_see_own_scoreboard": self.assertFalse,
+ "can_see_full_scoreboard": self.assertFalse,
+ "is_live_joinable_by": self.assertFalse,
+ "is_spectatable_by": self.assertTrue,
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertFalse,
+ "is_in_contest": self.assertFalse,
+ },
+ "anonymous": {
# False because contest has not begun
- 'can_see_own_scoreboard': self.assertFalse,
- 'can_see_full_scoreboard': self.assertFalse,
- 'is_live_joinable_by': self.assertFalse,
- 'is_spectatable_by': self.assertFalse,
- 'is_accessible_by': self.assertFalse,
- 'is_editable_by': self.assertFalse,
- 'is_in_contest': self.assertFalse,
+ "can_see_own_scoreboard": self.assertFalse,
+ "can_see_full_scoreboard": self.assertFalse,
+ "is_live_joinable_by": self.assertFalse,
+ "is_spectatable_by": self.assertFalse,
+ "is_accessible_by": self.assertFalse,
+ "is_editable_by": self.assertFalse,
+ "is_in_contest": self.assertFalse,
},
}
- self._test_object_methods_with_users(self.future_organization_private_contest, data)
+ self._test_object_methods_with_users(
+ self.future_organization_private_contest, data
+ )
def test_private_user_contest_methods(self):
data = {
- 'superuser': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertTrue,
- 'is_in_contest': self.assertFalse,
- },
- 'normal': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_accessible_by': self.assertFalse,
- 'is_editable_by': self.assertFalse,
- 'is_in_contest': self.assertFalse,
- },
- 'non_staff_tester': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertFalse,
- 'is_in_contest': self.assertFalse,
- },
- 'anonymous': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_accessible_by': self.assertFalse,
- 'is_editable_by': self.assertFalse,
- 'is_in_contest': self.assertFalse,
+ "superuser": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertTrue,
+ "is_in_contest": self.assertFalse,
+ },
+ "normal": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_accessible_by": self.assertFalse,
+ "is_editable_by": self.assertFalse,
+ "is_in_contest": self.assertFalse,
+ },
+ "non_staff_tester": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertFalse,
+ "is_in_contest": self.assertFalse,
+ },
+ "anonymous": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_accessible_by": self.assertFalse,
+ "is_editable_by": self.assertFalse,
+ "is_in_contest": self.assertFalse,
},
}
self._test_object_methods_with_users(self.private_user_contest, data)
def test_non_visible_contest_contest_methods(self):
data = {
- 'superuser': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertTrue,
- 'is_in_contest': self.assertFalse,
- },
- 'normal': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_accessible_by': self.assertFalse,
- 'is_editable_by': self.assertFalse,
- 'is_in_contest': self.assertFalse,
+ "superuser": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertTrue,
+ "is_in_contest": self.assertFalse,
+ },
+ "normal": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_accessible_by": self.assertFalse,
+ "is_editable_by": self.assertFalse,
+ "is_in_contest": self.assertFalse,
},
# not set as tester, in case something silly is happening
- 'non_staff_tester': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_accessible_by': self.assertFalse,
- 'is_editable_by': self.assertFalse,
- 'is_in_contest': self.assertFalse,
- },
- 'anonymous': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_accessible_by': self.assertFalse,
- 'is_editable_by': self.assertFalse,
- 'is_in_contest': self.assertFalse,
+ "non_staff_tester": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_accessible_by": self.assertFalse,
+ "is_editable_by": self.assertFalse,
+ "is_in_contest": self.assertFalse,
+ },
+ "anonymous": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_accessible_by": self.assertFalse,
+ "is_editable_by": self.assertFalse,
+ "is_in_contest": self.assertFalse,
},
}
self._test_object_methods_with_users(self.non_visible_contest, data)
def test_non_visible_contest_with_tester_contest_methods(self):
data = {
- 'superuser': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertTrue,
- 'is_in_contest': self.assertFalse,
- },
- 'normal': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_accessible_by': self.assertFalse,
- 'is_editable_by': self.assertFalse,
- 'is_in_contest': self.assertFalse,
- },
- 'non_staff_tester': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertFalse,
- 'is_in_contest': self.assertFalse,
- },
- 'anonymous': {
- 'can_see_own_scoreboard': self.assertTrue,
- 'can_see_full_scoreboard': self.assertTrue,
- 'is_accessible_by': self.assertFalse,
- 'is_editable_by': self.assertFalse,
- 'is_in_contest': self.assertFalse,
+ "superuser": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertTrue,
+ "is_in_contest": self.assertFalse,
+ },
+ "normal": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_accessible_by": self.assertFalse,
+ "is_editable_by": self.assertFalse,
+ "is_in_contest": self.assertFalse,
+ },
+ "non_staff_tester": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertFalse,
+ "is_in_contest": self.assertFalse,
+ },
+ "anonymous": {
+ "can_see_own_scoreboard": self.assertTrue,
+ "can_see_full_scoreboard": self.assertTrue,
+ "is_accessible_by": self.assertFalse,
+ "is_editable_by": self.assertFalse,
+ "is_in_contest": self.assertFalse,
},
}
self._test_object_methods_with_users(self.non_visible_contest_with_tester, data)
@@ -906,34 +942,36 @@ def test_contests_list(self):
with self.subTest(user=name):
# We only care about consistency between Contest.is_accessible_by and Contest.get_visible_contests
contest_keys = []
- for contest in Contest.objects.prefetch_related('testers', 'private_contestants', 'organizations'):
+ for contest in Contest.objects.prefetch_related(
+ "testers", "private_contestants", "organizations"
+ ):
if contest.is_accessible_by(user):
contest_keys.append(contest.key)
self.assertCountEqual(
- Contest.get_visible_contests(user).values_list('key', flat=True),
+ Contest.get_visible_contests(user).values_list("key", flat=True),
contest_keys,
)
def test_contest_clean(self):
_now = timezone.now()
contest = create_contest(
- key='contest',
+ key="contest",
start_time=_now,
end_time=_now - timezone.timedelta(days=1),
- problem_label_script='invalid',
- format_config={'invalid': 'invalid'},
+ problem_label_script="invalid",
+ format_config={"invalid": "invalid"},
)
- with self.assertRaisesRegex(ValidationError, 'ended before it starts'):
+ with self.assertRaisesRegex(ValidationError, "ended before it starts"):
contest.full_clean()
contest.end_time = _now
- with self.assertRaisesRegex(ValidationError, 'ended before it starts'):
+ with self.assertRaisesRegex(ValidationError, "ended before it starts"):
contest.full_clean()
contest.end_time = _now + timezone.timedelta(days=1)
- with self.assertRaisesRegex(ValidationError, 'default contest expects'):
+ with self.assertRaisesRegex(ValidationError, "default contest expects"):
contest.full_clean()
contest.format_config = {}
- with self.assertRaisesRegex(ValidationError, 'Contest problem label script'):
+ with self.assertRaisesRegex(ValidationError, "Contest problem label script"):
contest.full_clean()
contest.problem_label_script = """
function(n)
@@ -941,35 +979,35 @@ def test_contest_clean(self):
end
"""
# Test for bad problem label script caching
- with self.assertRaisesRegex(ValidationError, 'Contest problem label script'):
+ with self.assertRaisesRegex(ValidationError, "Contest problem label script"):
contest.full_clean()
del contest.get_label_for_problem
- with self.assertRaisesRegex(ValidationError, 'should return a string'):
+ with self.assertRaisesRegex(ValidationError, "should return a string"):
contest.full_clean()
- contest.problem_label_script = ''
+ contest.problem_label_script = ""
del contest.get_label_for_problem
contest.full_clean()
def test_normal_user_current_contest(self):
- current_contest = self.users['normal'].profile.current_contest
+ current_contest = self.users["normal"].profile.current_contest
self.assertIsNotNone(current_contest)
current_contest.set_disqualified(True)
- self.users['normal'].profile.refresh_from_db()
+ self.users["normal"].profile.refresh_from_db()
self.assertTrue(current_contest.is_disqualified)
- self.assertIsNone(self.users['normal'].profile.current_contest)
+ self.assertIsNone(self.users["normal"].profile.current_contest)
self.assertEqual(current_contest.score, -9999)
current_contest.set_disqualified(False)
- self.users['normal'].profile.refresh_from_db()
+ self.users["normal"].profile.refresh_from_db()
self.assertFalse(current_contest.is_disqualified)
- self.assertIsNone(self.users['normal'].profile.current_contest)
+ self.assertIsNone(self.users["normal"].profile.current_contest)
self.assertEqual(current_contest.score, 0)
def test_live_participation(self):
participation = ContestParticipation.objects.get(
contest=self.hidden_scoreboard_contest,
- user=self.users['normal'].profile,
+ user=self.users["normal"].profile,
virtual=ContestParticipation.LIVE,
)
self.assertTrue(participation.live)
@@ -980,8 +1018,8 @@ def test_live_participation(self):
def test_spectating_participation(self):
participation = create_contest_participation(
- contest='hidden_scoreboard',
- user='superuser',
+ contest="hidden_scoreboard",
+ user="superuser",
virtual=ContestParticipation.SPECTATE,
)
@@ -992,8 +1030,8 @@ def test_spectating_participation(self):
def test_virtual_participation(self):
participation = create_contest_participation(
- contest='private',
- user='superuser',
+ contest="private",
+ user="superuser",
virtual=1,
)
@@ -1007,20 +1045,20 @@ class ContestTagTestCase(TestCase):
@classmethod
def setUpTestData(self):
self.basic_tag = ContestTag.objects.create(
- name='basic',
- color='#fff',
+ name="basic",
+ color="#fff",
)
self.dark_tag = ContestTag.objects.create(
- name='dark',
- color='#010001',
+ name="dark",
+ color="#010001",
)
def test_basic_tag(self):
self.assertEqual(str(self.basic_tag), self.basic_tag.name)
- self.assertEqual(self.basic_tag.text_color, '#000')
+ self.assertEqual(self.basic_tag.text_color, "#000")
def test_dark_tag(self):
- self.assertEqual(self.dark_tag.text_color, '#fff')
+ self.assertEqual(self.dark_tag.text_color, "#fff")
class MinValueOrNoneValidatorTestCase(SimpleTestCase):
diff --git a/judge/models/tests/test_problem.py b/judge/models/tests/test_problem.py
index 05b636955d..f4122c7827 100644
--- a/judge/models/tests/test_problem.py
+++ b/judge/models/tests/test_problem.py
@@ -4,8 +4,16 @@
from judge.models import Language, LanguageLimit, Problem, Submission
from judge.models.problem import VotePermission, disallowed_characters_validator
-from judge.models.tests.util import CommonDataMixin, create_contest, create_contest_participation, \
- create_organization, create_problem, create_problem_type, create_solution, create_user
+from judge.models.tests.util import (
+ CommonDataMixin,
+ create_contest,
+ create_contest_participation,
+ create_organization,
+ create_problem,
+ create_problem_type,
+ create_solution,
+ create_user,
+)
class ProblemTestCase(CommonDataMixin, TestCase):
@@ -13,26 +21,30 @@ class ProblemTestCase(CommonDataMixin, TestCase):
def setUpTestData(self):
super().setUpTestData()
- self.users.update({
- 'staff_problem_edit_only_all': create_user(
- username='staff_problem_edit_only_all',
- is_staff=True,
- user_permissions=('edit_all_problem',),
- ),
- })
+ self.users.update(
+ {
+ "staff_problem_edit_only_all": create_user(
+ username="staff_problem_edit_only_all",
+ is_staff=True,
+ user_permissions=("edit_all_problem",),
+ ),
+ }
+ )
- create_problem_type(name='type')
+ create_problem_type(name="type")
self.basic_problem = create_problem(
- code='basic',
- allowed_languages=Language.objects.values_list('key', flat=True),
- types=('type',),
- authors=('normal',),
- testers=('staff_problem_edit_public',),
+ code="basic",
+ allowed_languages=Language.objects.values_list("key", flat=True),
+ types=("type",),
+ authors=("normal",),
+ testers=("staff_problem_edit_public",),
)
limits = []
- for lang in Language.objects.filter(common_name=Language.get_python3().common_name):
+ for lang in Language.objects.filter(
+ common_name=Language.get_python3().common_name
+ ):
limits.append(
LanguageLimit(
problem=self.basic_problem,
@@ -44,32 +56,32 @@ def setUpTestData(self):
LanguageLimit.objects.bulk_create(limits)
self.organization_private_problem = create_problem(
- code='organization_private',
+ code="organization_private",
time_limit=2,
is_public=True,
is_organization_private=True,
- curators=('staff_problem_edit_own', 'staff_problem_edit_own_no_staff'),
+ curators=("staff_problem_edit_own", "staff_problem_edit_own_no_staff"),
)
self.problem_organization = create_organization(
- name='problem organization',
- admins=('normal', 'staff_problem_edit_public'),
+ name="problem organization",
+ admins=("normal", "staff_problem_edit_public"),
)
self.organization_admin_private_problem = create_problem(
- code='org_admin_private',
+ code="org_admin_private",
is_organization_private=True,
- organizations=('problem organization',),
+ organizations=("problem organization",),
)
self.organization_admin_problem = create_problem(
- code='organization_admin',
- organizations=('problem organization',),
+ code="organization_admin",
+ organizations=("problem organization",),
)
def test_basic_problem(self):
self.assertEqual(str(self.basic_problem), self.basic_problem.name)
self.assertCountEqual(
self.basic_problem.languages_list(),
- set(Language.objects.values_list('common_name', flat=True)),
+ set(Language.objects.values_list("common_name", flat=True)),
)
self.basic_problem.user_count = -1000
self.basic_problem.ac_rate = -1000
@@ -77,14 +89,23 @@ def test_basic_problem(self):
self.assertEqual(self.basic_problem.user_count, 0)
self.assertEqual(self.basic_problem.ac_rate, 0)
- self.assertListEqual(list(self.basic_problem.author_ids), [self.users['normal'].profile.id])
- self.assertListEqual(list(self.basic_problem.editor_ids), [self.users['normal'].profile.id])
- self.assertListEqual(list(self.basic_problem.tester_ids), [self.users['staff_problem_edit_public'].profile.id])
+ self.assertListEqual(
+ list(self.basic_problem.author_ids), [self.users["normal"].profile.id]
+ )
+ self.assertListEqual(
+ list(self.basic_problem.editor_ids), [self.users["normal"].profile.id]
+ )
+ self.assertListEqual(
+ list(self.basic_problem.tester_ids),
+ [self.users["staff_problem_edit_public"].profile.id],
+ )
self.assertListEqual(list(self.basic_problem.usable_languages), [])
- self.assertListEqual(self.basic_problem.types_list, ['type'])
+ self.assertListEqual(self.basic_problem.types_list, ["type"])
self.assertSetEqual(self.basic_problem.usable_common_names, set())
- self.assertEqual(self.basic_problem.translated_name('ABCDEFGHIJK'), self.basic_problem.name)
+ self.assertEqual(
+ self.basic_problem.translated_name("ABCDEFGHIJK"), self.basic_problem.name
+ )
self.assertFalse(self.basic_problem.clarifications.exists())
@@ -95,152 +116,158 @@ def test_basic_problem_language_limits(self):
self.assertEqual(time_limit, 100)
def test_basic_problem_methods(self):
- self.assertTrue(self.basic_problem.is_editor(self.users['normal'].profile))
+ self.assertTrue(self.basic_problem.is_editor(self.users["normal"].profile))
data = {
- 'superuser': {
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertTrue,
+ "superuser": {
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertTrue,
},
- 'staff_problem_edit_own': {
- 'is_accessible_by': self.assertFalse,
- 'is_editable_by': self.assertFalse,
+ "staff_problem_edit_own": {
+ "is_accessible_by": self.assertFalse,
+ "is_editable_by": self.assertFalse,
},
- 'staff_problem_see_all': {
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertFalse,
+ "staff_problem_see_all": {
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertFalse,
},
- 'staff_problem_edit_all': {
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertTrue,
+ "staff_problem_edit_all": {
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertTrue,
},
- 'staff_problem_edit_public': {
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertFalse,
+ "staff_problem_edit_public": {
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertFalse,
},
- 'staff_problem_see_organization': {
- 'is_accessible_by': self.assertFalse,
- 'is_editable_by': self.assertFalse,
+ "staff_problem_see_organization": {
+ "is_accessible_by": self.assertFalse,
+ "is_editable_by": self.assertFalse,
},
- 'normal': {
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertFalse,
+ "normal": {
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertFalse,
},
- 'anonymous': {
- 'is_accessible_by': self.assertFalse,
- 'is_editable_by': self.assertFalse,
+ "anonymous": {
+ "is_accessible_by": self.assertFalse,
+ "is_editable_by": self.assertFalse,
},
}
self._test_object_methods_with_users(self.basic_problem, data)
def test_organization_private_problem_methods(self):
- self.assertFalse(self.organization_private_problem.is_accessible_by(self.users['normal']))
- self.users['normal'].profile.organizations.add(self.organizations['open'])
- self.assertFalse(self.organization_private_problem.is_accessible_by(self.users['normal']))
- self.organization_private_problem.organizations.add(self.organizations['open'])
+ self.assertFalse(
+ self.organization_private_problem.is_accessible_by(self.users["normal"])
+ )
+ self.users["normal"].profile.organizations.add(self.organizations["open"])
+ self.assertFalse(
+ self.organization_private_problem.is_accessible_by(self.users["normal"])
+ )
+ self.organization_private_problem.organizations.add(self.organizations["open"])
data = {
- 'staff_problem_edit_own': {
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertTrue,
- 'is_subs_manageable_by': self.assertTrue,
+ "staff_problem_edit_own": {
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertTrue,
+ "is_subs_manageable_by": self.assertTrue,
},
- 'staff_problem_see_all': {
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertFalse,
- 'is_subs_manageable_by': self.assertFalse,
+ "staff_problem_see_all": {
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertFalse,
+ "is_subs_manageable_by": self.assertFalse,
},
- 'staff_problem_edit_all': {
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertTrue,
+ "staff_problem_edit_all": {
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertTrue,
},
- 'staff_problem_edit_public': {
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertTrue,
+ "staff_problem_edit_public": {
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertTrue,
},
- 'staff_problem_see_organization': {
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertFalse,
+ "staff_problem_see_organization": {
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertFalse,
},
- 'staff_problem_edit_all_with_rejudge': {
- 'is_editable_by': self.assertTrue,
- 'is_subs_manageable_by': self.assertTrue,
+ "staff_problem_edit_all_with_rejudge": {
+ "is_editable_by": self.assertTrue,
+ "is_subs_manageable_by": self.assertTrue,
},
- 'staff_problem_edit_own_no_staff': {
- 'is_editable_by': self.assertTrue,
- 'is_subs_manageable_by': self.assertFalse,
+ "staff_problem_edit_own_no_staff": {
+ "is_editable_by": self.assertTrue,
+ "is_subs_manageable_by": self.assertFalse,
},
- 'normal': {
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertFalse,
+ "normal": {
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertFalse,
},
- 'anonymous': {
- 'is_accessible_by': self.assertFalse,
- 'is_editable_by': self.assertFalse,
+ "anonymous": {
+ "is_accessible_by": self.assertFalse,
+ "is_editable_by": self.assertFalse,
},
}
self._test_object_methods_with_users(self.organization_private_problem, data)
def test_organization_admin_private_problem_methods(self):
data = {
- 'staff_problem_edit_own': {
- 'is_accessible_by': self.assertFalse,
- 'is_editable_by': self.assertFalse,
- 'is_subs_manageable_by': self.assertFalse,
+ "staff_problem_edit_own": {
+ "is_accessible_by": self.assertFalse,
+ "is_editable_by": self.assertFalse,
+ "is_subs_manageable_by": self.assertFalse,
},
- 'staff_problem_see_all': {
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertFalse,
- 'is_subs_manageable_by': self.assertFalse,
+ "staff_problem_see_all": {
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertFalse,
+ "is_subs_manageable_by": self.assertFalse,
},
- 'staff_problem_edit_all': {
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertTrue,
+ "staff_problem_edit_all": {
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertTrue,
},
- 'staff_problem_edit_public': {
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertTrue,
+ "staff_problem_edit_public": {
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertTrue,
},
- 'staff_problem_see_organization': {
- 'is_accessible_by': self.assertFalse,
- 'is_editable_by': self.assertFalse,
+ "staff_problem_see_organization": {
+ "is_accessible_by": self.assertFalse,
+ "is_editable_by": self.assertFalse,
},
- 'staff_organization_admin': {
- 'is_accessible_by': self.assertFalse,
- 'is_editable_by': self.assertFalse,
+ "staff_organization_admin": {
+ "is_accessible_by": self.assertFalse,
+ "is_editable_by": self.assertFalse,
},
- 'normal': {
- 'is_accessible_by': self.assertFalse,
- 'is_editable_by': self.assertFalse,
+ "normal": {
+ "is_accessible_by": self.assertFalse,
+ "is_editable_by": self.assertFalse,
},
- 'anonymous': {
- 'is_accessible_by': self.assertFalse,
- 'is_editable_by': self.assertFalse,
+ "anonymous": {
+ "is_accessible_by": self.assertFalse,
+ "is_editable_by": self.assertFalse,
},
}
- self._test_object_methods_with_users(self.organization_admin_private_problem, data)
+ self._test_object_methods_with_users(
+ self.organization_admin_private_problem, data
+ )
def test_organization_admin_problem_methods(self):
data = {
- 'staff_problem_edit_all': {
- 'is_accessible_by': self.assertTrue,
- 'is_editable_by': self.assertTrue,
+ "staff_problem_edit_all": {
+ "is_accessible_by": self.assertTrue,
+ "is_editable_by": self.assertTrue,
},
- 'staff_problem_edit_public': {
- 'is_accessible_by': self.assertFalse,
- 'is_editable_by': self.assertFalse,
+ "staff_problem_edit_public": {
+ "is_accessible_by": self.assertFalse,
+ "is_editable_by": self.assertFalse,
},
- 'staff_organization_admin': {
- 'is_accessible_by': self.assertFalse,
- 'is_editable_by': self.assertFalse,
+ "staff_organization_admin": {
+ "is_accessible_by": self.assertFalse,
+ "is_editable_by": self.assertFalse,
},
- 'normal': {
- 'is_accessible_by': self.assertFalse,
- 'is_editable_by': self.assertFalse,
+ "normal": {
+ "is_accessible_by": self.assertFalse,
+ "is_editable_by": self.assertFalse,
},
- 'anonymous': {
- 'is_accessible_by': self.assertFalse,
- 'is_editable_by': self.assertFalse,
+ "anonymous": {
+ "is_accessible_by": self.assertFalse,
+ "is_editable_by": self.assertFalse,
},
}
self._test_object_methods_with_users(self.organization_admin_problem, data)
@@ -249,79 +276,110 @@ def give_basic_problem_ac(self, user, points=None):
Submission.objects.create(
user=user.profile,
problem=self.basic_problem,
- result='AC',
+ result="AC",
points=self.basic_problem.points if points is None else points,
language=Language.get_python3(),
)
def test_problem_voting_permissions(self):
- self.assertEqual(self.basic_problem.vote_permission_for_user(self.users['anonymous']), VotePermission.NONE)
+ self.assertEqual(
+ self.basic_problem.vote_permission_for_user(self.users["anonymous"]),
+ VotePermission.NONE,
+ )
now = timezone.now()
basic_contest = create_contest(
- key='basic',
+ key="basic",
start_time=now - timezone.timedelta(days=1),
end_time=now + timezone.timedelta(days=100),
- authors=('superuser', 'staff_contest_edit_own'),
- testers=('non_staff_tester',),
+ authors=("superuser", "staff_contest_edit_own"),
+ testers=("non_staff_tester",),
)
- in_contest = create_user(username='in_contest')
+ in_contest = create_user(username="in_contest")
in_contest.profile.current_contest = create_contest_participation(
user=in_contest,
contest=basic_contest,
)
self.give_basic_problem_ac(in_contest)
- self.assertEqual(self.basic_problem.vote_permission_for_user(in_contest), VotePermission.NONE)
+ self.assertEqual(
+ self.basic_problem.vote_permission_for_user(in_contest), VotePermission.NONE
+ )
- unlisted = create_user(username='unlisted')
+ unlisted = create_user(username="unlisted")
unlisted.profile.is_unlisted = True
self.give_basic_problem_ac(unlisted)
- self.assertEqual(self.basic_problem.vote_permission_for_user(unlisted), VotePermission.VIEW)
+ self.assertEqual(
+ self.basic_problem.vote_permission_for_user(unlisted), VotePermission.VIEW
+ )
- banned_from_voting = create_user(username='banned_from_voting')
+ banned_from_voting = create_user(username="banned_from_voting")
banned_from_voting.profile.is_banned_from_problem_voting = True
self.give_basic_problem_ac(banned_from_voting)
- self.assertEqual(self.basic_problem.vote_permission_for_user(banned_from_voting), VotePermission.VIEW)
+ self.assertEqual(
+ self.basic_problem.vote_permission_for_user(banned_from_voting),
+ VotePermission.VIEW,
+ )
- banned_from_problem = create_user(username='banned_from_problem')
+ banned_from_problem = create_user(username="banned_from_problem")
self.basic_problem.banned_users.add(banned_from_problem.profile)
self.give_basic_problem_ac(banned_from_problem)
- self.assertEqual(self.basic_problem.vote_permission_for_user(banned_from_problem), VotePermission.VIEW)
+ self.assertEqual(
+ self.basic_problem.vote_permission_for_user(banned_from_problem),
+ VotePermission.VIEW,
+ )
- self.assertEqual(self.basic_problem.vote_permission_for_user(self.users['normal']), VotePermission.VIEW)
+ self.assertEqual(
+ self.basic_problem.vote_permission_for_user(self.users["normal"]),
+ VotePermission.VIEW,
+ )
- self.give_basic_problem_ac(self.users['normal'])
- self.assertEqual(self.basic_problem.vote_permission_for_user(self.users['normal']), VotePermission.VOTE)
+ self.give_basic_problem_ac(self.users["normal"])
+ self.assertEqual(
+ self.basic_problem.vote_permission_for_user(self.users["normal"]),
+ VotePermission.VOTE,
+ )
- partial_ac = create_user(username='partial_ac')
- self.give_basic_problem_ac(partial_ac, 0.5) # ensure this value is not equal to its point value
+ partial_ac = create_user(username="partial_ac")
+ self.give_basic_problem_ac(
+ partial_ac, 0.5
+ ) # ensure this value is not equal to its point value
self.assertNotEqual(self.basic_problem.points, 0.5)
- self.assertEqual(self.basic_problem.vote_permission_for_user(partial_ac), VotePermission.VIEW)
+ self.assertEqual(
+ self.basic_problem.vote_permission_for_user(partial_ac), VotePermission.VIEW
+ )
def test_problems_list(self):
for name, user in self.users.items():
with self.subTest(user=name):
- with self.subTest(list='accessible problems'):
+ with self.subTest(list="accessible problems"):
# We only care about consistency between Problem.is_accessible_by and Problem.get_visible_problems
problem_codes = []
- for problem in Problem.objects.prefetch_related('authors', 'curators', 'testers', 'organizations'):
+ for problem in Problem.objects.prefetch_related(
+ "authors", "curators", "testers", "organizations"
+ ):
if problem.is_accessible_by(user):
problem_codes.append(problem.code)
self.assertCountEqual(
- Problem.get_visible_problems(user).distinct().values_list('code', flat=True),
+ Problem.get_visible_problems(user)
+ .distinct()
+ .values_list("code", flat=True),
problem_codes,
)
- with self.subTest(list='editable problems'):
+ with self.subTest(list="editable problems"):
# We only care about consistency between Problem.is_editable_by and Problem.get_editable_problems
problem_codes = []
- for problem in Problem.objects.prefetch_related('authors', 'curators'):
+ for problem in Problem.objects.prefetch_related(
+ "authors", "curators"
+ ):
if problem.is_editable_by(user):
problem_codes.append(problem.code)
self.assertCountEqual(
- Problem.get_editable_problems(user).distinct().values_list('code', flat=True),
+ Problem.get_editable_problems(user)
+ .distinct()
+ .values_list("code", flat=True),
problem_codes,
)
@@ -330,103 +388,105 @@ class SolutionTestCase(CommonDataMixin, TestCase):
@classmethod
def setUpTestData(self):
super().setUpTestData()
- self.users.update({
- 'staff_solution_see_all': create_user(
- username='staff_solution_see_all',
- user_permissions=('see_private_solution',),
- ),
- })
+ self.users.update(
+ {
+ "staff_solution_see_all": create_user(
+ username="staff_solution_see_all",
+ user_permissions=("see_private_solution",),
+ ),
+ }
+ )
now = timezone.now()
- self.basic_solution = create_solution(problem='basic')
+ self.basic_solution = create_solution(problem="basic")
self.private_solution = create_solution(
- problem='private',
+ problem="private",
is_public=False,
publish_on=now - timezone.timedelta(days=100),
)
self.unpublished_problem = create_problem(
- code='unpublished',
- name='Unpublished',
- authors=('staff_problem_edit_own',),
+ code="unpublished",
+ name="Unpublished",
+ authors=("staff_problem_edit_own",),
)
self.unpublished_solution = create_solution(
problem=self.unpublished_problem,
is_public=False,
publish_on=now + timezone.timedelta(days=100),
- authors=('normal',),
+ authors=("normal",),
)
def test_unpublished_solution(self):
- self.assertEqual(str(self.unpublished_solution), 'Editorial for Unpublished')
+ self.assertEqual(str(self.unpublished_solution), "Editorial for Unpublished")
def test_basic_solution_methods(self):
data = {
- 'superuser': {
- 'is_accessible_by': self.assertTrue,
+ "superuser": {
+ "is_accessible_by": self.assertTrue,
},
- 'staff_solution_see_all': {
- 'is_accessible_by': self.assertTrue,
+ "staff_solution_see_all": {
+ "is_accessible_by": self.assertTrue,
},
- 'normal': {
- 'is_accessible_by': self.assertTrue,
+ "normal": {
+ "is_accessible_by": self.assertTrue,
},
- 'anonymous': {
- 'is_accessible_by': self.assertTrue,
+ "anonymous": {
+ "is_accessible_by": self.assertTrue,
},
}
self._test_object_methods_with_users(self.basic_solution, data)
def test_private_solution_methods(self):
data = {
- 'superuser': {
- 'is_accessible_by': self.assertTrue,
+ "superuser": {
+ "is_accessible_by": self.assertTrue,
},
- 'staff_solution_see_all': {
- 'is_accessible_by': self.assertTrue,
+ "staff_solution_see_all": {
+ "is_accessible_by": self.assertTrue,
},
- 'staff_problem_edit_own': {
- 'is_accessible_by': self.assertFalse,
+ "staff_problem_edit_own": {
+ "is_accessible_by": self.assertFalse,
},
- 'staff_problem_see_all': {
- 'is_accessible_by': self.assertFalse,
+ "staff_problem_see_all": {
+ "is_accessible_by": self.assertFalse,
},
- 'staff_problem_edit_all': {
- 'is_accessible_by': self.assertTrue,
+ "staff_problem_edit_all": {
+ "is_accessible_by": self.assertTrue,
},
- 'staff_problem_edit_public': {
- 'is_accessible_by': self.assertFalse,
+ "staff_problem_edit_public": {
+ "is_accessible_by": self.assertFalse,
},
- 'normal': {
- 'is_accessible_by': self.assertFalse,
+ "normal": {
+ "is_accessible_by": self.assertFalse,
},
- 'anonymous': {
- 'is_accessible_by': self.assertFalse,
+ "anonymous": {
+ "is_accessible_by": self.assertFalse,
},
}
self._test_object_methods_with_users(self.private_solution, data)
def test_unpublished_solution_methods(self):
data = {
- 'staff_solution_see_all': {
- 'is_accessible_by': self.assertTrue,
+ "staff_solution_see_all": {
+ "is_accessible_by": self.assertTrue,
},
- 'staff_problem_edit_own': {
- 'is_accessible_by': self.assertTrue,
+ "staff_problem_edit_own": {
+ "is_accessible_by": self.assertTrue,
},
- 'staff_problem_edit_all': {
- 'is_accessible_by': self.assertTrue,
+ "staff_problem_edit_all": {
+ "is_accessible_by": self.assertTrue,
},
- 'staff_problem_edit_public': {
- 'is_accessible_by': self.assertFalse,
+ "staff_problem_edit_public": {
+ "is_accessible_by": self.assertFalse,
},
- 'normal': {
- 'is_accessible_by': self.assertFalse,
+ "normal": {
+ "is_accessible_by": self.assertFalse,
},
- 'anonymous': {
- 'is_accessible_by': self.assertFalse,
+ "anonymous": {
+ "is_accessible_by": self.assertFalse,
},
}
self._test_object_methods_with_users(self.unpublished_solution, data)
@@ -434,17 +494,23 @@ def test_unpublished_solution_methods(self):
class DisallowedCharactersValidatorTestCase(SimpleTestCase):
def test_valid(self):
- with self.settings(DMOJ_PROBLEM_STATEMENT_DISALLOWED_CHARACTERS={'“', '”', '‘', '’'}):
- self.assertIsNone(disallowed_characters_validator(''))
- self.assertIsNone(disallowed_characters_validator('"\'string\''))
+ with self.settings(
+ DMOJ_PROBLEM_STATEMENT_DISALLOWED_CHARACTERS={"“", "”", "‘", "’"}
+ ):
+ self.assertIsNone(disallowed_characters_validator(""))
+ self.assertIsNone(disallowed_characters_validator("\"'string'"))
with self.settings(DMOJ_PROBLEM_STATEMENT_DISALLOWED_CHARACTERS=set()):
- self.assertIsNone(disallowed_characters_validator(''))
- self.assertIsNone(disallowed_characters_validator('“”‘’'))
+ self.assertIsNone(disallowed_characters_validator(""))
+ self.assertIsNone(disallowed_characters_validator("“”‘’"))
def test_invalid(self):
- with self.settings(DMOJ_PROBLEM_STATEMENT_DISALLOWED_CHARACTERS={'“', '”', '‘', '’'}):
- with self.assertRaises(ValidationError, msg='Disallowed characters: “'):
- disallowed_characters_validator('“')
- with self.assertRaisesRegex(ValidationError, 'Disallowed characters: (?=.*‘)(?=.*’)'):
- disallowed_characters_validator('‘’')
+ with self.settings(
+ DMOJ_PROBLEM_STATEMENT_DISALLOWED_CHARACTERS={"“", "”", "‘", "’"}
+ ):
+ with self.assertRaises(ValidationError, msg="Disallowed characters: “"):
+ disallowed_characters_validator("“")
+ with self.assertRaisesRegex(
+ ValidationError, "Disallowed characters: (?=.*‘)(?=.*’)"
+ ):
+ disallowed_characters_validator("‘’")
diff --git a/judge/models/tests/test_profile.py b/judge/models/tests/test_profile.py
index 91922b5a10..e50bde5b5b 100644
--- a/judge/models/tests/test_profile.py
+++ b/judge/models/tests/test_profile.py
@@ -8,36 +8,40 @@
from django.utils.encoding import force_bytes
from judge.models import Profile
-from judge.models.tests.util import CommonDataMixin, create_contest, create_contest_participation
+from judge.models.tests.util import (
+ CommonDataMixin,
+ create_contest,
+ create_contest_participation,
+)
class OrganizationTestCase(CommonDataMixin, TestCase):
@classmethod
def setUpTestData(self):
super().setUpTestData()
- self.profile = self.users['normal'].profile
- self.profile.organizations.add(self.organizations['open'])
+ self.profile = self.users["normal"].profile
+ self.profile.organizations.add(self.organizations["open"])
def test_contains(self):
- self.assertIn(self.profile, self.organizations['open'])
- self.assertIn(self.profile.id, self.organizations['open'])
+ self.assertIn(self.profile, self.organizations["open"])
+ self.assertIn(self.profile.id, self.organizations["open"])
- self.assertNotIn(self.users['superuser'].profile, self.organizations['open'])
- self.assertNotIn(self.users['superuser'].profile.id, self.organizations['open'])
+ self.assertNotIn(self.users["superuser"].profile, self.organizations["open"])
+ self.assertNotIn(self.users["superuser"].profile.id, self.organizations["open"])
- with self.assertRaisesRegex(TypeError, 'Organization membership test'):
- 'aaaa' in self.organizations['open']
+ with self.assertRaisesRegex(TypeError, "Organization membership test"):
+ "aaaa" in self.organizations["open"]
def test_str(self):
- self.assertEqual(str(self.organizations['open']), 'open')
+ self.assertEqual(str(self.organizations["open"]), "open")
class ProfileTestCase(CommonDataMixin, TestCase):
@classmethod
def setUpTestData(self):
super().setUpTestData()
- self.profile = self.users['normal'].profile
- self.profile.organizations.add(self.organizations['open'])
+ self.profile = self.users["normal"].profile
+ self.profile.organizations.add(self.organizations["open"])
def setUp(self):
# We are doing a LOT of field modifications in this test case.
@@ -50,14 +54,14 @@ def test_username(self):
self.assertEqual(str(self.profile), self.profile.username)
def test_organization(self):
- self.assertIsNone(self.users['superuser'].profile.organization)
- self.assertEqual(self.profile.organization, self.organizations['open'])
+ self.assertIsNone(self.users["superuser"].profile.organization)
+ self.assertEqual(self.profile.organization, self.organizations["open"])
def test_calculate_points(self):
self.profile.calculate_points()
# Test saving
- for attr in ('points', 'problem_count', 'performance_points'):
+ for attr in ("points", "problem_count", "performance_points"):
with self.subTest(attribute=attr):
setattr(self.profile, attr, -1000)
self.assertEqual(getattr(self.profile, attr), -1000)
@@ -70,14 +74,18 @@ def test_generate_api_token(self):
self.assertIsInstance(token, str)
self.assertIsInstance(self.profile.api_token, str)
- user_id, raw_token = struct.unpack('>I32s', base64.urlsafe_b64decode(token))
+ user_id, raw_token = struct.unpack(">I32s", base64.urlsafe_b64decode(token))
- self.assertEqual(self.users['normal'].id, user_id)
+ self.assertEqual(self.users["normal"].id, user_id)
self.assertEqual(len(raw_token), 32)
self.assertTrue(
hmac.compare_digest(
- hmac.new(force_bytes(settings.SECRET_KEY), msg=force_bytes(raw_token), digestmod='sha256').hexdigest(),
+ hmac.new(
+ force_bytes(settings.SECRET_KEY),
+ msg=force_bytes(raw_token),
+ digestmod="sha256",
+ ).hexdigest(),
self.profile.api_token,
),
)
@@ -86,13 +94,13 @@ def test_update_contest(self):
_now = timezone.now()
for contest in (
create_contest(
- key='finished_contest',
+ key="finished_contest",
start_time=_now - timezone.timedelta(days=100),
end_time=_now - timezone.timedelta(days=10),
is_visible=True,
),
create_contest(
- key='inaccessible_contest',
+ key="inaccessible_contest",
start_time=_now - timezone.timedelta(days=100),
end_time=_now + timezone.timedelta(days=10),
),
@@ -107,22 +115,30 @@ def test_update_contest(self):
self.assertIsNone(self.profile.current_contest)
def test_css_class(self):
- self.assertEqual(self.profile.css_class, 'rating rate-none user')
+ self.assertEqual(self.profile.css_class, "rating rate-none user")
def test_get_user_css_class(self):
self.assertEqual(
- Profile.get_user_css_class(display_rank='abcdef', rating=None, rating_colors=True),
- 'rating rate-none abcdef',
+ Profile.get_user_css_class(
+ display_rank="abcdef", rating=None, rating_colors=True
+ ),
+ "rating rate-none abcdef",
)
self.assertEqual(
- Profile.get_user_css_class(display_rank='admin', rating=1300, rating_colors=True),
- 'rating rate-expert admin',
+ Profile.get_user_css_class(
+ display_rank="admin", rating=1300, rating_colors=True
+ ),
+ "rating rate-expert admin",
)
self.assertEqual(
- Profile.get_user_css_class(display_rank=1111, rating=1299, rating_colors=True),
- 'rating rate-amateur 1111',
+ Profile.get_user_css_class(
+ display_rank=1111, rating=1299, rating_colors=True
+ ),
+ "rating rate-amateur 1111",
)
self.assertEqual(
- Profile.get_user_css_class(display_rank='random', rating=1299, rating_colors=False),
- 'random',
+ Profile.get_user_css_class(
+ display_rank="random", rating=1299, rating_colors=False
+ ),
+ "random",
)
diff --git a/judge/models/tests/test_submission.py b/judge/models/tests/test_submission.py
index 8274d52488..9f4e57bb5d 100644
--- a/judge/models/tests/test_submission.py
+++ b/judge/models/tests/test_submission.py
@@ -2,8 +2,14 @@
from django.utils import timezone
from judge.models import ContestSubmission, Language, Submission, SubmissionSource
-from judge.models.tests.util import CommonDataMixin, create_contest, create_contest_participation, \
- create_contest_problem, create_problem, create_user
+from judge.models.tests.util import (
+ CommonDataMixin,
+ create_contest,
+ create_contest_participation,
+ create_contest_problem,
+ create_problem,
+ create_user,
+)
class SubmissionTestCase(CommonDataMixin, TestCase):
@@ -11,20 +17,22 @@ class SubmissionTestCase(CommonDataMixin, TestCase):
def setUpTestData(self):
super().setUpTestData()
- self.users.update({
- 'staff_submission_view_all': create_user(
- username='staff_submission_view_all',
- is_staff=True,
- user_permissions=('view_all_submission',),
- ),
- })
+ self.users.update(
+ {
+ "staff_submission_view_all": create_user(
+ username="staff_submission_view_all",
+ is_staff=True,
+ user_permissions=("view_all_submission",),
+ ),
+ }
+ )
self.basic_submission = Submission.objects.create(
- user=self.users['normal'].profile,
- problem=create_problem(code='basic'),
+ user=self.users["normal"].profile,
+ problem=create_problem(code="basic"),
language=Language.get_python3(),
- result='AC',
- status='D',
+ result="AC",
+ status="D",
case_points=99,
case_total=100,
memory=20,
@@ -32,58 +40,58 @@ def setUpTestData(self):
)
self.full_ac_submission = Submission.objects.create(
- user=self.users['normal'].profile,
- problem=create_problem(code='full_ac'),
+ user=self.users["normal"].profile,
+ problem=create_problem(code="full_ac"),
language=Language.get_python3(),
- result='AC',
- status='D',
+ result="AC",
+ status="D",
case_points=1,
case_total=1,
)
self.full_ac_submission_source = SubmissionSource.objects.create(
submission=self.full_ac_submission,
- source='',
+ source="",
)
self.locked_submission = Submission.objects.create(
- user=self.users['normal'].profile,
- problem=create_problem(code='locked'),
+ user=self.users["normal"].profile,
+ problem=create_problem(code="locked"),
language=Language.get_python3(),
- result='WA',
- status='D',
+ result="WA",
+ status="D",
case_points=1,
case_total=1,
locked_after=timezone.now() - timezone.timedelta(days=100),
)
self.future_locked_submission = Submission.objects.create(
- user=self.users['normal'].profile,
- problem=create_problem(code='future_locked'),
+ user=self.users["normal"].profile,
+ problem=create_problem(code="future_locked"),
language=Language.get_python3(),
- result='WA',
- status='D',
+ result="WA",
+ status="D",
case_points=1,
case_total=1,
locked_after=timezone.now() + timezone.timedelta(days=100),
)
self.ie_submission = Submission.objects.create(
- user=self.users['superuser'].profile,
+ user=self.users["superuser"].profile,
problem=create_problem(
- code='ie',
+ code="ie",
is_public=True,
),
language=Language.get_python3(),
- result='IE',
- status='IE',
+ result="IE",
+ status="IE",
memory=None,
)
# jump down the rabbit hole to attach a contest submission
- problem = create_problem(code='queued')
- contest = create_contest(key='queued')
+ problem = create_problem(code="queued")
+ contest = create_contest(key="queued")
self.queued_submission = Submission.objects.create(
- user=self.users['superuser'].profile,
+ user=self.users["superuser"].profile,
problem=problem,
language=Language.get_python3(),
contest_object=contest,
@@ -92,15 +100,19 @@ def setUpTestData(self):
)
self.queued_contest_submission = ContestSubmission.objects.create(
submission=self.queued_submission,
- problem=create_contest_problem(problem=problem, contest=contest, partial=False),
- participation=create_contest_participation(contest=contest, user='superuser'),
+ problem=create_contest_problem(
+ problem=problem, contest=contest, partial=False
+ ),
+ participation=create_contest_participation(
+ contest=contest, user="superuser"
+ ),
)
def test_basic_submission(self):
- self.assertEqual(self.basic_submission.result_class, '_AC')
+ self.assertEqual(self.basic_submission.result_class, "_AC")
self.assertEqual(self.basic_submission.memory_bytes, 20 * 1024)
- self.assertEqual(self.basic_submission.short_status, 'AC')
- self.assertEqual(self.basic_submission.long_status, 'Accepted')
+ self.assertEqual(self.basic_submission.short_status, "AC")
+ self.assertEqual(self.basic_submission.long_status, "Accepted")
self.assertTrue(self.basic_submission.is_graded)
self.assertIsNone(self.basic_submission.contest_key)
self.assertIsNone(self.basic_submission.contest_or_none)
@@ -108,12 +120,12 @@ def test_basic_submission(self):
self.assertFalse(self.basic_submission.is_locked)
def test_full_ac_submission(self):
- self.assertEqual(self.full_ac_submission.result_class, 'AC')
- self.assertEqual(self.full_ac_submission.short_status, 'AC')
+ self.assertEqual(self.full_ac_submission.result_class, "AC")
+ self.assertEqual(self.full_ac_submission.short_status, "AC")
self.assertEqual(
str(self.full_ac_submission_source),
- 'Source of Submission %d of full_ac by normal' % self.full_ac_submission.id,
+ "Source of Submission %d of full_ac by normal" % self.full_ac_submission.id,
)
def test_submission_lock(self):
@@ -121,7 +133,7 @@ def test_submission_lock(self):
self.assertFalse(self.future_locked_submission.is_locked)
def test_ie_submission(self):
- self.assertEqual(self.ie_submission.result_class, 'IE')
+ self.assertEqual(self.ie_submission.result_class, "IE")
self.assertEqual(self.ie_submission.memory_bytes, 0)
self.assertTrue(self.basic_submission.is_graded)
@@ -130,11 +142,11 @@ def test_ie_submission(self):
def test_queued_submission(self):
self.assertIsNone(self.queued_submission.result_class)
self.assertEqual(self.queued_submission.memory_bytes, 0)
- self.assertEqual(self.queued_submission.short_status, 'QU')
- self.assertEqual(self.queued_submission.long_status, 'Queued')
+ self.assertEqual(self.queued_submission.short_status, "QU")
+ self.assertEqual(self.queued_submission.long_status, "Queued")
self.assertFalse(self.queued_submission.is_graded)
- self.assertEqual(self.queued_submission.contest_key, 'queued')
+ self.assertEqual(self.queued_submission.contest_key, "queued")
self.assertIsNotNone(self.queued_submission.contest_or_none)
self.queued_contest_submission.points = -1000
self.queued_submission.update_contest()
@@ -142,49 +154,49 @@ def test_queued_submission(self):
def test_basic_submission_methods(self):
data = {
- 'superuser': {
- 'can_see_detail': self.assertTrue,
+ "superuser": {
+ "can_see_detail": self.assertTrue,
},
- 'staff_problem_edit_own': {
- 'can_see_detail': self.assertFalse,
+ "staff_problem_edit_own": {
+ "can_see_detail": self.assertFalse,
},
- 'staff_problem_edit_all': {
- 'can_see_detail': self.assertTrue,
+ "staff_problem_edit_all": {
+ "can_see_detail": self.assertTrue,
},
- 'staff_problem_edit_public': {
- 'can_see_detail': self.assertFalse,
+ "staff_problem_edit_public": {
+ "can_see_detail": self.assertFalse,
},
- 'staff_problem_see_organization': {
- 'can_see_detail': self.assertFalse,
+ "staff_problem_see_organization": {
+ "can_see_detail": self.assertFalse,
},
- 'staff_submission_view_all': {
- 'can_see_detail': self.assertTrue,
+ "staff_submission_view_all": {
+ "can_see_detail": self.assertTrue,
},
- 'normal': {
- 'can_see_detail': self.assertTrue,
+ "normal": {
+ "can_see_detail": self.assertTrue,
},
- 'anonymous': {
- 'can_see_detail': self.assertFalse,
+ "anonymous": {
+ "can_see_detail": self.assertFalse,
},
}
self._test_object_methods_with_users(self.basic_submission, data)
def test_ie_submission_methods(self):
data = {
- 'staff_problem_edit_own': {
- 'can_see_detail': self.assertFalse,
+ "staff_problem_edit_own": {
+ "can_see_detail": self.assertFalse,
},
- 'staff_problem_edit_all': {
- 'can_see_detail': self.assertTrue,
+ "staff_problem_edit_all": {
+ "can_see_detail": self.assertTrue,
},
- 'staff_problem_edit_public': {
- 'can_see_detail': self.assertTrue,
+ "staff_problem_edit_public": {
+ "can_see_detail": self.assertTrue,
},
- 'staff_submission_view_all': {
- 'can_see_detail': self.assertTrue,
+ "staff_submission_view_all": {
+ "can_see_detail": self.assertTrue,
},
- 'normal': {
- 'can_see_detail': self.assertFalse,
+ "normal": {
+ "can_see_detail": self.assertFalse,
},
}
self._test_object_methods_with_users(self.ie_submission, data)
diff --git a/judge/models/tests/util.py b/judge/models/tests/util.py
index 953308d7c3..a5054592f4 100644
--- a/judge/models/tests/util.py
+++ b/judge/models/tests/util.py
@@ -1,8 +1,20 @@
from django.contrib.auth.models import AnonymousUser, Permission, User
from django.utils import timezone
-from judge.models import BlogPost, Contest, ContestParticipation, ContestProblem, ContestTag, Language, Organization, \
- Problem, ProblemGroup, ProblemType, Profile, Solution
+from judge.models import (
+ BlogPost,
+ Contest,
+ ContestParticipation,
+ ContestProblem,
+ ContestTag,
+ Language,
+ Organization,
+ Problem,
+ ProblemGroup,
+ ProblemType,
+ Profile,
+ Solution,
+)
class CreateModel:
@@ -30,7 +42,7 @@ def __call__(self, *args, **kwargs):
for field in self.required_fields:
if field not in kwargs:
- raise ValueError('%s field is required.' % field)
+ raise ValueError("%s field is required." % field)
required_kwargs = {key: kwargs.pop(key) for key in self.required_fields}
@@ -48,7 +60,11 @@ def __call__(self, *args, **kwargs):
if created:
for field, relationship in self.m2m_fields.items():
related_model, query_field = relationship
- getattr(obj, field).set(related_model.objects.filter(**{query_field + '__in': m2m_data[field]}))
+ getattr(obj, field).set(
+ related_model.objects.filter(
+ **{query_field + "__in": m2m_data[field]}
+ )
+ )
self.on_created_object(obj)
@@ -58,15 +74,15 @@ def __call__(self, *args, **kwargs):
class CreateBlogPost(CreateModel):
model = BlogPost
m2m_fields = {
- 'authors': (Profile, 'user__username'),
+ "authors": (Profile, "user__username"),
}
- required_fields = ('title',)
+ required_fields = ("title",)
def get_defaults(self, required_kwargs, kwargs):
_now = timezone.now()
return {
- 'slug': required_kwargs['title'],
- 'publish_on': _now - timezone.timedelta(days=100),
+ "slug": required_kwargs["title"],
+ "publish_on": _now - timezone.timedelta(days=100),
}
@@ -76,9 +92,9 @@ def get_defaults(self, required_kwargs, kwargs):
class CreateUser(CreateModel):
model = User
m2m_fields = {
- 'user_permissions': (Permission, 'codename'),
+ "user_permissions": (Permission, "codename"),
}
- required_fields = ('username',)
+ required_fields = ("username",)
def on_created_object(self, obj):
Profile.objects.get_or_create(user=obj)
@@ -90,14 +106,14 @@ def on_created_object(self, obj):
class CreateOrganization(CreateModel):
model = Organization
m2m_fields = {
- 'admins': (Profile, 'user__username'),
+ "admins": (Profile, "user__username"),
}
- required_fields = ('name',)
+ required_fields = ("name",)
def get_defaults(self, required_kwargs, kwargs):
return {
- 'slug': required_kwargs['name'],
- 'short_name': required_kwargs['name'],
+ "slug": required_kwargs["name"],
+ "short_name": required_kwargs["name"],
}
@@ -106,11 +122,11 @@ def get_defaults(self, required_kwargs, kwargs):
class CreateProblemGroup(CreateModel):
model = ProblemGroup
- required_fields = ('name',)
+ required_fields = ("name",)
def get_defaults(self, required_kwargs, kwargs):
return {
- 'full_name': required_kwargs['name'],
+ "full_name": required_kwargs["name"],
}
@@ -119,11 +135,11 @@ def get_defaults(self, required_kwargs, kwargs):
class CreateProblemType(CreateModel):
model = ProblemType
- required_fields = ('name',)
+ required_fields = ("name",)
def get_defaults(self, required_kwargs, kwargs):
return {
- 'full_name': required_kwargs['name'],
+ "full_name": required_kwargs["name"],
}
@@ -133,29 +149,29 @@ def get_defaults(self, required_kwargs, kwargs):
class CreateProblem(CreateModel):
model = Problem
m2m_fields = {
- 'authors': (Profile, 'user__username'),
- 'curators': (Profile, 'user__username'),
- 'testers': (Profile, 'user__username'),
- 'types': (ProblemType, 'name'),
- 'allowed_languages': (Language, 'key'),
- 'banned_users': (Profile, 'user__username'),
- 'organizations': (Organization, 'name'),
+ "authors": (Profile, "user__username"),
+ "curators": (Profile, "user__username"),
+ "testers": (Profile, "user__username"),
+ "types": (ProblemType, "name"),
+ "allowed_languages": (Language, "key"),
+ "banned_users": (Profile, "user__username"),
+ "organizations": (Organization, "name"),
}
- required_fields = ('code',)
+ required_fields = ("code",)
def get_defaults(self, required_kwargs, kwargs):
return {
- 'name': required_kwargs['code'],
- 'description': '',
- 'time_limit': 1,
- 'memory_limit': 65536,
- 'points': 1,
- 'group': 'group',
+ "name": required_kwargs["code"],
+ "description": "",
+ "time_limit": 1,
+ "memory_limit": 65536,
+ "points": 1,
+ "group": "group",
}
def process_related_objects(self, required_kwargs, defaults):
- if not isinstance(defaults['group'], ProblemGroup):
- defaults['group'] = create_problem_group(defaults['group'])
+ if not isinstance(defaults["group"], ProblemGroup):
+ defaults["group"] = create_problem_group(defaults["group"])
create_problem = CreateProblem()
@@ -164,21 +180,21 @@ def process_related_objects(self, required_kwargs, defaults):
class CreateSolution(CreateModel):
model = Solution
m2m_fields = {
- 'authors': (Profile, 'user__username'),
+ "authors": (Profile, "user__username"),
}
- required_fields = ('problem',)
+ required_fields = ("problem",)
def get_defaults(self, required_kwargs, kwargs):
_now = timezone.now()
return {
- 'is_public': True,
- 'publish_on': _now - timezone.timedelta(days=4),
- 'content': '',
+ "is_public": True,
+ "publish_on": _now - timezone.timedelta(days=4),
+ "content": "",
}
def process_related_objects(self, required_kwargs, defaults):
- if not isinstance(required_kwargs['problem'], Problem):
- required_kwargs['problem'] = create_problem(required_kwargs['problem'])
+ if not isinstance(required_kwargs["problem"], Problem):
+ required_kwargs["problem"] = create_problem(required_kwargs["problem"])
create_solution = CreateSolution()
@@ -187,28 +203,28 @@ def process_related_objects(self, required_kwargs, defaults):
class CreateContest(CreateModel):
model = Contest
m2m_fields = {
- 'authors': (Profile, 'user__username'),
- 'curators': (Profile, 'user__username'),
- 'testers': (Profile, 'user__username'),
- 'spectators': (Profile, 'user__username'),
- 'problems': (Problem, 'code'),
- 'view_contest_scoreboard': (Profile, 'user__username'),
- 'rate_exclude': (Profile, 'user__username'),
- 'private_contestants': (Profile, 'user__username'),
- 'organizations': (Organization, 'name'),
- 'join_organizations': (Organization, 'name'),
- 'tags': (ContestTag, 'name'),
- 'banned_users': (Profile, 'user__username'),
+ "authors": (Profile, "user__username"),
+ "curators": (Profile, "user__username"),
+ "testers": (Profile, "user__username"),
+ "spectators": (Profile, "user__username"),
+ "problems": (Problem, "code"),
+ "view_contest_scoreboard": (Profile, "user__username"),
+ "rate_exclude": (Profile, "user__username"),
+ "private_contestants": (Profile, "user__username"),
+ "organizations": (Organization, "name"),
+ "join_organizations": (Organization, "name"),
+ "tags": (ContestTag, "name"),
+ "banned_users": (Profile, "user__username"),
}
- required_fields = ('key',)
+ required_fields = ("key",)
def get_defaults(self, required_kwargs, kwargs):
_now = timezone.now()
return {
- 'name': required_kwargs['key'],
- 'description': '',
- 'start_time': _now - timezone.timedelta(days=100),
- 'end_time': _now + timezone.timedelta(days=100),
+ "name": required_kwargs["key"],
+ "description": "",
+ "start_time": _now - timezone.timedelta(days=100),
+ "end_time": _now + timezone.timedelta(days=100),
}
@@ -217,13 +233,13 @@ def get_defaults(self, required_kwargs, kwargs):
class CreateContestParticipation(CreateModel):
model = ContestParticipation
- required_fields = ('contest', 'user')
+ required_fields = ("contest", "user")
def process_related_objects(self, required_kwargs, defaults):
- if not isinstance(required_kwargs['contest'], Contest):
- required_kwargs['contest'] = create_contest(required_kwargs['contest'])
- if not isinstance(required_kwargs['user'], Profile):
- required_kwargs['user'] = create_user(required_kwargs['user']).profile
+ if not isinstance(required_kwargs["contest"], Contest):
+ required_kwargs["contest"] = create_contest(required_kwargs["contest"])
+ if not isinstance(required_kwargs["user"], Profile):
+ required_kwargs["user"] = create_user(required_kwargs["user"]).profile
create_contest_participation = CreateContestParticipation()
@@ -231,82 +247,86 @@ def process_related_objects(self, required_kwargs, defaults):
class CreateContestProblem(CreateModel):
model = ContestProblem
- required_fields = ('contest', 'problem')
+ required_fields = ("contest", "problem")
def get_defaults(self, required_kwargs, kwargs):
return {
- 'points': 100,
- 'order': 1,
+ "points": 100,
+ "order": 1,
}
def process_related_objects(self, required_kwargs, defaults):
- if not isinstance(required_kwargs['contest'], Contest):
- required_kwargs['contest'] = create_contest(required_kwargs['contest'])
- if not isinstance(required_kwargs['problem'], Problem):
- required_kwargs['problem'] = create_problem(required_kwargs['problem'])
+ if not isinstance(required_kwargs["contest"], Contest):
+ required_kwargs["contest"] = create_contest(required_kwargs["contest"])
+ if not isinstance(required_kwargs["problem"], Problem):
+ required_kwargs["problem"] = create_problem(required_kwargs["problem"])
create_contest_problem = CreateContestProblem()
class CommonDataMixin:
- fixtures = ['language_all.json']
+ fixtures = ["language_all.json"]
@classmethod
def setUpTestData(self):
self.users = {
- 'superuser': create_user(
- username='superuser',
+ "superuser": create_user(
+ username="superuser",
is_superuser=True,
is_staff=True,
),
- 'staff_problem_edit_own': create_user(
- username='staff_problem_edit_own',
+ "staff_problem_edit_own": create_user(
+ username="staff_problem_edit_own",
is_staff=True,
- user_permissions=('edit_own_problem', 'rejudge_submission'),
+ user_permissions=("edit_own_problem", "rejudge_submission"),
),
- 'staff_problem_see_all': create_user(
- username='staff_problem_see_all',
- user_permissions=('see_private_problem',),
+ "staff_problem_see_all": create_user(
+ username="staff_problem_see_all",
+ user_permissions=("see_private_problem",),
),
- 'staff_problem_edit_all': create_user(
- username='staff_problem_edit_all',
+ "staff_problem_edit_all": create_user(
+ username="staff_problem_edit_all",
is_staff=True,
- user_permissions=('edit_own_problem', 'edit_all_problem'),
+ user_permissions=("edit_own_problem", "edit_all_problem"),
),
- 'staff_problem_edit_public': create_user(
- username='staff_problem_edit_public',
+ "staff_problem_edit_public": create_user(
+ username="staff_problem_edit_public",
is_staff=True,
- user_permissions=('edit_own_problem', 'edit_public_problem'),
+ user_permissions=("edit_own_problem", "edit_public_problem"),
),
- 'staff_problem_see_organization': create_user(
- username='staff_problem_see_organization',
- user_permissions=('see_organization_problem',),
+ "staff_problem_see_organization": create_user(
+ username="staff_problem_see_organization",
+ user_permissions=("see_organization_problem",),
),
- 'staff_problem_edit_all_with_rejudge': create_user(
- username='staff_problem_edit_all_with_rejudge',
+ "staff_problem_edit_all_with_rejudge": create_user(
+ username="staff_problem_edit_all_with_rejudge",
is_staff=True,
- user_permissions=('edit_own_problem', 'edit_all_problem', 'rejudge_submission'),
+ user_permissions=(
+ "edit_own_problem",
+ "edit_all_problem",
+ "rejudge_submission",
+ ),
),
- 'staff_problem_edit_own_no_staff': create_user(
- username='staff_problem_edit_own_no_staff',
- user_permissions=('edit_own_problem', 'rejudge_submission'),
+ "staff_problem_edit_own_no_staff": create_user(
+ username="staff_problem_edit_own_no_staff",
+ user_permissions=("edit_own_problem", "rejudge_submission"),
),
- 'staff_organization_admin': create_user(
- username='staff_organization_admin',
+ "staff_organization_admin": create_user(
+ username="staff_organization_admin",
is_staff=True,
- user_permissions=('organization_admin',),
+ user_permissions=("organization_admin",),
),
- 'normal': create_user(
- username='normal',
+ "normal": create_user(
+ username="normal",
),
- 'anonymous': AnonymousUser(),
+ "anonymous": AnonymousUser(),
}
self.organizations = {
- 'open': create_organization(
- name='open',
- admins=('staff_organization_admin',),
+ "open": create_organization(
+ name="open",
+ admins=("staff_organization_admin",),
),
}
@@ -317,5 +337,6 @@ def _test_object_methods_with_users(self, obj, data):
with self.subTest(method=method):
func(
getattr(obj, method)(self.users[username]),
- msg='Method "%s" failed for user "%s", object "%s".' % (method, username, obj),
+ msg='Method "%s" failed for user "%s", object "%s".'
+ % (method, username, obj),
)
diff --git a/judge/models/ticket.py b/judge/models/ticket.py
index 7203f759a3..5df771c4f1 100644
--- a/judge/models/ticket.py
+++ b/judge/models/ticket.py
@@ -7,33 +7,54 @@
class Ticket(models.Model):
- title = models.CharField(max_length=100, verbose_name=_('ticket title'))
- user = models.ForeignKey(Profile, verbose_name=_('ticket creator'), related_name='tickets',
- on_delete=models.CASCADE)
- time = models.DateTimeField(verbose_name=_('creation time'), auto_now_add=True)
- assignees = models.ManyToManyField(Profile, verbose_name=_('assignees'), related_name='assigned_tickets',
- blank=True)
- notes = models.TextField(verbose_name=_('quick notes'), blank=True,
- help_text=_('Staff notes for this issue to aid in processing.'))
- content_type = models.ForeignKey(ContentType, verbose_name=_('linked item type'),
- on_delete=models.CASCADE)
- object_id = models.PositiveIntegerField(verbose_name=_('linked item ID'))
+ title = models.CharField(max_length=100, verbose_name=_("ticket title"))
+ user = models.ForeignKey(
+ Profile,
+ verbose_name=_("ticket creator"),
+ related_name="tickets",
+ on_delete=models.CASCADE,
+ )
+ time = models.DateTimeField(verbose_name=_("creation time"), auto_now_add=True)
+ assignees = models.ManyToManyField(
+ Profile,
+ verbose_name=_("assignees"),
+ related_name="assigned_tickets",
+ blank=True,
+ )
+ notes = models.TextField(
+ verbose_name=_("quick notes"),
+ blank=True,
+ help_text=_("Staff notes for this issue to aid in processing."),
+ )
+ content_type = models.ForeignKey(
+ ContentType, verbose_name=_("linked item type"), on_delete=models.CASCADE
+ )
+ object_id = models.PositiveIntegerField(verbose_name=_("linked item ID"))
linked_item = GenericForeignKey()
- is_open = models.BooleanField(verbose_name=_('is ticket open?'), default=True)
+ is_open = models.BooleanField(verbose_name=_("is ticket open?"), default=True)
class Meta:
- verbose_name = _('ticket')
- verbose_name_plural = _('tickets')
+ verbose_name = _("ticket")
+ verbose_name_plural = _("tickets")
class TicketMessage(models.Model):
- ticket = models.ForeignKey(Ticket, verbose_name=_('ticket'), related_name='messages',
- related_query_name='message', on_delete=models.CASCADE)
- user = models.ForeignKey(Profile, verbose_name=_('user'), related_name='ticket_messages',
- on_delete=models.CASCADE)
- body = models.TextField(verbose_name=_('message body'))
- time = models.DateTimeField(verbose_name=_('message time'), auto_now_add=True)
+ ticket = models.ForeignKey(
+ Ticket,
+ verbose_name=_("ticket"),
+ related_name="messages",
+ related_query_name="message",
+ on_delete=models.CASCADE,
+ )
+ user = models.ForeignKey(
+ Profile,
+ verbose_name=_("user"),
+ related_name="ticket_messages",
+ on_delete=models.CASCADE,
+ )
+ body = models.TextField(verbose_name=_("message body"))
+ time = models.DateTimeField(verbose_name=_("message time"), auto_now_add=True)
class Meta:
- verbose_name = _('ticket message')
- verbose_name_plural = _('ticket messages')
+ verbose_name = _("ticket message")
+ verbose_name_plural = _("ticket messages")
diff --git a/judge/performance_points.py b/judge/performance_points.py
index 30de63e19d..5cee331dd6 100644
--- a/judge/performance_points.py
+++ b/judge/performance_points.py
@@ -6,18 +6,24 @@
from judge.models import Submission
from judge.timezone import from_database_time
-PP_WEIGHT_TABLE = [pow(settings.DMOJ_PP_STEP, i) for i in range(settings.DMOJ_PP_ENTRIES)]
+PP_WEIGHT_TABLE = [
+ pow(settings.DMOJ_PP_STEP, i) for i in range(settings.DMOJ_PP_ENTRIES)
+]
-PPBreakdown = namedtuple('PPBreakdown', 'points weight scaled_points problem_name problem_code '
- 'sub_id sub_date sub_points sub_total sub_result_class '
- 'sub_short_status sub_long_status sub_lang')
+PPBreakdown = namedtuple(
+ "PPBreakdown",
+ "points weight scaled_points problem_name problem_code "
+ "sub_id sub_date sub_points sub_total sub_result_class "
+ "sub_short_status sub_long_status sub_lang",
+)
def get_pp_breakdown(user, start=0, end=settings.DMOJ_PP_ENTRIES):
- join_type = 'STRAIGHT_JOIN' if connection.vendor == 'mysql' else 'INNER JOIN'
+ join_type = "STRAIGHT_JOIN" if connection.vendor == "mysql" else "INNER JOIN"
with connection.cursor() as cursor:
- cursor.execute(f"""
+ cursor.execute(
+ f"""
SELECT max_points_table.problem_code,
max_points_table.problem_name,
max_points_table.max_points,
@@ -51,32 +57,49 @@ def get_pp_breakdown(user, start=0, end=settings.DMOJ_PP_ENTRIES):
GROUP BY max_points_table.problem_id
ORDER BY max_points DESC, judge_submission.date DESC
LIMIT %s OFFSET %s
- """, (user.id, user.id, end - start + 1, start))
+ """,
+ (user.id, user.id, end - start + 1, start),
+ )
data = cursor.fetchall()
breakdown = []
for weight, contrib in zip(PP_WEIGHT_TABLE[start:end], data):
- code, name, points, id, date, case_points, case_total, result, lang_short_name, lang_key = contrib
+ (
+ code,
+ name,
+ points,
+ id,
+ date,
+ case_points,
+ case_total,
+ result,
+ lang_short_name,
+ lang_key,
+ ) = contrib
# Replicates a lot of the logic usually done on Submission objects
lang_short_display_name = lang_short_name or lang_key
- result_class = Submission.result_class_from_code(result, case_points, case_total)
- long_status = Submission.USER_DISPLAY_CODES.get(result, '')
+ result_class = Submission.result_class_from_code(
+ result, case_points, case_total
+ )
+ long_status = Submission.USER_DISPLAY_CODES.get(result, "")
- breakdown.append(PPBreakdown(
- points=points,
- weight=weight * 100,
- scaled_points=points * weight,
- problem_name=name,
- problem_code=code,
- sub_id=id,
- sub_date=from_database_time(date),
- sub_points=case_points,
- sub_total=case_total,
- sub_short_status=result,
- sub_long_status=long_status,
- sub_result_class=result_class,
- sub_lang=lang_short_display_name,
- ))
+ breakdown.append(
+ PPBreakdown(
+ points=points,
+ weight=weight * 100,
+ scaled_points=points * weight,
+ problem_name=name,
+ problem_code=code,
+ sub_id=id,
+ sub_date=from_database_time(date),
+ sub_points=case_points,
+ sub_total=case_total,
+ sub_short_status=result,
+ sub_long_status=long_status,
+ sub_result_class=result_class,
+ sub_lang=lang_short_display_name,
+ )
+ )
has_more = end < min(len(PP_WEIGHT_TABLE), start + len(data))
return breakdown, has_more
diff --git a/judge/ratings.py b/judge/ratings.py
index 05e8d1d59b..46c3a3dba9 100644
--- a/judge/ratings.py
+++ b/judge/ratings.py
@@ -9,19 +9,21 @@
from django.utils.translation import gettext_lazy
-BETA2 = 328.33 ** 2
-RATING_INIT = 1200 # Newcomer's rating when applying the rating floor/ceiling
-MEAN_INIT = 1500.
+BETA2 = 328.33**2
+RATING_INIT = 1200 # Newcomer's rating when applying the rating floor/ceiling
+MEAN_INIT = 1500.0
VAR_INIT = 350**2 * (BETA2 / 212**2)
SD_INIT = sqrt(VAR_INIT)
VALID_RANGE = MEAN_INIT - 20 * SD_INIT, MEAN_INIT + 20 * SD_INIT
VAR_PER_CONTEST = 1219.047619 * (BETA2 / 212**2)
-VAR_LIM = (sqrt(VAR_PER_CONTEST**2 + 4 * BETA2 * VAR_PER_CONTEST) - VAR_PER_CONTEST) / 2
+VAR_LIM = (
+ sqrt(VAR_PER_CONTEST**2 + 4 * BETA2 * VAR_PER_CONTEST) - VAR_PER_CONTEST
+) / 2
SD_LIM = sqrt(VAR_LIM)
TANH_C = sqrt(3) / pi
-def tie_ranker(iterable, key=attrgetter('points')):
+def tie_ranker(iterable, key=attrgetter("points")):
rank = 0
delta = 1
last = None
@@ -72,15 +74,15 @@ def solve(tanh_terms, y_tg, lin_factor=0, bounds=VALID_RANGE):
def get_var(times_ranked, cache=[VAR_INIT]):
while times_ranked >= len(cache):
- next_var = 1. / (1. / (cache[-1] + VAR_PER_CONTEST) + 1. / BETA2)
+ next_var = 1.0 / (1.0 / (cache[-1] + VAR_PER_CONTEST) + 1.0 / BETA2)
cache.append(next_var)
return cache[times_ranked]
def recalculate_ratings(ranking, old_mean, times_ranked, historical_p):
n = len(ranking)
- new_p = [0.] * n
- new_mean = [0.] * n
+ new_p = [0.0] * n
+ new_mean = [0.0] * n
# Note: pre-multiply delta by TANH_C to improve efficiency.
delta = [TANH_C * sqrt(get_var(t) + VAR_PER_CONTEST + BETA2) for t in times_ranked]
@@ -91,10 +93,10 @@ def solve_idx(i, bounds=VALID_RANGE):
r = ranking[i]
y_tg = 0
for d, s in zip(delta, ranking):
- if s > r: # s loses to r
- y_tg += 1. / d
- elif s < r: # s beats r
- y_tg -= 1. / d
+ if s > r: # s loses to r
+ y_tg += 1.0 / d
+ elif s < r: # s beats r
+ y_tg -= 1.0 / d
# Otherwise, this is a tie that counts as half a win, as per Elo-MMR.
new_p[i] = solve(p_tanh_terms, y_tg, bounds=bounds)
@@ -118,10 +120,10 @@ def divconq(i, j):
# Calculate mean.
for i, r in enumerate(ranking):
tanh_terms = []
- w_prev = 1.
- w_sum = 0.
+ w_prev = 1.0
+ w_sum = 0.0
for j, h in enumerate([new_p[i]] + historical_p[i]):
- gamma2 = (VAR_PER_CONTEST if j > 0 else 0)
+ gamma2 = VAR_PER_CONTEST if j > 0 else 0
h_var = get_var(times_ranked[i] + 1 - j)
k = h_var / (h_var + gamma2)
w = w_prev * k**2
@@ -129,13 +131,16 @@ def divconq(i, j):
tanh_terms.append((h, sqrt(BETA2) * TANH_C, w))
w_prev = w
w_sum += w / BETA2
- w0 = 1. / get_var(times_ranked[i] + 1) - w_sum
+ w0 = 1.0 / get_var(times_ranked[i] + 1) - w_sum
p0 = eval_tanhs(tanh_terms[1:], old_mean[i]) / w0 + old_mean[i]
new_mean[i] = solve(tanh_terms, w0 * p0, lin_factor=w0)
# Display a slightly lower rating to incentivize participation.
# As times_ranked increases, new_rating converges to new_mean.
- new_rating = [max(1, round(m - (sqrt(get_var(t + 1)) - SD_LIM))) for m, t in zip(new_mean, times_ranked)]
+ new_rating = [
+ max(1, round(m - (sqrt(get_var(t + 1)) - SD_LIM)))
+ for m, t in zip(new_mean, times_ranked)
+ ]
return new_rating, new_mean, new_p
@@ -143,17 +148,39 @@ def divconq(i, j):
def rate_contest(contest):
from judge.models import Rating, Profile
- rating_subquery = Rating.objects.filter(user=OuterRef('user'))
- rating_sorted = rating_subquery.order_by('-contest__end_time')
- users = contest.users.order_by('is_disqualified', '-score', 'cumtime', 'tiebreaker') \
- .annotate(submissions=Count('submission'),
- last_rating=Coalesce(Subquery(rating_sorted.values('rating')[:1]), RATING_INIT),
- last_mean=Coalesce(Subquery(rating_sorted.values('mean')[:1]), MEAN_INIT),
- times=Coalesce(Subquery(rating_subquery.order_by().values('user_id')
- .annotate(count=Count('id')).values('count')), 0)) \
- .exclude(user_id__in=contest.rate_exclude.all()) \
- .filter(virtual=0).values('id', 'user_id', 'score', 'cumtime', 'tiebreaker',
- 'last_rating', 'last_mean', 'times')
+ rating_subquery = Rating.objects.filter(user=OuterRef("user"))
+ rating_sorted = rating_subquery.order_by("-contest__end_time")
+ users = (
+ contest.users.order_by("is_disqualified", "-score", "cumtime", "tiebreaker")
+ .annotate(
+ submissions=Count("submission"),
+ last_rating=Coalesce(
+ Subquery(rating_sorted.values("rating")[:1]), RATING_INIT
+ ),
+ last_mean=Coalesce(Subquery(rating_sorted.values("mean")[:1]), MEAN_INIT),
+ times=Coalesce(
+ Subquery(
+ rating_subquery.order_by()
+ .values("user_id")
+ .annotate(count=Count("id"))
+ .values("count")
+ ),
+ 0,
+ ),
+ )
+ .exclude(user_id__in=contest.rate_exclude.all())
+ .filter(virtual=0)
+ .values(
+ "id",
+ "user_id",
+ "score",
+ "cumtime",
+ "tiebreaker",
+ "last_rating",
+ "last_mean",
+ "times",
+ )
+ )
if not contest.rate_all:
users = users.filter(submissions__gt=0)
if contest.rating_floor is not None:
@@ -162,46 +189,75 @@ def rate_contest(contest):
users = users.exclude(last_rating__gt=contest.rating_ceiling)
users = list(users)
- participation_ids = list(map(itemgetter('id'), users))
- user_ids = list(map(itemgetter('user_id'), users))
- ranking = list(tie_ranker(users, key=itemgetter('score', 'cumtime', 'tiebreaker')))
- old_mean = list(map(itemgetter('last_mean'), users))
- times_ranked = list(map(itemgetter('times'), users))
+ participation_ids = list(map(itemgetter("id"), users))
+ user_ids = list(map(itemgetter("user_id"), users))
+ ranking = list(tie_ranker(users, key=itemgetter("score", "cumtime", "tiebreaker")))
+ old_mean = list(map(itemgetter("last_mean"), users))
+ times_ranked = list(map(itemgetter("times"), users))
historical_p = [[] for _ in users]
user_id_to_idx = {uid: i for i, uid in enumerate(user_ids)}
- for h in Rating.objects.filter(user_id__in=user_ids) \
- .order_by('-contest__end_time') \
- .values('user_id', 'performance'):
- idx = user_id_to_idx[h['user_id']]
- historical_p[idx].append(h['performance'])
-
- rating, mean, performance = recalculate_ratings(ranking, old_mean, times_ranked, historical_p)
+ for h in (
+ Rating.objects.filter(user_id__in=user_ids)
+ .order_by("-contest__end_time")
+ .values("user_id", "performance")
+ ):
+ idx = user_id_to_idx[h["user_id"]]
+ historical_p[idx].append(h["performance"])
+
+ rating, mean, performance = recalculate_ratings(
+ ranking, old_mean, times_ranked, historical_p
+ )
now = timezone.now()
- ratings = [Rating(user_id=i, contest=contest, rating=r, mean=m, performance=perf,
- last_rated=now, participation_id=pid, rank=z)
- for i, pid, r, m, perf, z in zip(user_ids, participation_ids, rating, mean, performance, ranking)]
+ ratings = [
+ Rating(
+ user_id=i,
+ contest=contest,
+ rating=r,
+ mean=m,
+ performance=perf,
+ last_rated=now,
+ participation_id=pid,
+ rank=z,
+ )
+ for i, pid, r, m, perf, z in zip(
+ user_ids, participation_ids, rating, mean, performance, ranking
+ )
+ ]
with transaction.atomic():
Rating.objects.bulk_create(ratings)
- Profile.objects.filter(contest_history__contest=contest, contest_history__virtual=0).update(
- rating=Subquery(Rating.objects.filter(user=OuterRef('id'))
- .order_by('-contest__end_time').values('rating')[:1]))
+ Profile.objects.filter(
+ contest_history__contest=contest, contest_history__virtual=0
+ ).update(
+ rating=Subquery(
+ Rating.objects.filter(user=OuterRef("id"))
+ .order_by("-contest__end_time")
+ .values("rating")[:1]
+ )
+ )
RATING_LEVELS = [
- gettext_lazy('Newbie'),
- gettext_lazy('Amateur'),
- gettext_lazy('Expert'),
- gettext_lazy('Candidate Master'),
- gettext_lazy('Master'),
- gettext_lazy('Grandmaster'),
- gettext_lazy('Target'),
+ gettext_lazy("Newbie"),
+ gettext_lazy("Amateur"),
+ gettext_lazy("Expert"),
+ gettext_lazy("Candidate Master"),
+ gettext_lazy("Master"),
+ gettext_lazy("Grandmaster"),
+ gettext_lazy("Target"),
]
RATING_VALUES = [1000, 1300, 1600, 1900, 2400, 3000]
-RATING_CLASS = ['rate-newbie', 'rate-amateur', 'rate-expert', 'rate-candidate-master',
- 'rate-master', 'rate-grandmaster', 'rate-target']
+RATING_CLASS = [
+ "rate-newbie",
+ "rate-amateur",
+ "rate-expert",
+ "rate-candidate-master",
+ "rate-master",
+ "rate-grandmaster",
+ "rate-target",
+]
def rating_level(rating):
diff --git a/judge/signals.py b/judge/signals.py
index ce4c5d4612..6199a75f0f 100644
--- a/judge/signals.py
+++ b/judge/signals.py
@@ -9,8 +9,22 @@
from django.dispatch import receiver
from .caching import finished_submission
-from .models import BlogPost, Comment, Contest, ContestSubmission, EFFECTIVE_MATH_ENGINES, Judge, Language, License, \
- MiscConfig, Organization, Problem, Profile, Submission, WebAuthnCredential
+from .models import (
+ BlogPost,
+ Comment,
+ Contest,
+ ContestSubmission,
+ EFFECTIVE_MATH_ENGINES,
+ Judge,
+ Language,
+ License,
+ MiscConfig,
+ Organization,
+ Problem,
+ Profile,
+ Submission,
+ WebAuthnCredential,
+)
def get_pdf_path(basename: str) -> Optional[str]:
@@ -30,35 +44,58 @@ def unlink_if_exists(file):
@receiver(post_save, sender=Problem)
def problem_update(sender, instance, **kwargs):
- if hasattr(instance, '_updating_stats_only'):
+ if hasattr(instance, "_updating_stats_only"):
return
- cache.delete_many([
- make_template_fragment_key('submission_problem', (instance.id,)),
- make_template_fragment_key('problem_feed', (instance.id,)),
- 'problem_tls:%s' % instance.id, 'problem_mls:%s' % instance.id,
- ])
- cache.delete_many([make_template_fragment_key('problem_html', (instance.id, engine, lang))
- for lang, _ in settings.LANGUAGES for engine in EFFECTIVE_MATH_ENGINES])
- cache.delete_many([make_template_fragment_key('problem_authors', (instance.id, lang))
- for lang, _ in settings.LANGUAGES])
- cache.delete_many(['generated-meta-problem:%s:%d' % (lang, instance.id) for lang, _ in settings.LANGUAGES])
+ cache.delete_many(
+ [
+ make_template_fragment_key("submission_problem", (instance.id,)),
+ make_template_fragment_key("problem_feed", (instance.id,)),
+ "problem_tls:%s" % instance.id,
+ "problem_mls:%s" % instance.id,
+ ]
+ )
+ cache.delete_many(
+ [
+ make_template_fragment_key("problem_html", (instance.id, engine, lang))
+ for lang, _ in settings.LANGUAGES
+ for engine in EFFECTIVE_MATH_ENGINES
+ ]
+ )
+ cache.delete_many(
+ [
+ make_template_fragment_key("problem_authors", (instance.id, lang))
+ for lang, _ in settings.LANGUAGES
+ ]
+ )
+ cache.delete_many(
+ [
+ "generated-meta-problem:%s:%d" % (lang, instance.id)
+ for lang, _ in settings.LANGUAGES
+ ]
+ )
for lang, _ in settings.LANGUAGES:
- cached_pdf_filename = get_pdf_path('%s.%s.pdf' % (instance.code, lang))
+ cached_pdf_filename = get_pdf_path("%s.%s.pdf" % (instance.code, lang))
if cached_pdf_filename is not None:
unlink_if_exists(cached_pdf_filename)
@receiver(post_save, sender=Profile)
def profile_update(sender, instance, **kwargs):
- if hasattr(instance, '_updating_stats_only'):
+ if hasattr(instance, "_updating_stats_only"):
return
- cache.delete_many([make_template_fragment_key('user_about', (instance.id, engine))
- for engine in EFFECTIVE_MATH_ENGINES] +
- [make_template_fragment_key('org_member_count', (org_id,))
- for org_id in instance.organizations.values_list('id', flat=True)])
+ cache.delete_many(
+ [
+ make_template_fragment_key("user_about", (instance.id, engine))
+ for engine in EFFECTIVE_MATH_ENGINES
+ ]
+ + [
+ make_template_fragment_key("org_member_count", (org_id,))
+ for org_id in instance.organizations.values_list("id", flat=True)
+ ]
+ )
@receiver(post_delete, sender=WebAuthnCredential)
@@ -66,49 +103,60 @@ def webauthn_delete(sender, instance, **kwargs):
profile = instance.user
if profile.webauthn_credentials.count() == 0:
profile.is_webauthn_enabled = False
- profile.save(update_fields=['is_webauthn_enabled'])
+ profile.save(update_fields=["is_webauthn_enabled"])
@receiver(post_save, sender=Contest)
def contest_update(sender, instance, **kwargs):
- if hasattr(instance, '_updating_stats_only'):
+ if hasattr(instance, "_updating_stats_only"):
return
- cache.delete_many(['generated-meta-contest:%d' % instance.id] +
- [make_template_fragment_key('contest_html', (instance.id, engine))
- for engine in EFFECTIVE_MATH_ENGINES])
+ cache.delete_many(
+ ["generated-meta-contest:%d" % instance.id]
+ + [
+ make_template_fragment_key("contest_html", (instance.id, engine))
+ for engine in EFFECTIVE_MATH_ENGINES
+ ]
+ )
@receiver(post_save, sender=License)
def license_update(sender, instance, **kwargs):
- cache.delete(make_template_fragment_key('license_html', (instance.id,)))
+ cache.delete(make_template_fragment_key("license_html", (instance.id,)))
@receiver(post_save, sender=Language)
def language_update(sender, instance, **kwargs):
- cache.delete_many([make_template_fragment_key('language_html', (instance.id,)),
- 'lang:cn_map'])
+ cache.delete_many(
+ [make_template_fragment_key("language_html", (instance.id,)), "lang:cn_map"]
+ )
@receiver(post_save, sender=Judge)
def judge_update(sender, instance, **kwargs):
- cache.delete(make_template_fragment_key('judge_html', (instance.id,)))
+ cache.delete(make_template_fragment_key("judge_html", (instance.id,)))
@receiver(post_save, sender=Comment)
def comment_update(sender, instance, **kwargs):
- cache.delete('comment_feed:%d' % instance.id)
+ cache.delete("comment_feed:%d" % instance.id)
@receiver(post_save, sender=BlogPost)
def post_update(sender, instance, **kwargs):
- cache.delete_many([
- make_template_fragment_key('post_summary', (instance.id,)),
- 'blog_slug:%d' % instance.id,
- 'blog_feed:%d' % instance.id,
- ])
- cache.delete_many([make_template_fragment_key('post_content', (instance.id, engine))
- for engine in EFFECTIVE_MATH_ENGINES])
+ cache.delete_many(
+ [
+ make_template_fragment_key("post_summary", (instance.id,)),
+ "blog_slug:%d" % instance.id,
+ "blog_feed:%d" % instance.id,
+ ]
+ )
+ cache.delete_many(
+ [
+ make_template_fragment_key("post_content", (instance.id, engine))
+ for engine in EFFECTIVE_MATH_ENGINES
+ ]
+ )
@receiver(post_delete, sender=Submission)
@@ -129,20 +177,26 @@ def contest_submission_delete(sender, instance, **kwargs):
@receiver(post_save, sender=Organization)
def organization_update(sender, instance, **kwargs):
- cache.delete_many([make_template_fragment_key('organization_html', (instance.id, engine))
- for engine in EFFECTIVE_MATH_ENGINES])
+ cache.delete_many(
+ [
+ make_template_fragment_key("organization_html", (instance.id, engine))
+ for engine in EFFECTIVE_MATH_ENGINES
+ ]
+ )
@receiver(post_save, sender=MiscConfig)
def misc_config_update(sender, instance, **kwargs):
- cache.delete('misc_config')
+ cache.delete("misc_config")
@receiver(post_delete, sender=MiscConfig)
def misc_config_delete(sender, instance, **kwargs):
- cache.delete('misc_config')
+ cache.delete("misc_config")
@receiver(post_save, sender=ContestSubmission)
def contest_submission_update(sender, instance, **kwargs):
- Submission.objects.filter(id=instance.submission_id).update(contest_object_id=instance.participation.contest_id)
+ Submission.objects.filter(id=instance.submission_id).update(
+ contest_object_id=instance.participation.contest_id
+ )
diff --git a/judge/sitemap.py b/judge/sitemap.py
index 5c3426ad29..13c0c6ab8e 100644
--- a/judge/sitemap.py
+++ b/judge/sitemap.py
@@ -7,79 +7,85 @@
class ProblemSitemap(Sitemap):
- changefreq = 'weekly'
+ changefreq = "weekly"
priority = 0.8
def items(self):
- return Problem.get_public_problems().values_list('code')
+ return Problem.get_public_problems().values_list("code")
def location(self, obj):
- return reverse('problem_detail', args=obj)
+ return reverse("problem_detail", args=obj)
class UserSitemap(Sitemap):
- changefreq = 'weekly'
+ changefreq = "weekly"
priority = 0.5
def items(self):
- return User.objects.values_list('username')
+ return User.objects.values_list("username")
def location(self, obj):
- return reverse('user_page', args=obj)
+ return reverse("user_page", args=obj)
class ContestSitemap(Sitemap):
- changefreq = 'hourly'
+ changefreq = "hourly"
priority = 0.7
def items(self):
- return Contest.objects.filter(is_visible=True, is_private=False,
- is_organization_private=False).values_list('key')
+ return Contest.objects.filter(
+ is_visible=True, is_private=False, is_organization_private=False
+ ).values_list("key")
def location(self, obj):
- return reverse('contest_view', args=obj)
+ return reverse("contest_view", args=obj)
class OrganizationSitemap(Sitemap):
- changefreq = 'weekly'
+ changefreq = "weekly"
priority = 0.5
def items(self):
- return Organization.objects.values_list('id', 'slug')
+ return Organization.objects.values_list("id", "slug")
def location(self, obj):
- return reverse('organization_home', args=obj)
+ return reverse("organization_home", args=obj)
class BlogPostSitemap(Sitemap):
- changefreq = 'hourly'
+ changefreq = "hourly"
priority = 0.7
def items(self):
- return BlogPost.objects.filter(visible=True, publish_on__lte=timezone.now()).values_list('id', 'slug')
+ return BlogPost.objects.filter(
+ visible=True, publish_on__lte=timezone.now()
+ ).values_list("id", "slug")
def location(self, obj):
- return reverse('blog_post', args=obj)
+ return reverse("blog_post", args=obj)
class SolutionSitemap(Sitemap):
- changefreq = 'weekly'
+ changefreq = "weekly"
priority = 0.8
def items(self):
- return (Solution.objects.filter(is_public=True, publish_on__lte=timezone.now(),
- problem__in=Problem.get_public_problems()).values_list('problem__code'))
+ return Solution.objects.filter(
+ is_public=True,
+ publish_on__lte=timezone.now(),
+ problem__in=Problem.get_public_problems(),
+ ).values_list("problem__code")
def location(self, obj):
- return reverse('problem_editorial', args=obj)
+ return reverse("problem_editorial", args=obj)
class HomePageSitemap(Sitemap):
priority = 1.0
- changefreq = 'hourly'
+ changefreq = "hourly"
def items(self):
- return ['home']
+ return ["home"]
def location(self, obj):
return reverse(obj)
@@ -93,24 +99,26 @@ def items(self):
return self.pages
def location(self, obj):
- return obj['location'] if isinstance(obj, dict) else obj
+ return obj["location"] if isinstance(obj, dict) else obj
def priority(self, obj):
- return obj.get('priority', 0.5) if isinstance(obj, dict) else 0.5
+ return obj.get("priority", 0.5) if isinstance(obj, dict) else 0.5
def changefreq(self, obj):
- return obj.get('changefreq', 'daily') if isinstance(obj, dict) else 'daily'
+ return obj.get("changefreq", "daily") if isinstance(obj, dict) else "daily"
sitemaps = {
- 'home': HomePageSitemap,
- 'pages': UrlSitemap([
- {'location': '/about/', 'priority': 0.9},
- ]),
- 'problem': ProblemSitemap,
- 'solutions': SolutionSitemap,
- 'blog': BlogPostSitemap,
- 'contest': ContestSitemap,
- 'organization': OrganizationSitemap,
- 'user': UserSitemap,
+ "home": HomePageSitemap,
+ "pages": UrlSitemap(
+ [
+ {"location": "/about/", "priority": 0.9},
+ ]
+ ),
+ "problem": ProblemSitemap,
+ "solutions": SolutionSitemap,
+ "blog": BlogPostSitemap,
+ "contest": ContestSitemap,
+ "organization": OrganizationSitemap,
+ "user": UserSitemap,
}
diff --git a/judge/social_auth.py b/judge/social_auth.py
index f8fcc2453e..8a704d82f2 100644
--- a/judge/social_auth.py
+++ b/judge/social_auth.py
@@ -13,53 +13,65 @@
from social_core.backends.github import GithubOAuth2
from social_core.exceptions import InvalidEmail, SocialAuthBaseException
from social_core.pipeline.partial import partial
-from social_django.middleware import SocialAuthExceptionMiddleware as OldSocialAuthExceptionMiddleware
+from social_django.middleware import (
+ SocialAuthExceptionMiddleware as OldSocialAuthExceptionMiddleware,
+)
from judge.forms import ProfileForm
from judge.models import Language, Profile
-logger = logging.getLogger('judge.social_auth')
+logger = logging.getLogger("judge.social_auth")
class GitHubSecureEmailOAuth2(GithubOAuth2):
- name = 'github-secure'
+ name = "github-secure"
def user_data(self, access_token, *args, **kwargs):
data = self._user_data(access_token)
try:
- emails = self._user_data(access_token, '/emails')
+ emails = self._user_data(access_token, "/emails")
except (HTTPError, ValueError, TypeError):
emails = []
- emails = [(e.get('email'), e.get('primary'), 0) for e in emails if isinstance(e, dict) and e.get('verified')]
+ emails = [
+ (e.get("email"), e.get("primary"), 0)
+ for e in emails
+ if isinstance(e, dict) and e.get("verified")
+ ]
emails.sort(key=itemgetter(1), reverse=True)
emails = list(map(itemgetter(0), emails))
if emails:
- data['email'] = emails[0]
+ data["email"] = emails[0]
else:
- data['email'] = None
+ data["email"] = None
return data
-def slugify_username(username, renotword=re.compile(r'[^\w]')):
- return renotword.sub('', username.replace('-', '_'))
+def slugify_username(username, renotword=re.compile(r"[^\w]")):
+ return renotword.sub("", username.replace("-", "_"))
def verify_email(backend, details, *args, **kwargs):
- if not details['email']:
+ if not details["email"]:
raise InvalidEmail(backend)
class UsernameForm(forms.Form):
- username = forms.RegexField(regex=r'^\w+$', max_length=30, label='Username',
- error_messages={'invalid': 'A username must contain letters, numbers, or underscores.'})
+ username = forms.RegexField(
+ regex=r"^\w+$",
+ max_length=30,
+ label="Username",
+ error_messages={
+ "invalid": "A username must contain letters, numbers, or underscores."
+ },
+ )
def clean_username(self):
- if User.objects.filter(username=self.cleaned_data['username']).exists():
- raise forms.ValidationError('Sorry, the username is taken.')
- return self.cleaned_data['username']
+ if User.objects.filter(username=self.cleaned_data["username"]).exists():
+ raise forms.ValidationError("Sorry, the username is taken.")
+ return self.cleaned_data["username"]
@partial
@@ -69,21 +81,26 @@ def choose_username(backend, user, username=None, *args, **kwargs):
if request.POST:
form = UsernameForm(request.POST)
if form.is_valid():
- return {'username': form.cleaned_data['username']}
+ return {"username": form.cleaned_data["username"]}
else:
- form = UsernameForm(initial={'username': username})
- return render(request, 'registration/username_select.html', {
- 'title': 'Choose a username', 'form': form,
- })
+ form = UsernameForm(initial={"username": username})
+ return render(
+ request,
+ "registration/username_select.html",
+ {
+ "title": "Choose a username",
+ "form": form,
+ },
+ )
@partial
def make_profile(backend, user, response, is_new=False, *args, **kwargs):
if is_new:
- if not hasattr(user, 'profile'):
+ if not hasattr(user, "profile"):
profile = Profile(user=user)
profile.language = Language.get_default_language()
- logger.info('Info from %s: %s', backend.name, response)
+ logger.info("Info from %s: %s", backend.name, response)
profile.save()
form = ProfileForm(instance=profile, user=user)
else:
@@ -94,15 +111,25 @@ def make_profile(backend, user, response, is_new=False, *args, **kwargs):
with revisions.create_revision(atomic=True):
form.save()
revisions.set_user(user)
- revisions.set_comment('Updated on registration')
+ revisions.set_comment("Updated on registration")
return
- return render(backend.strategy.request, 'registration/profile_creation.html', {
- 'title': 'Create your profile', 'form': form,
- })
+ return render(
+ backend.strategy.request,
+ "registration/profile_creation.html",
+ {
+ "title": "Create your profile",
+ "form": form,
+ },
+ )
class SocialAuthExceptionMiddleware(OldSocialAuthExceptionMiddleware):
def process_exception(self, request, exception):
if isinstance(exception, SocialAuthBaseException):
- return HttpResponseRedirect('%s?message=%s' % (reverse('social_auth_error'),
- quote(self.get_message(request, exception))))
+ return HttpResponseRedirect(
+ "%s?message=%s"
+ % (
+ reverse("social_auth_error"),
+ quote(self.get_message(request, exception)),
+ )
+ )
diff --git a/judge/tasks/contest.py b/judge/tasks/contest.py
index 0c36e23645..ca896a9e11 100644
--- a/judge/tasks/contest.py
+++ b/judge/tasks/contest.py
@@ -7,7 +7,7 @@
from judge.models import Contest, ContestMoss, ContestParticipation, Submission
from judge.utils.celery import Progress
-__all__ = ('rescore_contest', 'run_moss')
+__all__ = ("rescore_contest", "run_moss")
@shared_task(bind=True)
@@ -16,7 +16,9 @@ def rescore_contest(self, contest_key):
participations = contest.users
rescored = 0
- with Progress(self, participations.count(), stage=_('Recalculating contest scores')) as p:
+ with Progress(
+ self, participations.count(), stage=_("Recalculating contest scores")
+ ) as p:
for participation in participations.iterator():
participation.recompute_results()
rescored += 1
@@ -29,7 +31,7 @@ def rescore_contest(self, contest_key):
def run_moss(self, contest_key):
moss_api_key = settings.MOSS_API_KEY
if moss_api_key is None:
- raise ImproperlyConfigured('No MOSS API Key supplied')
+ raise ImproperlyConfigured("No MOSS API Key supplied")
contest = Contest.objects.get(key=contest_key)
ContestMoss.objects.filter(contest=contest).delete()
@@ -37,21 +39,34 @@ def run_moss(self, contest_key):
length = len(ContestMoss.LANG_MAPPING) * contest.problems.count()
moss_results = []
- with Progress(self, length, stage=_('Running MOSS')) as p:
+ with Progress(self, length, stage=_("Running MOSS")) as p:
for problem in contest.problems.all():
for dmoj_lang, moss_lang in ContestMoss.LANG_MAPPING:
- result = ContestMoss(contest=contest, problem=problem, language=dmoj_lang)
-
- subs = Submission.objects.filter(
- contest__participation__virtual__in=(ContestParticipation.LIVE, ContestParticipation.SPECTATE),
- contest_object=contest,
- problem=problem,
- language__common_name=dmoj_lang,
- ).order_by('-points').values_list('user__user__username', 'source__source')
+ result = ContestMoss(
+ contest=contest, problem=problem, language=dmoj_lang
+ )
+
+ subs = (
+ Submission.objects.filter(
+ contest__participation__virtual__in=(
+ ContestParticipation.LIVE,
+ ContestParticipation.SPECTATE,
+ ),
+ contest_object=contest,
+ problem=problem,
+ language__common_name=dmoj_lang,
+ )
+ .order_by("-points")
+ .values_list("user__user__username", "source__source")
+ )
if subs.exists():
- moss_call = MOSS(moss_api_key, language=moss_lang, matching_file_limit=100,
- comment='%s - %s' % (contest.key, problem.code))
+ moss_call = MOSS(
+ moss_api_key,
+ language=moss_lang,
+ matching_file_limit=100,
+ comment="%s - %s" % (contest.key, problem.code),
+ )
users = set()
@@ -59,7 +74,7 @@ def run_moss(self, contest_key):
if username in users:
continue
users.add(username)
- moss_call.add_file_from_memory(username, source.encode('utf-8'))
+ moss_call.add_file_from_memory(username, source.encode("utf-8"))
result.url = moss_call.process()
result.submission_count = len(users)
diff --git a/judge/tasks/demo.py b/judge/tasks/demo.py
index c09dcbf0c0..bd97401545 100644
--- a/judge/tasks/demo.py
+++ b/judge/tasks/demo.py
@@ -4,7 +4,7 @@
from judge.utils.celery import Progress
-__all__ = ('success', 'failure', 'progress')
+__all__ = ("success", "failure", "progress")
@shared_task
@@ -14,7 +14,7 @@ def success():
@shared_task
def failure():
- raise RuntimeError('This task always fails.')
+ raise RuntimeError("This task always fails.")
@shared_task(bind=True)
diff --git a/judge/tasks/submission.py b/judge/tasks/submission.py
index a190ba71ee..d48c3faf6d 100644
--- a/judge/tasks/submission.py
+++ b/judge/tasks/submission.py
@@ -7,7 +7,7 @@
from judge.models import Problem, Profile, Submission
from judge.utils.celery import Progress
-__all__ = ('apply_submission_filter', 'rejudge_problem_filter', 'rescore_problem')
+__all__ = ("apply_submission_filter", "rejudge_problem_filter", "rescore_problem")
def apply_submission_filter(queryset, id_range, languages, results):
@@ -18,13 +18,16 @@ def apply_submission_filter(queryset, id_range, languages, results):
queryset = queryset.filter(language_id__in=languages)
if results:
queryset = queryset.filter(result__in=results)
- queryset = queryset.exclude(locked_after__lt=timezone.now()) \
- .exclude(status__in=Submission.IN_PROGRESS_GRADING_STATUS)
+ queryset = queryset.exclude(locked_after__lt=timezone.now()).exclude(
+ status__in=Submission.IN_PROGRESS_GRADING_STATUS
+ )
return queryset
@shared_task(bind=True)
-def rejudge_problem_filter(self, problem_id, id_range=None, languages=None, results=None, user_id=None):
+def rejudge_problem_filter(
+ self, problem_id, id_range=None, languages=None, results=None, user_id=None
+):
queryset = Submission.objects.filter(problem_id=problem_id)
queryset = apply_submission_filter(queryset, id_range, languages, results)
user = User.objects.get(id=user_id)
@@ -44,27 +47,37 @@ def rescore_problem(self, problem_id):
problem = Problem.objects.get(id=problem_id)
submissions = Submission.objects.filter(problem_id=problem_id)
- with Progress(self, submissions.count(), stage=_('Modifying submissions')) as p:
+ with Progress(self, submissions.count(), stage=_("Modifying submissions")) as p:
rescored = 0
for submission in submissions.iterator():
- submission.points = round(submission.case_points / submission.case_total * problem.points
- if submission.case_total else 0, 1)
+ submission.points = round(
+ submission.case_points / submission.case_total * problem.points
+ if submission.case_total
+ else 0,
+ 1,
+ )
if not problem.partial and submission.points < problem.points:
submission.points = 0
- submission.save(update_fields=['points'])
+ submission.save(update_fields=["points"])
submission.update_contest()
rescored += 1
if rescored % 10 == 0:
p.done = rescored
- with Progress(self, submissions.values('user_id').distinct().count(), stage=_('Recalculating user points')) as p:
+ with Progress(
+ self,
+ submissions.values("user_id").distinct().count(),
+ stage=_("Recalculating user points"),
+ ) as p:
users = 0
- profiles = Profile.objects.filter(id__in=submissions.values_list('user_id', flat=True).distinct())
+ profiles = Profile.objects.filter(
+ id__in=submissions.values_list("user_id", flat=True).distinct()
+ )
for profile in profiles.iterator():
profile._updating_stats_only = True
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)
users += 1
if users % 10 == 0:
p.done = users
diff --git a/judge/tasks/user.py b/judge/tasks/user.py
index 4fa6304add..77dd6ab0f2 100644
--- a/judge/tasks/user.py
+++ b/judge/tasks/user.py
@@ -13,32 +13,34 @@
from judge.utils.raw_sql import use_straight_join
from judge.utils.unicode import utf8bytes
-__all__ = ('prepare_user_data',)
-rewildcard = re.compile(r'\*+')
+__all__ = ("prepare_user_data",)
+rewildcard = re.compile(r"\*+")
def apply_submission_filter(queryset, options):
- if not options['submission_download']:
+ if not options["submission_download"]:
return []
use_straight_join(queryset)
- if options['submission_results']:
- queryset = queryset.filter(result__in=options['submission_results'])
+ if options["submission_results"]:
+ queryset = queryset.filter(result__in=options["submission_results"])
# Compress wildcards to avoid exponential complexity on certain glob patterns before Python 3.9.
# For details, see .
- problem_glob = rewildcard.sub('*', options['submission_problem_glob'])
- if problem_glob != '*':
+ problem_glob = rewildcard.sub("*", options["submission_problem_glob"])
+ if problem_glob != "*":
queryset = queryset.filter(
- problem__in=Problem.objects.filter(code__regex=fnmatch.translate(problem_glob)),
+ problem__in=Problem.objects.filter(
+ code__regex=fnmatch.translate(problem_glob)
+ ),
)
return list(queryset)
def apply_comment_filter(queryset, options):
- if not options['comment_download']:
+ if not options["comment_download"]:
return []
return list(queryset)
@@ -46,39 +48,48 @@ def apply_comment_filter(queryset, options):
@shared_task(bind=True)
def prepare_user_data(self, profile_id, options):
options = json.loads(options)
- with Progress(self, 2, stage=_('Applying filters')) as p:
+ with Progress(self, 2, stage=_("Applying filters")) as p:
# Force an update so that we get a progress bar.
p.done = 0
submissions = apply_submission_filter(
- Submission.objects.select_related('problem', 'language', 'source').filter(user_id=profile_id),
+ Submission.objects.select_related("problem", "language", "source").filter(
+ user_id=profile_id
+ ),
options,
)
p.did(1)
- comments = apply_comment_filter(Comment.objects.filter(author_id=profile_id), options)
+ comments = apply_comment_filter(
+ Comment.objects.filter(author_id=profile_id), options
+ )
p.did(1)
- with zipfile.ZipFile(os.path.join(settings.DMOJ_USER_DATA_CACHE, '%s.zip' % profile_id), mode='w') as data_file:
+ with zipfile.ZipFile(
+ os.path.join(settings.DMOJ_USER_DATA_CACHE, "%s.zip" % profile_id), mode="w"
+ ) as data_file:
submission_count = len(submissions)
if submission_count:
submission_info = {}
- with Progress(self, submission_count, stage=_('Preparing your submission data')) as p:
+ with Progress(
+ self, submission_count, stage=_("Preparing your submission data")
+ ) as p:
prepared = 0
interval = max(submission_count // 10, 1)
for submission in submissions:
submission_info[submission.id] = {
- 'problem': submission.problem.code,
- 'date': submission.date.isoformat(),
- 'time': submission.time,
- 'memory': submission.memory,
- 'language': submission.language.key,
- 'status': submission.status,
- 'result': submission.result,
- 'case_points': submission.case_points,
- 'case_total': submission.case_total,
+ "problem": submission.problem.code,
+ "date": submission.date.isoformat(),
+ "time": submission.time,
+ "memory": submission.memory,
+ "language": submission.language.key,
+ "status": submission.status,
+ "result": submission.result,
+ "case_points": submission.case_points,
+ "case_total": submission.case_total,
}
with data_file.open(
- 'submissions/%s.%s' % (submission.id, submission.language.extension),
- 'w',
+ "submissions/%s.%s"
+ % (submission.id, submission.language.extension),
+ "w",
) as f:
f.write(utf8bytes(submission.source.source))
@@ -86,36 +97,42 @@ def prepare_user_data(self, profile_id, options):
if prepared % interval == 0:
p.done = prepared
- with data_file.open('submissions/info.json', 'w') as f:
- f.write(utf8bytes(json.dumps(submission_info, sort_keys=True, indent=4)))
+ with data_file.open("submissions/info.json", "w") as f:
+ f.write(
+ utf8bytes(json.dumps(submission_info, sort_keys=True, indent=4))
+ )
comment_count = len(comments)
if comment_count:
comment_info = {}
- with Progress(self, comment_count, stage=_('Preparing your comment data')) as p:
+ with Progress(
+ self, comment_count, stage=_("Preparing your comment data")
+ ) as p:
prepared = 0
interval = max(comment_count // 10, 1)
for comment in comments:
related_object = {
- 'b': 'blog post',
- 'c': 'contest',
- 'p': 'problem',
- 's': 'problem editorial',
+ "b": "blog post",
+ "c": "contest",
+ "p": "problem",
+ "s": "problem editorial",
}
comment_info[comment.id] = {
- 'date': comment.time.isoformat(),
- 'related_object': related_object[comment.page[0]],
- 'page': comment.page[2:],
- 'score': comment.score,
+ "date": comment.time.isoformat(),
+ "related_object": related_object[comment.page[0]],
+ "page": comment.page[2:],
+ "score": comment.score,
}
- with data_file.open('comments/%s.txt' % comment.id, 'w') as f:
+ with data_file.open("comments/%s.txt" % comment.id, "w") as f:
f.write(utf8bytes(comment.body))
prepared += 1
if prepared % interval == 0:
p.done = prepared
- with data_file.open('comments/info.json', 'w') as f:
- f.write(utf8bytes(json.dumps(comment_info, sort_keys=True, indent=4)))
+ with data_file.open("comments/info.json", "w") as f:
+ f.write(
+ utf8bytes(json.dumps(comment_info, sort_keys=True, indent=4))
+ )
return submission_count + comment_count
diff --git a/judge/template_context.py b/judge/template_context.py
index 196416377d..06af6ca43a 100644
--- a/judge/template_context.py
+++ b/judge/template_context.py
@@ -10,26 +10,26 @@
class FixedSimpleLazyObject(SimpleLazyObject):
- if not hasattr(SimpleLazyObject, '__iter__'):
+ if not hasattr(SimpleLazyObject, "__iter__"):
__iter__ = new_method_proxy(iter)
def get_resource(request):
use_https = settings.DMOJ_SSL
if use_https == 1:
- scheme = 'https' if request.is_secure() else 'http'
+ scheme = "https" if request.is_secure() else "http"
elif use_https > 1:
- scheme = 'https'
+ scheme = "https"
else:
- scheme = 'http'
+ scheme = "http"
return {
- 'INLINE_JQUERY': settings.INLINE_JQUERY,
- 'INLINE_FONTAWESOME': settings.INLINE_FONTAWESOME,
- 'JQUERY_JS': settings.JQUERY_JS,
- 'FONTAWESOME_CSS': settings.FONTAWESOME_CSS,
- 'DMOJ_SCHEME': scheme,
- 'DMOJ_CANONICAL': settings.DMOJ_CANONICAL,
- 'DMOJ_SELECT2_THEME': settings.DMOJ_SELECT2_THEME,
+ "INLINE_JQUERY": settings.INLINE_JQUERY,
+ "INLINE_FONTAWESOME": settings.INLINE_FONTAWESOME,
+ "JQUERY_JS": settings.JQUERY_JS,
+ "FONTAWESOME_CSS": settings.FONTAWESOME_CSS,
+ "DMOJ_SCHEME": scheme,
+ "DMOJ_CANONICAL": settings.DMOJ_CANONICAL,
+ "DMOJ_SELECT2_THEME": settings.DMOJ_SELECT2_THEME,
}
@@ -41,66 +41,75 @@ def get_profile(request):
def comet_location(request):
if request.is_secure():
- websocket = getattr(settings, 'EVENT_DAEMON_GET_SSL', settings.EVENT_DAEMON_GET)
- poll = getattr(settings, 'EVENT_DAEMON_POLL_SSL', settings.EVENT_DAEMON_POLL)
+ websocket = getattr(settings, "EVENT_DAEMON_GET_SSL", settings.EVENT_DAEMON_GET)
+ poll = getattr(settings, "EVENT_DAEMON_POLL_SSL", settings.EVENT_DAEMON_POLL)
else:
websocket = settings.EVENT_DAEMON_GET
poll = settings.EVENT_DAEMON_POLL
- return {'EVENT_DAEMON_LOCATION': websocket,
- 'EVENT_DAEMON_POLL_LOCATION': poll}
+ return {"EVENT_DAEMON_LOCATION": websocket, "EVENT_DAEMON_POLL_LOCATION": poll}
def __nav_tab(path):
- result = list(NavigationBar.objects.extra(where=['%s REGEXP BINARY regex'], params=[path])[:1])
- return result[0].get_ancestors(include_self=True).values_list('key', flat=True) if result else []
+ result = list(
+ NavigationBar.objects.extra(where=["%s REGEXP BINARY regex"], params=[path])[:1]
+ )
+ return (
+ result[0].get_ancestors(include_self=True).values_list("key", flat=True)
+ if result
+ else []
+ )
def general_info(request):
path = request.get_full_path()
return {
- 'nav_tab': FixedSimpleLazyObject(partial(__nav_tab, request.path)),
- 'nav_bar': NavigationBar.objects.all(),
- 'LOGIN_RETURN_PATH': '' if path.startswith('/accounts/') else path,
- 'perms': PermWrapper(request.user),
- 'HAS_WEBAUTHN': bool(settings.WEBAUTHN_RP_ID),
+ "nav_tab": FixedSimpleLazyObject(partial(__nav_tab, request.path)),
+ "nav_bar": NavigationBar.objects.all(),
+ "LOGIN_RETURN_PATH": "" if path.startswith("/accounts/") else path,
+ "perms": PermWrapper(request.user),
+ "HAS_WEBAUTHN": bool(settings.WEBAUTHN_RP_ID),
}
def site(request):
- return {'site': get_current_site(request)}
+ return {"site": get_current_site(request)}
def misc_config(request):
- return {'misc_config': request.misc_config}
+ return {"misc_config": request.misc_config}
def site_name(request):
- return {'SITE_NAME': settings.SITE_NAME,
- 'SITE_LONG_NAME': settings.SITE_LONG_NAME,
- 'SITE_ADMIN_EMAIL': settings.SITE_ADMIN_EMAIL}
+ return {
+ "SITE_NAME": settings.SITE_NAME,
+ "SITE_LONG_NAME": settings.SITE_LONG_NAME,
+ "SITE_ADMIN_EMAIL": settings.SITE_ADMIN_EMAIL,
+ }
def site_theme(request):
# Middleware populating `profile` may not have loaded at this point if we're called from an error context.
- if hasattr(request.user, 'profile'):
+ if hasattr(request.user, "profile"):
preferred_css = settings.DMOJ_THEME_CSS.get(request.profile.site_theme)
else:
preferred_css = None
return {
- 'DARK_STYLE_CSS': settings.DMOJ_THEME_CSS['dark'],
- 'LIGHT_STYLE_CSS': settings.DMOJ_THEME_CSS['light'],
- 'PREFERRED_STYLE_CSS': preferred_css,
+ "DARK_STYLE_CSS": settings.DMOJ_THEME_CSS["dark"],
+ "LIGHT_STYLE_CSS": settings.DMOJ_THEME_CSS["light"],
+ "PREFERRED_STYLE_CSS": preferred_css,
}
def math_setting(request):
- caniuse = CanIUse(request.META.get('HTTP_USER_AGENT', ''))
+ caniuse = CanIUse(request.META.get("HTTP_USER_AGENT", ""))
# Middleware populating `profile` may not have loaded at this point if we're called from an error context.
- if hasattr(request.user, 'profile'):
+ if hasattr(request.user, "profile"):
engine = request.profile.math_engine
else:
engine = settings.MATHOID_DEFAULT_TYPE
- if engine == 'auto':
- engine = 'mml' if bool(settings.MATHOID_URL) and caniuse.mathml == SUPPORT else 'jax'
- return {'MATH_ENGINE': engine, 'REQUIRE_JAX': engine == 'jax', 'caniuse': caniuse}
+ if engine == "auto":
+ engine = (
+ "mml" if bool(settings.MATHOID_URL) and caniuse.mathml == SUPPORT else "jax"
+ )
+ return {"MATH_ENGINE": engine, "REQUIRE_JAX": engine == "jax", "caniuse": caniuse}
diff --git a/judge/templatetags/dicts.py b/judge/templatetags/dicts.py
index 3afc15b7ac..efb321c194 100644
--- a/judge/templatetags/dicts.py
+++ b/judge/templatetags/dicts.py
@@ -3,6 +3,6 @@
register = template.Library()
-@register.filter(name='get_dict_item')
+@register.filter(name="get_dict_item")
def get_item(dictionary, key):
return dictionary.get(key)
diff --git a/judge/templatetags/list_processor.py b/judge/templatetags/list_processor.py
index dc371052a7..15109cf56b 100644
--- a/judge/templatetags/list_processor.py
+++ b/judge/templatetags/list_processor.py
@@ -5,7 +5,7 @@
register = template.Library()
-@register.filter(name='list_attr')
+@register.filter(name="list_attr")
def list_attr(iterable, prop):
result = []
for item in iterable:
@@ -15,43 +15,43 @@ def list_attr(iterable, prop):
try:
result.append(item[prop])
except KeyError:
- result.append('')
+ result.append("")
except TypeError:
try:
result.append(item[int(prop)])
except (IndexError, ValueError, TypeError):
- result.append('')
+ result.append("")
return result
-@register.filter(name='list_getitem')
+@register.filter(name="list_getitem")
def list_getitem(iterable, prop):
return list(map(itemgetter(prop), iterable))
-@register.filter(name='list_getindex')
+@register.filter(name="list_getindex")
def list_getindex(iterable, index):
return list(map(itemgetter(int(index)), iterable))
-@register.filter(name='list_getattr')
+@register.filter(name="list_getattr")
def list_getattr(iterable, prop):
return list(map(attrgetter(prop), iterable))
-@register.filter(name='sum_list')
+@register.filter(name="sum_list")
def sum_list(iterable):
return sum(iterable)
-@register.filter(name='max_list')
+@register.filter(name="max_list")
def max_list(iterable):
if not iterable:
return 0
return max(iterable)
-@register.filter(name='min_list')
+@register.filter(name="min_list")
def min_list(iterable):
if not iterable:
return 0
diff --git a/judge/templatetags/strings.py b/judge/templatetags/strings.py
index 6bd685af5a..475a615ddb 100644
--- a/judge/templatetags/strings.py
+++ b/judge/templatetags/strings.py
@@ -3,16 +3,16 @@
register = template.Library()
-@register.filter(name='split')
+@register.filter(name="split")
def split(value):
- return value.split('\n')
+ return value.split("\n")
-@register.filter(name='cutoff')
+@register.filter(name="cutoff")
def cutoff(value, length):
- return value[:int(length)]
+ return value[: int(length)]
-@register.filter(name='roundfloat')
+@register.filter(name="roundfloat")
def roundfloat(value, at):
return str(round(value, int(at)))
diff --git a/judge/user_log.py b/judge/user_log.py
index d5304ff1b8..91a4c67d63 100644
--- a/judge/user_log.py
+++ b/judge/user_log.py
@@ -10,12 +10,15 @@ def __init__(self, get_response=None):
def __call__(self, request):
response = self.get_response(request)
- if (hasattr(request, 'user') and request.user.is_authenticated and
- not getattr(request, 'no_profile_update', False)):
- updates = {'last_access': now()}
+ if (
+ hasattr(request, "user")
+ and request.user.is_authenticated
+ and not getattr(request, "no_profile_update", False)
+ ):
+ updates = {"last_access": now()}
# Decided on using REMOTE_ADDR as nginx will translate it to the external IP that hits it.
- if request.META.get('REMOTE_ADDR'):
- updates['ip'] = request.META.get('REMOTE_ADDR')
+ if request.META.get("REMOTE_ADDR"):
+ updates["ip"] = request.META.get("REMOTE_ADDR")
Profile.objects.filter(user_id=request.user.pk).update(**updates)
return response
diff --git a/judge/user_translations.py b/judge/user_translations.py
index 29a8a6b43b..f93deaae0d 100644
--- a/judge/user_translations.py
+++ b/judge/user_translations.py
@@ -9,23 +9,25 @@
def translation(language):
global _translations
if language not in _translations:
- _translations[language] = DjangoTranslation(language, domain='dmoj-user')
+ _translations[language] = DjangoTranslation(language, domain="dmoj-user")
return _translations[language]
def do_translate(message, translation_function):
"""Copied from django.utils.translation.trans_real"""
# str() is allowing a bytestring message to remain bytestring on Python 2
- eol_message = message.replace(str('\r\n'), str('\n')).replace(str('\r'), str('\n'))
+ eol_message = message.replace(str("\r\n"), str("\n")).replace(
+ str("\r"), str("\n")
+ )
if len(eol_message) == 0:
# Returns an empty value of the corresponding type if an empty message
# is given, instead of metadata, which is the default gettext behavior.
- result = ''
+ result = ""
else:
translation_object = translation(get_language())
result = getattr(translation_object, translation_function)(eol_message)
if not isinstance(result, str):
- result = result.decode('utf-8')
+ result = result.decode("utf-8")
if isinstance(message, SafeData):
return mark_safe(result)
@@ -33,7 +35,9 @@ def do_translate(message, translation_function):
return result
def gettext(message):
- return do_translate(message, 'gettext')
+ return do_translate(message, "gettext")
+
else:
+
def gettext(message):
return message
diff --git a/judge/utils/camo.py b/judge/utils/camo.py
index a3c90a591a..025963f398 100644
--- a/judge/utils/camo.py
+++ b/judge/utils/camo.py
@@ -10,39 +10,44 @@ class CamoClient(object):
"""Based on https://github.com/sionide21/camo-client"""
def __init__(self, server, key, excluded=(), https=False):
- self.server = server.rstrip('/')
+ self.server = server.rstrip("/")
self.key = key
self.https = https
self.excluded = excluded
def image_url(self, url):
- return '%s/%s/%s' % (self.server,
- hmac.new(utf8bytes(self.key), utf8bytes(url), sha1).hexdigest(),
- utf8bytes(url).hex())
+ return "%s/%s/%s" % (
+ self.server,
+ hmac.new(utf8bytes(self.key), utf8bytes(url), sha1).hexdigest(),
+ utf8bytes(url).hex(),
+ )
def rewrite_url(self, url):
if url.startswith(self.server) or url.startswith(self.excluded):
return url
- elif url.startswith(('http://', 'https://')):
+ elif url.startswith(("http://", "https://")):
return self.image_url(url)
- elif url.startswith('//'):
- return self.rewrite_url(('https:' if self.https else 'http:') + url)
+ elif url.startswith("//"):
+ return self.rewrite_url(("https:" if self.https else "http:") + url)
else:
return url
def update_tree(self, doc):
- for img in doc.xpath('.//img'):
- for attr in ('src', 'data-src'):
+ for img in doc.xpath(".//img"):
+ for attr in ("src", "data-src"):
if img.get(attr):
img.set(attr, self.rewrite_url(img.get(attr)))
- for obj in doc.xpath('.//object'):
- if obj.get('data'):
- obj.set('data', self.rewrite_url(obj.get('data')))
+ for obj in doc.xpath(".//object"):
+ if obj.get("data"):
+ obj.set("data", self.rewrite_url(obj.get("data")))
if settings.DMOJ_CAMO_URL and settings.DMOJ_CAMO_KEY:
- client = CamoClient(settings.DMOJ_CAMO_URL, key=settings.DMOJ_CAMO_KEY,
- excluded=settings.DMOJ_CAMO_EXCLUDE,
- https=settings.DMOJ_CAMO_HTTPS)
+ client = CamoClient(
+ settings.DMOJ_CAMO_URL,
+ key=settings.DMOJ_CAMO_KEY,
+ excluded=settings.DMOJ_CAMO_EXCLUDE,
+ https=settings.DMOJ_CAMO_HTTPS,
+ )
else:
client = None
diff --git a/judge/utils/caniuse.py b/judge/utils/caniuse.py
index ef77175bd6..4a3a8bacb1 100644
--- a/judge/utils/caniuse.py
+++ b/judge/utils/caniuse.py
@@ -3,16 +3,18 @@
from ua_parser import user_agent_parser
-with open(os.path.join(os.path.dirname(__file__), '..', '..', 'resources', 'caniuse.json')) as f:
- _SUPPORT_DATA = json.loads(f.read())['data']
+with open(
+ os.path.join(os.path.dirname(__file__), "..", "..", "resources", "caniuse.json")
+) as f:
+ _SUPPORT_DATA = json.loads(f.read())["data"]
-SUPPORT = 'y'
-PARTIAL_SUPPORT = 'a'
-UNSUPPORTED = 'n'
-POLYFILL = 'p'
-UNKNOWN = 'u'
-PREFIX = 'x'
-DISABLED = 'd'
+SUPPORT = "y"
+PARTIAL_SUPPORT = "a"
+UNSUPPORTED = "n"
+POLYFILL = "p"
+UNKNOWN = "u"
+PREFIX = "x"
+DISABLED = "d"
def safe_int(string):
@@ -31,19 +33,19 @@ def __init__(self, data):
max_support = UNKNOWN
for version, support in data.items():
- if version == 'all':
+ if version == "all":
self.max_support = support
- elif '-' in version:
- start, end = version.split('-')
- start = tuple(map(int, start.split('.')))
- end = tuple(map(int, end.split('.'))) + (1e3000,)
+ elif "-" in version:
+ start, end = version.split("-")
+ start = tuple(map(int, start.split(".")))
+ end = tuple(map(int, end.split("."))) + (1e3000,)
ranges.append((start, end, support))
if end > max_version:
max_version = end
max_support = support
else:
try:
- version = tuple(map(int, version.split('.')))
+ version = tuple(map(int, version.split(".")))
except ValueError:
pass
else:
@@ -62,7 +64,12 @@ def check(self, major, minor, patch):
if version > self.max_version:
return self.max_support
- for key in ((int_major, int_minor, int_patch), (int_major, int_minor), (int_major,), major):
+ for key in (
+ (int_major, int_minor, int_patch),
+ (int_major, int_minor),
+ (int_major,),
+ major,
+ ):
try:
return self._versions[key]
except KeyError:
@@ -78,7 +85,9 @@ def check(self, major, minor, patch):
class Feat(object):
def __init__(self, data):
self._data = data
- self._family = {name: BrowserFamily(data) for name, data in data['stats'].items()}
+ self._family = {
+ name: BrowserFamily(data) for name, data in data["stats"].items()
+ }
def __getitem__(self, item):
return self._family[item]
@@ -100,31 +109,31 @@ class CanIUse(object):
def __init__(self, ua):
self._agent = user_agent_parser.Parse(ua)
- os_family = self._agent['os']['family']
- browser_family = self._agent['user_agent']['family']
+ os_family = self._agent["os"]["family"]
+ browser_family = self._agent["user_agent"]["family"]
family = None
- if os_family == 'Android':
- if 'Firefox' in browser_family:
- family = 'and_ff'
- elif 'Chrome' in browser_family:
- family = 'and_chr'
- elif 'Android' in browser_family:
- family = 'android'
+ if os_family == "Android":
+ if "Firefox" in browser_family:
+ family = "and_ff"
+ elif "Chrome" in browser_family:
+ family = "and_chr"
+ elif "Android" in browser_family:
+ family = "android"
else:
- if 'Edge' in browser_family:
- family = 'edge'
- elif 'Firefox' in browser_family:
- family = 'firefox'
- elif 'Chrome' in browser_family:
- family = 'chrome'
- elif 'IE' in browser_family:
- family = 'ie'
- elif 'Opera' in browser_family:
- family = 'opera'
- elif 'Safari' in browser_family:
- family = 'safari'
+ if "Edge" in browser_family:
+ family = "edge"
+ elif "Firefox" in browser_family:
+ family = "firefox"
+ elif "Chrome" in browser_family:
+ family = "chrome"
+ elif "IE" in browser_family:
+ family = "ie"
+ elif "Opera" in browser_family:
+ family = "opera"
+ elif "Safari" in browser_family:
+ family = "safari"
self._family = family
@@ -137,12 +146,12 @@ def _check_feat(self, feat):
except KeyError:
return UNKNOWN
else:
- ua = self._agent['user_agent']
- return stats.check(ua['major'], ua['minor'], ua['patch'])[0]
+ ua = self._agent["user_agent"]
+ return stats.check(ua["major"], ua["minor"], ua["patch"])[0]
def __getattr__(self, attr):
try:
- feat = database[attr.replace('_', '-')]
+ feat = database[attr.replace("_", "-")]
except KeyError:
raise AttributeError(attr)
else:
diff --git a/judge/utils/celery.py b/judge/utils/celery.py
index 6bcc1e28cb..9429661972 100644
--- a/judge/utils/celery.py
+++ b/judge/utils/celery.py
@@ -13,11 +13,11 @@ def __init__(self, task, total, stage=None):
def _update_state(self):
self.task.update_state(
- state='PROGRESS',
+ state="PROGRESS",
meta={
- 'done': self._done,
- 'total': self._total,
- 'stage': self._stage,
+ "done": self._done,
+ "total": self._total,
+ "stage": self._stage,
},
)
@@ -55,12 +55,12 @@ def __exit__(self, exc_type, exc_val, exc_tb):
def task_status_url_by_id(result_id, message=None, redirect=None):
args = {}
if message:
- args['message'] = message
+ args["message"] = message
if redirect:
- args['redirect'] = redirect
- url = reverse('task_status', args=[result_id])
+ args["redirect"] = redirect
+ url = reverse("task_status", args=[result_id])
if args:
- url += '?' + urlencode(args)
+ url += "?" + urlencode(args)
return url
diff --git a/judge/utils/diggpaginator.py b/judge/utils/diggpaginator.py
index ba71aa572e..2ba8581a6e 100644
--- a/judge/utils/diggpaginator.py
+++ b/judge/utils/diggpaginator.py
@@ -4,10 +4,10 @@
from django.core.paginator import InvalidPage, Page, Paginator
__all__ = (
- 'InvalidPage',
- 'ExPaginator',
- 'DiggPaginator',
- 'QuerySetDiggPaginator',
+ "InvalidPage",
+ "ExPaginator",
+ "DiggPaginator",
+ "QuerySetDiggPaginator",
)
@@ -182,18 +182,20 @@ class DiggPaginator(ExPaginator):
"""
def __init__(self, *args, **kwargs):
- self.body = kwargs.pop('body', 10)
- self.tail = kwargs.pop('tail', 2)
- self.align_left = kwargs.pop('align_left', False)
- self.margin = kwargs.pop('margin', 4) # TODO: make the default relative to body?
+ self.body = kwargs.pop("body", 10)
+ self.tail = kwargs.pop("tail", 2)
+ self.align_left = kwargs.pop("align_left", False)
+ self.margin = kwargs.pop(
+ "margin", 4
+ ) # TODO: make the default relative to body?
# validate padding value
max_padding = int(math.ceil(self.body / 2.0) - 1)
- self.padding = kwargs.pop('padding', min(4, max_padding))
- count_override = kwargs.pop('count', None)
+ self.padding = kwargs.pop("padding", min(4, max_padding))
+ count_override = kwargs.pop("count", None)
if count_override is not None:
- self.__dict__['count'] = count_override
+ self.__dict__["count"] = count_override
if self.padding > max_padding:
- raise ValueError('padding too large for body (max %d)' % max_padding)
+ raise ValueError("padding too large for body (max %d)" % max_padding)
super(DiggPaginator, self).__init__(*args, **kwargs)
def page(self, number, *args, **kwargs):
@@ -205,13 +207,24 @@ def page(self, number, *args, **kwargs):
number = int(number) # we know this will work
# easier access
- num_pages, body, tail, padding, margin = \
- self.num_pages, self.body, self.tail, self.padding, self.margin
+ num_pages, body, tail, padding, margin = (
+ self.num_pages,
+ self.body,
+ self.tail,
+ self.padding,
+ self.margin,
+ )
# put active page in middle of main range
- main_range = list(map(int, [
- math.floor(number - body / 2.0) + 1, # +1 = shift odd body to right
- math.floor(number + body / 2.0)]))
+ main_range = list(
+ map(
+ int,
+ [
+ math.floor(number - body / 2.0) + 1, # +1 = shift odd body to right
+ math.floor(number + body / 2.0),
+ ],
+ )
+ )
# adjust bounds
if main_range[0] < 1:
main_range = list(map(abs(main_range[0] - 1).__add__, main_range))
@@ -252,7 +265,10 @@ def page(self, number, *args, **kwargs):
# section, again.
main_range = [1, num_pages]
else:
- main_range = [min(num_pages - body + 1, max(number - padding, main_range[0])), num_pages]
+ main_range = [
+ min(num_pages - body + 1, max(number - padding, main_range[0])),
+ num_pages,
+ ]
else:
trailing = list(range(num_pages - tail + 1, num_pages + 1))
@@ -266,8 +282,10 @@ def page(self, number, *args, **kwargs):
page.main_range = list(range(main_range[0], main_range[1] + 1))
page.leading_range = leading
page.trailing_range = trailing
- page.page_range = reduce(lambda x, y: x + ((x and y) and [False]) + y,
- [page.leading_range, page.main_range, page.trailing_range])
+ page.page_range = reduce(
+ lambda x, y: x + ((x and y) and [False]) + y,
+ [page.leading_range, page.main_range, page.trailing_range],
+ )
page.__class__ = DiggPage
return page
@@ -275,10 +293,16 @@ def page(self, number, *args, **kwargs):
class DiggPage(Page):
def __str__(self):
- return ' ... '.join(filter(None, [
- ' '.join(map(str, self.leading_range)),
- ' '.join(map(str, self.main_range)),
- ' '.join(map(str, self.trailing_range))]))
+ return " ... ".join(
+ filter(
+ None,
+ [
+ " ".join(map(str, self.leading_range)),
+ " ".join(map(str, self.main_range)),
+ " ".join(map(str, self.trailing_range)),
+ ],
+ )
+ )
@property
def num_pages(self):
@@ -287,7 +311,7 @@ def num_pages(self):
QuerySetDiggPaginator = DiggPaginator
-if __name__ == '__main__':
+if __name__ == "__main__":
import doctest
doctest.testmod()
diff --git a/judge/utils/file_cache.py b/judge/utils/file_cache.py
index 3864f593e5..3b19b4c2da 100644
--- a/judge/utils/file_cache.py
+++ b/judge/utils/file_cache.py
@@ -24,10 +24,10 @@ def get_path(self, hash, file):
return os.path.join(self.root, hash, file)
def get_url(self, hash, file):
- return urljoin(self.url, '%s/%s' % (hash, file))
+ return urljoin(self.url, "%s/%s" % (hash, file))
def read_file(self, hash, file):
- return open(self.get_path(hash, file), 'rb')
+ return open(self.get_path(hash, file), "rb")
def read_data(self, hash, file):
with self.read_file(hash, file) as f:
@@ -35,10 +35,10 @@ def read_data(self, hash, file):
def cache_data(self, hash, file, data, url=True, gzip=True):
if gzip and self.gzip:
- with gzip_open(self.get_path(hash, file + '.gz'), 'wb') as f:
+ with gzip_open(self.get_path(hash, file + ".gz"), "wb") as f:
f.write(data)
- with open(self.get_path(hash, file), 'wb') as f:
+ with open(self.get_path(hash, file), "wb") as f:
f.write(data)
if url:
diff --git a/judge/utils/infinite_paginator.py b/judge/utils/infinite_paginator.py
index 06e4f2198f..e31b5b6940 100644
--- a/judge/utils/infinite_paginator.py
+++ b/judge/utils/infinite_paginator.py
@@ -9,7 +9,9 @@
class InfinitePage(collections.abc.Sequence):
- def __init__(self, object_list, number, unfiltered_queryset, page_size, pad_pages, paginator):
+ def __init__(
+ self, object_list, number, unfiltered_queryset, page_size, pad_pages, paginator
+ ):
self.object_list = list(object_list)
self.number = number
self.unfiltered_queryset = unfiltered_queryset
@@ -19,7 +21,7 @@ def __init__(self, object_list, number, unfiltered_queryset, page_size, pad_page
self.paginator = paginator
def __repr__(self):
- return '' % self.number
+ return "" % self.number
def __len__(self):
return len(self.object_list)
@@ -31,8 +33,10 @@ def __getitem__(self, index):
def _after_up_to_pad(self):
first_after = self.number * self.page_size
padding_length = self.pad_pages * self.page_size
- queryset = self.unfiltered_queryset[first_after:first_after + padding_length + 1]
- c = getattr(queryset, 'count', None)
+ queryset = self.unfiltered_queryset[
+ first_after : first_after + padding_length + 1
+ ]
+ c = getattr(queryset, "count", None)
if callable(c) and not inspect.isbuiltin(c) and method_has_no_args(c):
return c()
return len(queryset)
@@ -65,7 +69,9 @@ def end_index(self):
@cached_property
def main_range(self):
start = max(1, self.number - self.pad_pages)
- end = self.number + min(int(ceil(self._after_up_to_pad / self.page_size)), self.pad_pages)
+ end = self.number + min(
+ int(ceil(self._after_up_to_pad / self.page_size)), self.pad_pages
+ )
return range(start, end + 1)
@cached_property
@@ -103,7 +109,7 @@ def __init__(self, per_page):
def infinite_paginate(queryset, page, page_size, pad_pages, paginator=None):
if page < 1:
raise EmptyPage()
- sliced = queryset[(page - 1) * page_size:page * page_size]
+ sliced = queryset[(page - 1) * page_size : page * page_size]
if page > 1 and not sliced:
raise EmptyPage()
return InfinitePage(sliced, page, queryset, page_size, pad_pages, paginator)
@@ -118,7 +124,9 @@ def use_infinite_pagination(self):
def paginate_queryset(self, queryset, page_size):
if not self.use_infinite_pagination:
- paginator, page, object_list, has_other = super().paginate_queryset(queryset, page_size)
+ paginator, page, object_list, has_other = super().paginate_queryset(
+ queryset, page_size
+ )
paginator.is_infinite = False
return paginator, page, object_list, has_other
@@ -127,13 +135,18 @@ def paginate_queryset(self, queryset, page_size):
try:
page_number = int(page)
except ValueError:
- raise Http404('Page cannot be converted to an int.')
+ raise Http404("Page cannot be converted to an int.")
try:
paginator = DummyPaginator(page_size)
- page = infinite_paginate(queryset, page_number, page_size, self.pad_pages, paginator)
+ page = infinite_paginate(
+ queryset, page_number, page_size, self.pad_pages, paginator
+ )
return paginator, page, page.object_list, page.has_other_pages()
except InvalidPage as e:
- raise Http404('Invalid page (%(page_number)s): %(message)s' % {
- 'page_number': page_number,
- 'message': str(e),
- })
+ raise Http404(
+ "Invalid page (%(page_number)s): %(message)s"
+ % {
+ "page_number": page_number,
+ "message": str(e),
+ }
+ )
diff --git a/judge/utils/mail.py b/judge/utils/mail.py
index a7ab3f37aa..650dfbed9f 100644
--- a/judge/utils/mail.py
+++ b/judge/utils/mail.py
@@ -8,15 +8,23 @@
from django.utils.translation import gettext
-bad_mail_regex: List[Pattern[str]] = list(map(re.compile, settings.BAD_MAIL_PROVIDER_REGEX))
+bad_mail_regex: List[Pattern[str]] = list(
+ map(re.compile, settings.BAD_MAIL_PROVIDER_REGEX)
+)
def validate_email_domain(email: str) -> None:
- if '@' in email:
- domain = email.split('@')[-1].lower()
- if domain in settings.BAD_MAIL_PROVIDERS or any(regex.match(domain) for regex in bad_mail_regex):
- raise ValidationError(gettext('Your email provider is not allowed due to history of abuse. '
- 'Please use a reputable email provider.'))
+ if "@" in email:
+ domain = email.split("@")[-1].lower()
+ if domain in settings.BAD_MAIL_PROVIDERS or any(
+ regex.match(domain) for regex in bad_mail_regex
+ ):
+ raise ValidationError(
+ gettext(
+ "Your email provider is not allowed due to history of abuse. "
+ "Please use a reputable email provider."
+ )
+ )
# Inspired by django.contrib.auth.forms.PasswordResetForm.send_mail
@@ -31,12 +39,12 @@ def send_mail(
) -> None:
subject = loader.render_to_string(subject_template_name, context)
# Email subject *must not* contain newlines
- subject = ''.join(subject.splitlines())
+ subject = "".join(subject.splitlines())
body = loader.render_to_string(email_template_name, context)
email_message = EmailMultiAlternatives(subject, body, from_email, [to_email])
if html_email_template_name is not None:
html_email = loader.render_to_string(html_email_template_name, context)
- email_message.attach_alternative(html_email, 'text/html')
+ email_message.attach_alternative(html_email, "text/html")
email_message.send()
diff --git a/judge/utils/mathoid.py b/judge/utils/mathoid.py
index ae45500568..3fa46caeb4 100644
--- a/judge/utils/mathoid.py
+++ b/judge/utils/mathoid.py
@@ -11,25 +11,25 @@
from judge.utils.file_cache import HashFileCache
from judge.utils.unicode import utf8bytes, utf8text
-logger = logging.getLogger('judge.mathoid')
-reescape = re.compile(r'(?'),
- ('&', '&'),
- ('−', '-'),
- ('≤', r'\le'),
- ('≥', r'\ge'),
- ('…', '...'),
- (r'\lt', '<'),
- (r'\gt', '>'),
+ ("\u2264", r"\le"),
+ ("\u2265", r"\ge"),
+ ("\u2026", "..."),
+ ("\u2212", "-"),
+ ("≤", r"\le"),
+ ("≥", r"\ge"),
+ ("<", "<"),
+ (">", ">"),
+ ("&", "&"),
+ ("−", "-"),
+ ("≤", r"\le"),
+ ("≥", r"\ge"),
+ ("…", "..."),
+ (r"\lt", "<"),
+ (r"\gt", ">"),
]
@@ -40,15 +40,17 @@ def format_math(math):
class MathoidMathParser(object):
- types = ('svg', 'mml', 'tex', 'jax')
+ types = ("svg", "mml", "tex", "jax")
def __init__(self, type):
self.type = type
self.mathoid_url = settings.MATHOID_URL
- self.cache = HashFileCache(settings.MATHOID_CACHE_ROOT,
- settings.MATHOID_CACHE_URL,
- settings.MATHOID_GZIP)
+ self.cache = HashFileCache(
+ settings.MATHOID_CACHE_ROOT,
+ settings.MATHOID_CACHE_URL,
+ settings.MATHOID_GZIP,
+ )
mml_cache = settings.MATHOID_MML_CACHE
self.mml_cache = mml_cache and caches[mml_cache]
@@ -60,66 +62,76 @@ def query_mathoid(self, formula, hash):
self.cache.create(hash)
try:
- response = requests.post(self.mathoid_url, data={
- 'q': reescape.sub(lambda m: '\\' + m.group(0), formula).encode('utf-8'),
- 'type': 'tex' if formula.startswith(r'\displaystyle') else 'inline-tex',
- })
+ response = requests.post(
+ self.mathoid_url,
+ data={
+ "q": reescape.sub(lambda m: "\\" + m.group(0), formula).encode(
+ "utf-8"
+ ),
+ "type": "tex"
+ if formula.startswith(r"\displaystyle")
+ else "inline-tex",
+ },
+ )
response.raise_for_status()
data = response.json()
except requests.ConnectionError:
- logger.exception('Failed to connect to mathoid for: %s', formula)
+ logger.exception("Failed to connect to mathoid for: %s", formula)
return
except requests.HTTPError as e:
- logger.error('Mathoid failed to render: %s\n%s', formula, e.response.text)
+ logger.error("Mathoid failed to render: %s\n%s", formula, e.response.text)
return
except Exception:
- logger.exception('Failed to connect to mathoid for: %s', formula)
+ logger.exception("Failed to connect to mathoid for: %s", formula)
return
- if not data['success']:
- logger.error('Mathoid failure for: %s\n%s', formula, data)
+ if not data["success"]:
+ logger.error("Mathoid failure for: %s\n%s", formula, data)
return
- if any(i not in data for i in ('mml', 'svg', 'mathoidStyle')):
- logger.error('Mathoid did not return required information (mml, svg, mathoidStyle needed):\n%s', data)
+ if any(i not in data for i in ("mml", "svg", "mathoidStyle")):
+ logger.error(
+ "Mathoid did not return required information (mml, svg, mathoidStyle needed):\n%s",
+ data,
+ )
return
- css = data['mathoidStyle']
- mml = data['mml']
+ css = data["mathoidStyle"]
+ mml = data["mml"]
result = {
- 'css': css,
- 'mml': mml,
- 'svg': self.cache.cache_data(hash, 'svg', data['svg'].encode('utf-8')),
+ "css": css,
+ "mml": mml,
+ "svg": self.cache.cache_data(hash, "svg", data["svg"].encode("utf-8")),
}
- self.cache.cache_data(hash, 'mml', mml.encode('utf-8'), url=False, gzip=False)
- self.cache.cache_data(hash, 'css', css.encode('utf-8'), url=False, gzip=False)
+ self.cache.cache_data(hash, "mml", mml.encode("utf-8"), url=False, gzip=False)
+ self.cache.cache_data(hash, "css", css.encode("utf-8"), url=False, gzip=False)
return result
def query_cache(self, hash):
- result = {'svg': self.cache.get_url(hash, 'svg')}
+ result = {"svg": self.cache.get_url(hash, "svg")}
- key = 'mathoid:css:' + hash
- css = result['css'] = self.css_cache.get(key)
+ key = "mathoid:css:" + hash
+ css = result["css"] = self.css_cache.get(key)
if css is None:
- css = result['css'] = self.cache.read_data(hash, 'css').decode('utf-8')
+ css = result["css"] = self.cache.read_data(hash, "css").decode("utf-8")
self.css_cache.set(key, css, self.mml_cache_ttl)
mml = None
if self.mml_cache:
- mml = result['mml'] = self.mml_cache.get('mathoid:mml:' + hash)
+ mml = result["mml"] = self.mml_cache.get("mathoid:mml:" + hash)
if mml is None:
- mml = result['mml'] = self.cache.read_data(hash, 'mml').decode('utf-8')
+ mml = result["mml"] = self.cache.read_data(hash, "mml").decode("utf-8")
if self.mml_cache:
- self.mml_cache.set('mathoid:mml:' + hash, mml, self.mml_cache_ttl)
+ self.mml_cache.set("mathoid:mml:" + hash, mml, self.mml_cache_ttl)
return result
def get_result(self, formula):
- if self.type == 'tex':
+ if self.type == "tex":
return
hash = hashlib.sha1(utf8bytes(formula)).hexdigest()
formula = utf8text(formula)
- if self.cache.has_file(hash, 'css'):
+ if self.cache.has_file(hash, "css"):
result = self.query_cache(hash)
else:
result = self.query_mathoid(formula, hash)
@@ -127,35 +139,44 @@ def get_result(self, formula):
if not result:
return None
- result['tex'] = formula
- result['display'] = formula.startswith(r'\displaystyle')
+ result["tex"] = formula
+ result["display"] = formula.startswith(r"\displaystyle")
return {
- 'mml': self.output_mml,
- 'jax': self.output_jax,
- 'svg': self.output_svg,
- 'raw': lambda x: x,
+ "mml": self.output_mml,
+ "jax": self.output_jax,
+ "svg": self.output_svg,
+ "raw": lambda x: x,
}[self.type](result)
def output_mml(self, result):
- return result['mml']
+ return result["mml"]
def output_jax(self, result):
- return format_html(''
- ' '
- '{4}{2}{4} '
- ' ',
- result['svg'], result['css'], result['tex'],
- ['inline-math', 'display-math'][result['display']], ['~', '$$'][result['display']])
+ return format_html(
+ ''
+ ' '
+ '{4}{2}{4} '
+ " ",
+ result["svg"],
+ result["css"],
+ result["tex"],
+ ["inline-math", "display-math"][result["display"]],
+ ["~", "$$"][result["display"]],
+ )
def output_svg(self, result):
- return format_html(' ',
- result['svg'], result['css'], result['tex'],
- ['inline-math', 'display-math'][result['display']])
+ return format_html(
+ ' ',
+ result["svg"],
+ result["css"],
+ result["tex"],
+ ["inline-math", "display-math"][result["display"]],
+ )
def display_math(self, math):
math = format_math(math)
- return self.get_result(r'\displaystyle ' + math) or r'\[%s\]' % escape(math)
+ return self.get_result(r"\displaystyle " + math) or r"\[%s\]" % escape(math)
def inline_math(self, math):
math = format_math(math)
- return self.get_result(math) or r'\(%s\)' % escape(math)
+ return self.get_result(math) or r"\(%s\)" % escape(math)
diff --git a/judge/utils/opengraph.py b/judge/utils/opengraph.py
index 7d409b7be7..94208fb4ff 100644
--- a/judge/utils/opengraph.py
+++ b/judge/utils/opengraph.py
@@ -10,15 +10,15 @@ def generate_opengraph(cache_key, data, style):
if metadata is None:
description = None
tree = reference(markdown(data, style)).tree
- for p in tree.iterfind('..//p'):
+ for p in tree.iterfind("..//p"):
text = p.text_content().strip()
if text:
description = text
break
if description:
- for remove in (r'\[', r'\]', r'\(', r'\)'):
- description = description.replace(remove, '')
- img = tree.xpath('.//img')
- metadata = truncatewords(description, 60), img[0].get('src') if img else None
+ for remove in (r"\[", r"\]", r"\(", r"\)"):
+ description = description.replace(remove, "")
+ img = tree.xpath(".//img")
+ metadata = truncatewords(description, 60), img[0].get("src") if img else None
cache.set(cache_key, metadata, 86400)
return metadata
diff --git a/judge/utils/pdfoid.py b/judge/utils/pdfoid.py
index 9fd6fc3c0d..27d447e31e 100644
--- a/judge/utils/pdfoid.py
+++ b/judge/utils/pdfoid.py
@@ -5,7 +5,7 @@
from django.conf import settings
from django.utils.translation import gettext
-logger = logging.getLogger('judge.problem.pdf')
+logger = logging.getLogger("judge.problem.pdf")
PDFOID_URL = settings.DMOJ_PDF_PDFOID_URL
@@ -18,27 +18,28 @@ def render_pdf(*, title: str, html: str, footer: bool = False) -> bytes:
if footer:
footer_template = (
- '' +
- gettext('Page {page_number} of {total_pages}') +
- ' ')
+ ''
+ + gettext("Page {page_number} of {total_pages}")
+ + " "
+ )
else:
footer_template = None
response = requests.post(
PDFOID_URL,
data={
- 'html': html,
- 'title': title,
- 'footer-template': footer_template,
- 'wait-for-class': 'math-loaded',
- 'wait-for-duration-secs': 15,
+ "html": html,
+ "title": title,
+ "footer-template": footer_template,
+ "wait-for-class": "math-loaded",
+ "wait-for-duration-secs": 15,
},
)
response.raise_for_status()
data = response.json()
- if not data['success']:
- raise RuntimeError(data['error'])
+ if not data["success"]:
+ raise RuntimeError(data["error"])
- return base64.b64decode(data['pdf'])
+ return base64.b64decode(data["pdf"])
diff --git a/judge/utils/problem_data.py b/judge/utils/problem_data.py
index ce59d29adb..9d75089d59 100644
--- a/judge/utils/problem_data.py
+++ b/judge/utils/problem_data.py
@@ -10,9 +10,14 @@
from django.utils.translation import gettext as _
if os.altsep:
- def split_path_first(path, repath=re.compile('[%s]' % re.escape(os.sep + os.altsep))):
+
+ def split_path_first(
+ path, repath=re.compile("[%s]" % re.escape(os.sep + os.altsep))
+ ):
return repath.split(path, 1)
+
else:
+
def split_path_first(path):
return path.split(os.sep, 1)
@@ -24,8 +29,8 @@ def __init__(self):
def url(self, name):
path = split_path_first(name)
if len(path) != 2:
- raise ValueError('This file is not accessible via a URL.')
- return reverse('problem_data_file', args=path)
+ raise ValueError("This file is not accessible via a URL.")
+ return reverse("problem_data_file", args=path)
def _save(self, name, content):
if self.exists(name):
@@ -59,87 +64,97 @@ def make_init(self):
batch = None
def end_batch():
- if not batch['batched']:
- raise ProblemDataError(_('Empty batches not allowed.'))
+ if not batch["batched"]:
+ raise ProblemDataError(_("Empty batches not allowed."))
cases.append(batch)
def make_checker(case):
if case.checker_args:
return {
- 'name': case.checker,
- 'args': json.loads(case.checker_args),
+ "name": case.checker,
+ "args": json.loads(case.checker_args),
}
return case.checker
for i, case in enumerate(self.cases, 1):
- if case.type == 'C':
+ if case.type == "C":
data = {}
if batch:
case.points = None
- case.is_pretest = batch['is_pretest']
+ case.is_pretest = batch["is_pretest"]
else:
if case.points is None:
- raise ProblemDataError(_('Points must be defined for non-batch case #%d.') % i)
- data['is_pretest'] = case.is_pretest
+ raise ProblemDataError(
+ _("Points must be defined for non-batch case #%d.") % i
+ )
+ data["is_pretest"] = case.is_pretest
if not self.generator:
if case.input_file not in self.files:
- raise ProblemDataError(_('Input file for case %d does not exist: %s') %
- (i, case.input_file))
+ raise ProblemDataError(
+ _("Input file for case %d does not exist: %s")
+ % (i, case.input_file)
+ )
if case.output_file not in self.files:
- raise ProblemDataError(_('Output file for case %d does not exist: %s') %
- (i, case.output_file))
+ raise ProblemDataError(
+ _("Output file for case %d does not exist: %s")
+ % (i, case.output_file)
+ )
if case.input_file:
- data['in'] = case.input_file
+ data["in"] = case.input_file
if case.output_file:
- data['out'] = case.output_file
+ data["out"] = case.output_file
if case.points is not None:
- data['points'] = case.points
+ data["points"] = case.points
if case.generator_args:
- data['generator_args'] = case.generator_args.splitlines()
+ data["generator_args"] = case.generator_args.splitlines()
if case.output_limit is not None:
- data['output_limit_length'] = case.output_limit
+ data["output_limit_length"] = case.output_limit
if case.output_prefix is not None:
- data['output_prefix_length'] = case.output_prefix
+ data["output_prefix_length"] = case.output_prefix
if case.checker:
- data['checker'] = make_checker(case)
+ data["checker"] = make_checker(case)
else:
- case.checker_args = ''
- case.save(update_fields=('checker_args', 'is_pretest'))
- (batch['batched'] if batch else cases).append(data)
- elif case.type == 'S':
+ case.checker_args = ""
+ case.save(update_fields=("checker_args", "is_pretest"))
+ (batch["batched"] if batch else cases).append(data)
+ elif case.type == "S":
if batch:
end_batch()
if case.points is None:
- raise ProblemDataError(_('Batch start case #%d requires points.') % i)
+ raise ProblemDataError(
+ _("Batch start case #%d requires points.") % i
+ )
batch = {
- 'points': case.points,
- 'batched': [],
- 'is_pretest': case.is_pretest,
+ "points": case.points,
+ "batched": [],
+ "is_pretest": case.is_pretest,
}
if case.generator_args:
- batch['generator_args'] = case.generator_args.splitlines()
+ batch["generator_args"] = case.generator_args.splitlines()
if case.output_limit is not None:
- batch['output_limit_length'] = case.output_limit
+ batch["output_limit_length"] = case.output_limit
if case.output_prefix is not None:
- batch['output_prefix_length'] = case.output_prefix
+ batch["output_prefix_length"] = case.output_prefix
if case.checker:
- batch['checker'] = make_checker(case)
+ batch["checker"] = make_checker(case)
else:
- case.checker_args = ''
- case.input_file = ''
- case.output_file = ''
- case.save(update_fields=('checker_args', 'input_file', 'output_file'))
- elif case.type == 'E':
+ case.checker_args = ""
+ case.input_file = ""
+ case.output_file = ""
+ case.save(update_fields=("checker_args", "input_file", "output_file"))
+ elif case.type == "E":
if not batch:
- raise ProblemDataError(_('Attempt to end batch outside of one in case #%d.') % i)
- case.is_pretest = batch['is_pretest']
- case.input_file = ''
- case.output_file = ''
- case.generator_args = ''
- case.checker = ''
- case.checker_args = ''
+ raise ProblemDataError(
+ _("Attempt to end batch outside of one in case #%d.") % i
+ )
+ case.is_pretest = batch["is_pretest"]
+ case.input_file = ""
+ case.output_file = ""
+ case.generator_args = ""
+ case.checker = ""
+ case.checker_args = ""
case.save()
end_batch()
batch = None
@@ -151,53 +166,53 @@ def make_checker(case):
if self.data.zipfile:
zippath = split_path_first(self.data.zipfile.name)
if len(zippath) != 2:
- raise ProblemDataError(_('How did you corrupt the zip path?'))
- init['archive'] = zippath[1]
+ raise ProblemDataError(_("How did you corrupt the zip path?"))
+ init["archive"] = zippath[1]
if self.generator:
generator_path = split_path_first(self.generator.name)
if len(generator_path) != 2:
- raise ProblemDataError(_('How did you corrupt the generator path?'))
- init['generator'] = generator_path[1]
+ raise ProblemDataError(_("How did you corrupt the generator path?"))
+ init["generator"] = generator_path[1]
pretest_test_cases = []
test_cases = []
hints = []
for case in cases:
- if case['is_pretest']:
+ if case["is_pretest"]:
pretest_test_cases.append(case)
else:
test_cases.append(case)
- del case['is_pretest']
+ del case["is_pretest"]
if pretest_test_cases:
- init['pretest_test_cases'] = pretest_test_cases
+ init["pretest_test_cases"] = pretest_test_cases
if test_cases:
- init['test_cases'] = test_cases
+ init["test_cases"] = test_cases
if self.data.output_limit is not None:
- init['output_limit_length'] = self.data.output_limit
+ init["output_limit_length"] = self.data.output_limit
if self.data.output_prefix is not None:
- init['output_prefix_length'] = self.data.output_prefix
+ init["output_prefix_length"] = self.data.output_prefix
if self.data.unicode:
- hints.append('unicode')
+ hints.append("unicode")
if self.data.nobigmath:
- hints.append('nobigmath')
+ hints.append("nobigmath")
if self.data.checker:
- init['checker'] = make_checker(self.data)
+ init["checker"] = make_checker(self.data)
else:
- self.data.checker_args = ''
+ self.data.checker_args = ""
if hints:
- init['hints'] = hints
+ init["hints"] = hints
return init
def compile(self):
from judge.models import problem_data_storage
- yml_file = '%s/init.yml' % self.problem.code
+ yml_file = "%s/init.yml" % self.problem.code
try:
init = self.make_init()
if init:
@@ -207,7 +222,7 @@ def compile(self):
self.data.save()
problem_data_storage.delete(yml_file)
else:
- self.data.feedback = ''
+ self.data.feedback = ""
self.data.save()
if init:
problem_data_storage.save(yml_file, ContentFile(init))
diff --git a/judge/utils/problems.py b/judge/utils/problems.py
index 44e7af4576..f9e933d02c 100644
--- a/judge/utils/problems.py
+++ b/judge/utils/problems.py
@@ -9,68 +9,106 @@
from judge.models import Problem, Submission
-__all__ = ['contest_completed_ids', 'get_result_data', 'user_completed_ids', 'user_editable_ids', 'user_tester_ids']
+__all__ = [
+ "contest_completed_ids",
+ "get_result_data",
+ "user_completed_ids",
+ "user_editable_ids",
+ "user_tester_ids",
+]
def user_tester_ids(profile):
- return set(Problem.testers.through.objects.filter(profile=profile).values_list('problem_id', flat=True))
+ return set(
+ Problem.testers.through.objects.filter(profile=profile).values_list(
+ "problem_id", flat=True
+ )
+ )
def user_editable_ids(profile):
- return set(Problem.get_editable_problems(profile.user).values_list('id', flat=True))
+ return set(Problem.get_editable_problems(profile.user).values_list("id", flat=True))
def contest_completed_ids(participation):
- key = 'contest_complete:%d' % participation.id
+ key = "contest_complete:%d" % participation.id
result = cache.get(key)
if result is None:
- result = set(participation.submissions.filter(submission__result='AC', points__gte=F('problem__points'))
- .values_list('problem__problem_id', flat=True).distinct())
+ result = set(
+ participation.submissions.filter(
+ submission__result="AC", points__gte=F("problem__points")
+ )
+ .values_list("problem__problem_id", flat=True)
+ .distinct()
+ )
cache.set(key, result, 86400)
return result
def user_completed_ids(profile):
- key = 'user_complete:%d' % profile.id
+ key = "user_complete:%d" % profile.id
result = cache.get(key)
if result is None:
- result = set(Submission.objects.filter(user=profile, result='AC', case_points__gte=F('case_total'))
- .values_list('problem_id', flat=True).distinct())
+ result = set(
+ Submission.objects.filter(
+ user=profile, result="AC", case_points__gte=F("case_total")
+ )
+ .values_list("problem_id", flat=True)
+ .distinct()
+ )
cache.set(key, result, 86400)
return result
def contest_attempted_ids(participation):
- key = 'contest_attempted:%s' % participation.id
+ key = "contest_attempted:%s" % participation.id
result = cache.get(key)
if result is None:
- result = set(participation.submissions.values_list('problem__problem_id', flat=True).distinct())
+ result = set(
+ participation.submissions.values_list(
+ "problem__problem_id", flat=True
+ ).distinct()
+ )
cache.set(key, result, 86400)
return result
def user_attempted_ids(profile):
- key = 'user_attempted:%s' % profile.id
+ key = "user_attempted:%s" % profile.id
result = cache.get(key)
if result is None:
- result = set(profile.submission_set.values_list('problem_id', flat=True).distinct())
+ result = set(
+ profile.submission_set.values_list("problem_id", flat=True).distinct()
+ )
cache.set(key, result, 86400)
return result
def _get_result_data(results):
return {
- 'categories': [
+ "categories": [
# Using gettext_noop here since this will be tacked into the cache, so it must be language neutral.
# The caller, SubmissionList.get_result_data will run gettext on the name.
- {'code': 'AC', 'name': gettext_noop('Accepted'), 'count': results['AC']},
- {'code': 'WA', 'name': gettext_noop('Wrong'), 'count': results['WA']},
- {'code': 'CE', 'name': gettext_noop('Compile Error'), 'count': results['CE']},
- {'code': 'TLE', 'name': gettext_noop('Timeout'), 'count': results['TLE']},
- {'code': 'ERR', 'name': gettext_noop('Error'),
- 'count': results['MLE'] + results['OLE'] + results['IR'] + results['RTE'] + results['AB'] + results['IE']},
+ {"code": "AC", "name": gettext_noop("Accepted"), "count": results["AC"]},
+ {"code": "WA", "name": gettext_noop("Wrong"), "count": results["WA"]},
+ {
+ "code": "CE",
+ "name": gettext_noop("Compile Error"),
+ "count": results["CE"],
+ },
+ {"code": "TLE", "name": gettext_noop("Timeout"), "count": results["TLE"]},
+ {
+ "code": "ERR",
+ "name": gettext_noop("Error"),
+ "count": results["MLE"]
+ + results["OLE"]
+ + results["IR"]
+ + results["RTE"]
+ + results["AB"]
+ + results["IE"],
+ },
],
- 'total': sum(results.values()),
+ "total": sum(results.values()),
}
@@ -80,45 +118,78 @@ def get_result_data(*args, **kwargs):
if kwargs:
raise ValueError("Can't pass both queryset and keyword filters")
else:
- submissions = Submission.objects.filter(**kwargs) if kwargs is not None else Submission.objects
- raw = submissions.values('result').annotate(count=Count('result')).values_list('result', 'count')
+ submissions = (
+ Submission.objects.filter(**kwargs)
+ if kwargs is not None
+ else Submission.objects
+ )
+ raw = (
+ submissions.values("result")
+ .annotate(count=Count("result"))
+ .values_list("result", "count")
+ )
return _get_result_data(defaultdict(int, raw))
def hot_problems(duration, limit):
- cache_key = 'hot_problems:%d:%d' % (duration.total_seconds(), limit)
+ cache_key = "hot_problems:%d:%d" % (duration.total_seconds(), limit)
qs = cache.get(cache_key)
if qs is None:
- qs = Problem.get_public_problems() \
- .filter(submission__date__gt=timezone.now() - duration, points__gt=3, points__lt=25)
- qs0 = qs.annotate(k=Count('submission__user', distinct=True)).order_by('-k').values_list('k', flat=True)
+ qs = Problem.get_public_problems().filter(
+ submission__date__gt=timezone.now() - duration, points__gt=3, points__lt=25
+ )
+ qs0 = (
+ qs.annotate(k=Count("submission__user", distinct=True))
+ .order_by("-k")
+ .values_list("k", flat=True)
+ )
if not qs0:
return []
# make this an aggregate
mx = float(qs0[0])
- qs = qs.annotate(unique_user_count=Count('submission__user', distinct=True))
+ qs = qs.annotate(unique_user_count=Count("submission__user", distinct=True))
# fix braindamage in excluding CE
- qs = qs.annotate(submission_volume=Count(Case(
- When(submission__result='AC', then=1),
- When(submission__result='WA', then=1),
- When(submission__result='IR', then=1),
- When(submission__result='RTE', then=1),
- When(submission__result='TLE', then=1),
- When(submission__result='OLE', then=1),
- output_field=FloatField(),
- )))
- qs = qs.annotate(ac_volume=Count(Case(
- When(submission__result='AC', then=1),
- output_field=FloatField(),
- )))
+ qs = qs.annotate(
+ submission_volume=Count(
+ Case(
+ When(submission__result="AC", then=1),
+ When(submission__result="WA", then=1),
+ When(submission__result="IR", then=1),
+ When(submission__result="RTE", then=1),
+ When(submission__result="TLE", then=1),
+ When(submission__result="OLE", then=1),
+ output_field=FloatField(),
+ )
+ )
+ )
+ qs = qs.annotate(
+ ac_volume=Count(
+ Case(
+ When(submission__result="AC", then=1),
+ output_field=FloatField(),
+ )
+ )
+ )
qs = qs.filter(unique_user_count__gt=max(mx / 3.0, 1))
- qs = qs.annotate(ordering=ExpressionWrapper(
- 0.5 * F('points') * (0.4 * F('ac_volume') / F('submission_volume') + 0.6 * F('ac_rate')) +
- 100 * e ** (F('unique_user_count') / mx), output_field=FloatField(),
- )).order_by('-ordering').defer('description')[:limit]
+ qs = (
+ qs.annotate(
+ ordering=ExpressionWrapper(
+ 0.5
+ * F("points")
+ * (
+ 0.4 * F("ac_volume") / F("submission_volume")
+ + 0.6 * F("ac_rate")
+ )
+ + 100 * e ** (F("unique_user_count") / mx),
+ output_field=FloatField(),
+ )
+ )
+ .order_by("-ordering")
+ .defer("description")[:limit]
+ )
cache.set(cache_key, qs, 900)
return qs
diff --git a/judge/utils/pwned.py b/judge/utils/pwned.py
index bab3562233..de6168c112 100644
--- a/judge/utils/pwned.py
+++ b/judge/utils/pwned.py
@@ -46,7 +46,7 @@
log = logging.getLogger(__name__)
-API_ENDPOINT = 'https://api.pwnedpasswords.com/range/{}'
+API_ENDPOINT = "https://api.pwnedpasswords.com/range/{}"
REQUEST_TIMEOUT = 2.0 # 2 seconds
@@ -60,20 +60,20 @@ def _get_pwned(prefix):
url=API_ENDPOINT.format(prefix),
timeout=getattr(
settings,
- 'PWNED_PASSWORDS_API_TIMEOUT',
+ "PWNED_PASSWORDS_API_TIMEOUT",
REQUEST_TIMEOUT,
),
)
response.raise_for_status()
except requests.RequestException:
# Gracefully handle timeouts and HTTP error response codes.
- log.warning('Skipped Pwned Passwords check due to error', exc_info=True)
+ log.warning("Skipped Pwned Passwords check due to error", exc_info=True)
return None
results = {}
for line in response.text.splitlines():
- line_suffix, _, times = line.partition(':')
- results[line_suffix] = int(times.replace(',', ''))
+ line_suffix, _, times = line.partition(":")
+ results[line_suffix] = int(times.replace(",", ""))
return results
@@ -83,7 +83,7 @@ def pwned_password(password):
Checks a password against the Pwned Passwords database.
"""
if not isinstance(password, str):
- raise TypeError('Password values to check must be strings.')
+ raise TypeError("Password values to check must be strings.")
password_hash = hashlib.sha1(utf8bytes(password)).hexdigest().upper()
prefix, suffix = password_hash[:5], password_hash[5:]
results = _get_pwned(prefix)
@@ -106,7 +106,7 @@ def validate(self, password, user=None):
# the same database.
CommonPasswordValidator().validate(password, user)
elif amount:
- raise ValidationError(_('This password is too common.'))
+ raise ValidationError(_("This password is too common."))
def get_help_text(self):
return _("Your password can't be a commonly used password.")
diff --git a/judge/utils/ranker.py b/judge/utils/ranker.py
index 5fcac555cf..3f15e62f83 100644
--- a/judge/utils/ranker.py
+++ b/judge/utils/ranker.py
@@ -1,7 +1,7 @@
from operator import attrgetter
-def ranker(iterable, key=attrgetter('points'), rank=0):
+def ranker(iterable, key=attrgetter("points"), rank=0):
delta = 1
last = None
for item in iterable:
diff --git a/judge/utils/raw_sql.py b/judge/utils/raw_sql.py
index bbf7235bd8..736314d547 100644
--- a/judge/utils/raw_sql.py
+++ b/judge/utils/raw_sql.py
@@ -6,13 +6,30 @@
class RawSQLJoin(Join):
- def __init__(self, subquery, subquery_params, parent_alias, table_alias, join_type, join_field, nullable,
- filtered_relation=None):
+ def __init__(
+ self,
+ subquery,
+ subquery_params,
+ parent_alias,
+ table_alias,
+ join_type,
+ join_field,
+ nullable,
+ filtered_relation=None,
+ ):
self.subquery_params = subquery_params
- super().__init__(subquery, parent_alias, table_alias, join_type, join_field, nullable, filtered_relation)
+ super().__init__(
+ subquery,
+ parent_alias,
+ table_alias,
+ join_type,
+ join_field,
+ nullable,
+ filtered_relation,
+ )
def as_sql(self, compiler, connection):
- compiler.quote_cache[self.table_name] = '(%s)' % self.table_name
+ compiler.quote_cache[self.table_name] = "(%s)" % self.table_name
sql, params = super().as_sql(compiler, connection)
return sql, self.subquery_params + params
@@ -30,7 +47,15 @@ def get_extra_restriction(self, where_class, alias, remote_alias):
def join_sql_subquery(
- queryset, subquery, params, join_fields, alias, related_model, join_type=INNER, parent_model=None):
+ queryset,
+ subquery,
+ params,
+ join_fields,
+ alias,
+ related_model,
+ join_type=INNER,
+ parent_model=None,
+):
if parent_model is not None:
parent_alias = parent_model._meta.db_table
else:
@@ -39,8 +64,15 @@ def join_sql_subquery(
queryset.query.external_aliases[alias] = True
else:
queryset.query.external_aliases.add(alias)
- join = RawSQLJoin(subquery, params, parent_alias, alias, join_type, FakeJoinField(join_fields, related_model),
- join_type == LOUTER)
+ join = RawSQLJoin(
+ subquery,
+ params,
+ parent_alias,
+ alias,
+ join_type,
+ FakeJoinField(join_fields, related_model),
+ join_type == LOUTER,
+ )
queryset.query.join(join)
join.table_alias = alias
@@ -51,7 +83,7 @@ def join(self, join, *args, **kwargs):
alias = super().join(join, *args, **kwargs)
join = self.alias_map[alias]
if join.join_type == INNER:
- join.join_type = 'STRAIGHT_JOIN'
+ join.join_type = "STRAIGHT_JOIN"
return alias
return Query
@@ -61,7 +93,7 @@ def join(self, join, *args, **kwargs):
def use_straight_join(queryset):
- if connections[queryset.db].vendor != 'mysql':
+ if connections[queryset.db].vendor != "mysql":
return
try:
cloner = queryset.query.chain
diff --git a/judge/utils/recaptcha.py b/judge/utils/recaptcha.py
index 331a12d0b7..9be7e5f3d8 100644
--- a/judge/utils/recaptcha.py
+++ b/judge/utils/recaptcha.py
@@ -6,6 +6,7 @@
ReCaptchaWidget = None
else:
from django.conf import settings
- if not hasattr(settings, 'RECAPTCHA_PRIVATE_KEY'):
+
+ if not hasattr(settings, "RECAPTCHA_PRIVATE_KEY"):
ReCaptchaField = None
ReCaptchaWidget = None
diff --git a/judge/utils/safe_translations.py b/judge/utils/safe_translations.py
index 9d95149d78..551f697c5e 100644
--- a/judge/utils/safe_translations.py
+++ b/judge/utils/safe_translations.py
@@ -12,7 +12,15 @@ def wrapper(*args, **kwargs):
return wrapper
- for func in ['gettext', 'gettext_lazy', 'gettext_noop', 'ngettext', 'ngettext_lazy', 'pgettext', 'pgettext_lazy']:
+ for func in [
+ "gettext",
+ "gettext_lazy",
+ "gettext_noop",
+ "ngettext",
+ "ngettext_lazy",
+ "pgettext",
+ "pgettext_lazy",
+ ]:
globals()[func] = wrap(getattr(translation, func))
diff --git a/judge/utils/stats.py b/judge/utils/stats.py
index 450a61afce..6842aabbcc 100644
--- a/judge/utils/stats.py
+++ b/judge/utils/stats.py
@@ -1,29 +1,53 @@
from operator import itemgetter
-__all__ = ('chart_colors', 'highlight_colors', 'get_pie_chart', 'get_bar_chart')
+__all__ = ("chart_colors", "highlight_colors", "get_pie_chart", "get_bar_chart")
-BASE_COLORS = [0x3366CC, 0xDC3912, 0xFF9900, 0x109618, 0x990099, 0x3B3EAC, 0x0099C6, 0xDD4477, 0x66AA00, 0xB82E2E,
- 0x316395, 0x994499, 0x22AA99, 0xAAAA11, 0x6633CC, 0xE67300, 0x8B0707, 0x329262, 0x5574A6, 0x3B3EAC]
+BASE_COLORS = [
+ 0x3366CC,
+ 0xDC3912,
+ 0xFF9900,
+ 0x109618,
+ 0x990099,
+ 0x3B3EAC,
+ 0x0099C6,
+ 0xDD4477,
+ 0x66AA00,
+ 0xB82E2E,
+ 0x316395,
+ 0x994499,
+ 0x22AA99,
+ 0xAAAA11,
+ 0x6633CC,
+ 0xE67300,
+ 0x8B0707,
+ 0x329262,
+ 0x5574A6,
+ 0x3B3EAC,
+]
def _to_highlight_color(color):
r, g, b = color >> 16, (color >> 8) & 0xFF, color & 0xFF
- return '#%02X%02X%02X' % (min(int(r * 1.2), 255), min(int(g * 1.2), 255), min(int(b * 1.2), 255))
+ return "#%02X%02X%02X" % (
+ min(int(r * 1.2), 255),
+ min(int(g * 1.2), 255),
+ min(int(b * 1.2), 255),
+ )
-chart_colors = list(map('#%06X'.__mod__, BASE_COLORS))
+chart_colors = list(map("#%06X".__mod__, BASE_COLORS))
highlight_colors = list(map(_to_highlight_color, BASE_COLORS))
def get_pie_chart(data):
return {
- 'labels': list(map(itemgetter(0), data)),
- 'datasets': [
+ "labels": list(map(itemgetter(0), data)),
+ "datasets": [
{
- 'backgroundColor': chart_colors,
- 'highlightBackgroundColor': highlight_colors,
- 'data': list(map(itemgetter(1), data)),
+ "backgroundColor": chart_colors,
+ "highlightBackgroundColor": highlight_colors,
+ "data": list(map(itemgetter(1), data)),
},
],
}
@@ -31,15 +55,19 @@ def get_pie_chart(data):
def get_bar_chart(data, **kwargs):
return {
- 'labels': list(map(itemgetter(0), data)),
- 'datasets': [
+ "labels": list(map(itemgetter(0), data)),
+ "datasets": [
{
- 'backgroundColor': kwargs.get('fillColor', 'rgba(151,187,205,0.5)'),
- 'borderColor': kwargs.get('strokeColor', 'rgba(151,187,205,0.8)'),
- 'borderWidth': 1,
- 'hoverBackgroundColor': kwargs.get('highlightFill', 'rgba(151,187,205,0.75)'),
- 'hoverBorderColor': kwargs.get('highlightStroke', 'rgba(151,187,205,1)'),
- 'data': list(map(itemgetter(1), data)),
+ "backgroundColor": kwargs.get("fillColor", "rgba(151,187,205,0.5)"),
+ "borderColor": kwargs.get("strokeColor", "rgba(151,187,205,0.8)"),
+ "borderWidth": 1,
+ "hoverBackgroundColor": kwargs.get(
+ "highlightFill", "rgba(151,187,205,0.75)"
+ ),
+ "hoverBorderColor": kwargs.get(
+ "highlightStroke", "rgba(151,187,205,1)"
+ ),
+ "data": list(map(itemgetter(1), data)),
},
],
}
diff --git a/judge/utils/subscription.py b/judge/utils/subscription.py
index 1129b0d47c..883d905d16 100644
--- a/judge/utils/subscription.py
+++ b/judge/utils/subscription.py
@@ -1,8 +1,10 @@
from django.conf import settings
-if 'newsletter' in settings.INSTALLED_APPS:
+if "newsletter" in settings.INSTALLED_APPS:
from newsletter.models import Subscription
else:
Subscription = None
-newsletter_id = None if Subscription is None else settings.DMOJ_NEWSLETTER_ID_ON_REGISTER
+newsletter_id = (
+ None if Subscription is None else settings.DMOJ_NEWSLETTER_ID_ON_REGISTER
+)
diff --git a/judge/utils/tests/test_infinite_paginator.py b/judge/utils/tests/test_infinite_paginator.py
index a177ba5fa6..08ac5bb4f0 100644
--- a/judge/utils/tests/test_infinite_paginator.py
+++ b/judge/utils/tests/test_infinite_paginator.py
@@ -5,11 +5,19 @@
class InfinitePaginatorTestCase(SimpleTestCase):
def test_first_page(self):
- self.assertEqual(infinite_paginate(range(1, 101), 1, 10, 2).object_list, list(range(1, 11)))
+ self.assertEqual(
+ infinite_paginate(range(1, 101), 1, 10, 2).object_list, list(range(1, 11))
+ )
- self.assertEqual(infinite_paginate(range(1, 101), 1, 10, 2).page_range, [1, 2, 3, False])
- self.assertEqual(infinite_paginate(range(1, 31), 1, 10, 2).page_range, [1, 2, 3])
- self.assertEqual(infinite_paginate(range(1, 22), 1, 10, 2).page_range, [1, 2, 3])
+ self.assertEqual(
+ infinite_paginate(range(1, 101), 1, 10, 2).page_range, [1, 2, 3, False]
+ )
+ self.assertEqual(
+ infinite_paginate(range(1, 31), 1, 10, 2).page_range, [1, 2, 3]
+ )
+ self.assertEqual(
+ infinite_paginate(range(1, 22), 1, 10, 2).page_range, [1, 2, 3]
+ )
self.assertEqual(infinite_paginate(range(1, 21), 1, 10, 2).page_range, [1, 2])
self.assertEqual(infinite_paginate(range(1, 12), 1, 10, 2).page_range, [1, 2])
self.assertEqual(infinite_paginate(range(1, 11), 1, 10, 2).page_range, [1])
@@ -17,16 +25,47 @@ def test_first_page(self):
self.assertEqual(infinite_paginate([], 1, 10, 2).page_range, [1])
def test_gaps(self):
- self.assertEqual(infinite_paginate(range(1, 101), 1, 10, 2).page_range, [1, 2, 3, False])
- self.assertEqual(infinite_paginate(range(1, 101), 2, 10, 2).page_range, [1, 2, 3, 4, False])
- self.assertEqual(infinite_paginate(range(1, 101), 3, 10, 2).page_range, [1, 2, 3, 4, 5, False])
- self.assertEqual(infinite_paginate(range(1, 101), 5, 10, 2).page_range, [1, 2, 3, 4, 5, 6, 7, False])
- self.assertEqual(infinite_paginate(range(1, 101), 6, 10, 2).page_range, [1, 2, False, 4, 5, 6, 7, 8, False])
+ self.assertEqual(
+ infinite_paginate(range(1, 101), 1, 10, 2).page_range, [1, 2, 3, False]
+ )
+ self.assertEqual(
+ infinite_paginate(range(1, 101), 2, 10, 2).page_range, [1, 2, 3, 4, False]
+ )
+ self.assertEqual(
+ infinite_paginate(range(1, 101), 3, 10, 2).page_range,
+ [1, 2, 3, 4, 5, False],
+ )
+ self.assertEqual(
+ infinite_paginate(range(1, 101), 5, 10, 2).page_range,
+ [1, 2, 3, 4, 5, 6, 7, False],
+ )
+ self.assertEqual(
+ infinite_paginate(range(1, 101), 6, 10, 2).page_range,
+ [1, 2, False, 4, 5, 6, 7, 8, False],
+ )
def test_end(self):
- self.assertEqual(infinite_paginate(range(1, 101), 7, 10, 2).page_range, [1, 2, False, 5, 6, 7, 8, 9, False])
- self.assertEqual(infinite_paginate(range(1, 101), 8, 10, 2).page_range, [1, 2, False, 6, 7, 8, 9, 10])
- self.assertEqual(infinite_paginate(range(1, 101), 9, 10, 2).page_range, [1, 2, False, 7, 8, 9, 10])
- self.assertEqual(infinite_paginate(range(1, 101), 10, 10, 2).page_range, [1, 2, False, 8, 9, 10])
- self.assertEqual(infinite_paginate(range(1, 100), 10, 10, 2).page_range, [1, 2, False, 8, 9, 10])
- self.assertEqual(infinite_paginate(range(1, 100), 10, 10, 2).object_list, list(range(91, 100)))
+ self.assertEqual(
+ infinite_paginate(range(1, 101), 7, 10, 2).page_range,
+ [1, 2, False, 5, 6, 7, 8, 9, False],
+ )
+ self.assertEqual(
+ infinite_paginate(range(1, 101), 8, 10, 2).page_range,
+ [1, 2, False, 6, 7, 8, 9, 10],
+ )
+ self.assertEqual(
+ infinite_paginate(range(1, 101), 9, 10, 2).page_range,
+ [1, 2, False, 7, 8, 9, 10],
+ )
+ self.assertEqual(
+ infinite_paginate(range(1, 101), 10, 10, 2).page_range,
+ [1, 2, False, 8, 9, 10],
+ )
+ self.assertEqual(
+ infinite_paginate(range(1, 100), 10, 10, 2).page_range,
+ [1, 2, False, 8, 9, 10],
+ )
+ self.assertEqual(
+ infinite_paginate(range(1, 100), 10, 10, 2).object_list,
+ list(range(91, 100)),
+ )
diff --git a/judge/utils/tests/test_iterator.py b/judge/utils/tests/test_iterator.py
index 30153616be..82ba050147 100644
--- a/judge/utils/tests/test_iterator.py
+++ b/judge/utils/tests/test_iterator.py
@@ -10,7 +10,7 @@ def test_empty(self):
def test_normal(self):
for size in [10, 13, 100, 200]:
- with self.subTest(f'chunk size {size}'):
+ with self.subTest(f"chunk size {size}"):
result = list(chunk(range(100), size))
self.assertEqual(list(chain(*result)), list(range(100)))
for part in result[:-1]:
diff --git a/judge/utils/texoid.py b/judge/utils/texoid.py
index 1f57dd0198..bd3fb9570b 100644
--- a/judge/utils/texoid.py
+++ b/judge/utils/texoid.py
@@ -10,16 +10,16 @@
from judge.utils.file_cache import HashFileCache
from judge.utils.unicode import utf8bytes
-logger = logging.getLogger('judge.texoid')
+logger = logging.getLogger("judge.texoid")
-TEXOID_ENABLED = hasattr(settings, 'TEXOID_URL')
+TEXOID_ENABLED = hasattr(settings, "TEXOID_URL")
class TexoidRenderer(object):
def __init__(self):
- self.cache = HashFileCache(settings.TEXOID_CACHE_ROOT,
- settings.TEXOID_CACHE_URL,
- settings.TEXOID_GZIP)
+ self.cache = HashFileCache(
+ settings.TEXOID_CACHE_ROOT, settings.TEXOID_CACHE_URL, settings.TEXOID_GZIP
+ )
self.meta_cache = caches[settings.TEXOID_META_CACHE]
self.meta_cache_ttl = settings.TEXOID_META_CACHE_TTL
@@ -27,59 +27,69 @@ def query_texoid(self, document, hash):
self.cache.create(hash)
try:
- response = requests.post(settings.TEXOID_URL, data=utf8bytes(document), headers={
- 'Content-Type': 'application/x-tex',
- })
+ response = requests.post(
+ settings.TEXOID_URL,
+ data=utf8bytes(document),
+ headers={
+ "Content-Type": "application/x-tex",
+ },
+ )
response.raise_for_status()
except requests.HTTPError as e:
if e.response.status_code == 400:
- logger.error('Texoid failed to render: %s\n%s', document, e.response.text)
+ logger.error(
+ "Texoid failed to render: %s\n%s", document, e.response.text
+ )
else:
- logger.exception('Failed to connect to texoid for: %s', document)
+ logger.exception("Failed to connect to texoid for: %s", document)
return
except Exception:
- logger.exception('Failed to connect to texoid for: %s', document)
+ logger.exception("Failed to connect to texoid for: %s", document)
return
try:
data = response.json()
except ValueError:
- logger.exception('Invalid texoid response for: %s\n%s', document, response.text)
+ logger.exception(
+ "Invalid texoid response for: %s\n%s", document, response.text
+ )
return
- if not data['success']:
- logger.error('Texoid failure for: %s\n%s', document, data)
- return {'error': data['error']}
+ if not data["success"]:
+ logger.error("Texoid failure for: %s\n%s", document, data)
+ return {"error": data["error"]}
- meta = data['meta']
- self.cache.cache_data(hash, 'meta', utf8bytes(json.dumps(meta)), url=False, gzip=False)
+ meta = data["meta"]
+ self.cache.cache_data(
+ hash, "meta", utf8bytes(json.dumps(meta)), url=False, gzip=False
+ )
result = {
- 'png': self.cache.cache_data(hash, 'png', b64decode(data['png'])),
- 'svg': self.cache.cache_data(hash, 'svg', data['svg'].encode('utf-8')),
- 'meta': meta,
+ "png": self.cache.cache_data(hash, "png", b64decode(data["png"])),
+ "svg": self.cache.cache_data(hash, "svg", data["svg"].encode("utf-8")),
+ "meta": meta,
}
return result
def query_cache(self, hash):
result = {
- 'svg': self.cache.get_url(hash, 'svg'),
- 'png': self.cache.get_url(hash, 'png'),
+ "svg": self.cache.get_url(hash, "svg"),
+ "png": self.cache.get_url(hash, "png"),
}
- key = 'texoid:meta:' + hash
+ key = "texoid:meta:" + hash
cached_meta = self.meta_cache.get(key)
if cached_meta is None:
- cached_meta = json.loads(self.cache.read_data(hash, 'meta').decode('utf-8'))
+ cached_meta = json.loads(self.cache.read_data(hash, "meta").decode("utf-8"))
self.meta_cache.set(key, cached_meta, self.meta_cache_ttl)
- result['meta'] = cached_meta
+ result["meta"] = cached_meta
return result
def get_result(self, formula):
hash = hashlib.sha1(utf8bytes(formula)).hexdigest()
- if self.cache.has_file(hash, 'svg'):
+ if self.cache.has_file(hash, "svg"):
return self.query_cache(hash)
else:
return self.query_texoid(formula, hash)
diff --git a/judge/utils/tickets.py b/judge/utils/tickets.py
index 28973d6ee7..50a4b83e3e 100644
--- a/judge/utils/tickets.py
+++ b/judge/utils/tickets.py
@@ -9,6 +9,10 @@ def own_ticket_filter(profile_id):
def filter_visible_tickets(queryset, user):
- return queryset.filter(own_ticket_filter(user.profile.id) |
- Q(content_type=ContentType.objects.get_for_model(Problem),
- object_id__in=Problem.get_editable_problems(user))).distinct()
+ return queryset.filter(
+ own_ticket_filter(user.profile.id)
+ | Q(
+ content_type=ContentType.objects.get_for_model(Problem),
+ object_id__in=Problem.get_editable_problems(user),
+ )
+ ).distinct()
diff --git a/judge/utils/timedelta.py b/judge/utils/timedelta.py
index cb1b4d7521..3975327a91 100644
--- a/judge/utils/timedelta.py
+++ b/judge/utils/timedelta.py
@@ -3,7 +3,7 @@
from django.utils.translation import ngettext, npgettext, pgettext
-def nice_repr(timedelta, display='long', sep=', '):
+def nice_repr(timedelta, display="long", sep=", "):
"""
Turns a datetime.timedelta object into a nice string repr.
@@ -16,7 +16,9 @@ def nice_repr(timedelta, display='long', sep=', '):
'1d, 1s'
"""
- assert isinstance(timedelta, datetime.timedelta), 'First argument must be a timedelta.'
+ assert isinstance(
+ timedelta, datetime.timedelta
+ ), "First argument must be a timedelta."
result = []
@@ -26,65 +28,94 @@ def nice_repr(timedelta, display='long', sep=', '):
minutes = (timedelta.seconds % 3600) // 60
seconds = timedelta.seconds % 60
- if display == 'simple-no-seconds':
+ if display == "simple-no-seconds":
days += weeks * 7
if days:
if hours or minutes:
- return '%d day%s %d:%02d' % (days, 's'[days == 1:], hours, minutes)
- return '%d day%s' % (days, 's'[days == 1:])
+ return "%d day%s %d:%02d" % (days, "s"[days == 1 :], hours, minutes)
+ return "%d day%s" % (days, "s"[days == 1 :])
else:
- return '%d:%02d' % (hours, minutes)
- elif display == 'sql':
+ return "%d:%02d" % (hours, minutes)
+ elif display == "sql":
days += weeks * 7
- return '%d %02d:%02d:%02d' % (days, hours, minutes, seconds)
- elif display == 'simple':
+ return "%d %02d:%02d:%02d" % (days, hours, minutes, seconds)
+ elif display == "simple":
days += weeks * 7
if days:
- return '%d day%s %02d:%02d:%02d' % (days, 's'[days == 1:], hours, minutes, seconds)
+ return "%d day%s %02d:%02d:%02d" % (
+ days,
+ "s"[days == 1 :],
+ hours,
+ minutes,
+ seconds,
+ )
else:
- return '%02d:%02d:%02d' % (hours, minutes, seconds)
- elif display == 'localized':
+ return "%02d:%02d:%02d" % (hours, minutes, seconds)
+ elif display == "localized":
days += weeks * 7
if days:
- return npgettext('time format with day', '%d day %h:%m:%s', '%d days %h:%m:%s', days) \
- .replace('%d', str(days)).replace('%h', '%02d' % hours).replace('%m', '%02d' % minutes) \
- .replace('%s', '%02d' % seconds)
+ return (
+ npgettext(
+ "time format with day", "%d day %h:%m:%s", "%d days %h:%m:%s", days
+ )
+ .replace("%d", str(days))
+ .replace("%h", "%02d" % hours)
+ .replace("%m", "%02d" % minutes)
+ .replace("%s", "%02d" % seconds)
+ )
else:
- return pgettext('time format without day', '%h:%m:%s') \
- .replace('%h', '%02d' % hours).replace('%m', '%02d' % minutes).replace('%s', '%02d' % seconds)
- elif display == 'localized-no-seconds':
+ return (
+ pgettext("time format without day", "%h:%m:%s")
+ .replace("%h", "%02d" % hours)
+ .replace("%m", "%02d" % minutes)
+ .replace("%s", "%02d" % seconds)
+ )
+ elif display == "localized-no-seconds":
days += weeks * 7
if days:
if hours or minutes:
- return npgettext('time format no seconds with day', '%d day %h:%m', '%d days %h:%m', days) \
- .replace('%d', str(days)).replace('%h', '%02d' % hours).replace('%m', '%02d' % minutes)
- return ngettext('%d day', '%d days', days) % days
+ return (
+ npgettext(
+ "time format no seconds with day",
+ "%d day %h:%m",
+ "%d days %h:%m",
+ days,
+ )
+ .replace("%d", str(days))
+ .replace("%h", "%02d" % hours)
+ .replace("%m", "%02d" % minutes)
+ )
+ return ngettext("%d day", "%d days", days) % days
else:
- return pgettext('hours and minutes', '%h:%m').replace('%h', '%02d' % hours).replace('%m', '%02d' % minutes)
- elif display == 'concise':
+ return (
+ pgettext("hours and minutes", "%h:%m")
+ .replace("%h", "%02d" % hours)
+ .replace("%m", "%02d" % minutes)
+ )
+ elif display == "concise":
days += weeks * 7
if days:
- return '%dd %02d:%02d:%02d' % (days, hours, minutes, seconds)
+ return "%dd %02d:%02d:%02d" % (days, hours, minutes, seconds)
else:
- return '%02d:%02d:%02d' % (hours, minutes, seconds)
- elif display == 'noday':
+ return "%02d:%02d:%02d" % (hours, minutes, seconds)
+ elif display == "noday":
days += weeks * 7
hours += days * 24
- return '%02d:%02d:%02d' % (hours, minutes, seconds)
- elif display == 'minimal':
- words = ['w', 'd', 'h', 'm', 's']
- elif display == 'short':
- words = [' wks', ' days', ' hrs', ' min', ' sec']
+ return "%02d:%02d:%02d" % (hours, minutes, seconds)
+ elif display == "minimal":
+ words = ["w", "d", "h", "m", "s"]
+ elif display == "short":
+ words = [" wks", " days", " hrs", " min", " sec"]
else:
- words = [' weeks', ' days', ' hours', ' minutes', ' seconds']
+ words = [" weeks", " days", " hours", " minutes", " seconds"]
values = [weeks, days, hours, minutes, seconds]
for i in range(len(values)):
if values[i]:
if values[i] == 1 and len(words[i]) > 1:
- result.append('%i%s' % (values[i], words[i].rstrip('s')))
+ result.append("%i%s" % (values[i], words[i].rstrip("s")))
else:
- result.append('%i%s' % (values[i], words[i]))
+ result.append("%i%s" % (values[i], words[i]))
return sep.join(result)
diff --git a/judge/utils/two_factor.py b/judge/utils/two_factor.py
index 2fd4e91943..d4061207d8 100644
--- a/judge/utils/two_factor.py
+++ b/judge/utils/two_factor.py
@@ -3,16 +3,16 @@
def webauthn_encode(binary):
- return base64.urlsafe_b64encode(binary).decode('ascii').rstrip('=')
+ return base64.urlsafe_b64encode(binary).decode("ascii").rstrip("=")
def webauthn_decode(text):
- text += '=' * (-len(text) % 4)
+ text += "=" * (-len(text) % 4)
return base64.urlsafe_b64decode(text)
class WebAuthnJSONEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, bytes):
- return {'_bytes': webauthn_encode(o)}
+ return {"_bytes": webauthn_encode(o)}
return super().default(o)
diff --git a/judge/utils/unicode.py b/judge/utils/unicode.py
index 8066c45596..084be4f543 100644
--- a/judge/utils/unicode.py
+++ b/judge/utils/unicode.py
@@ -16,22 +16,22 @@ def utf8bytes(maybe_text):
return None
if isinstance(maybe_text, bytes):
return maybe_text
- return maybe_text.encode('utf-8')
+ return maybe_text.encode("utf-8")
@overload
-def utf8text(maybe_bytes: AnyStr, errors='strict') -> str:
+def utf8text(maybe_bytes: AnyStr, errors="strict") -> str:
pass
@overload
-def utf8text(maybe_bytes: None, errors='strict') -> None:
+def utf8text(maybe_bytes: None, errors="strict") -> None:
pass
-def utf8text(maybe_bytes, errors='strict') -> Optional[str]:
+def utf8text(maybe_bytes, errors="strict") -> Optional[str]:
if maybe_bytes is None:
return None
if isinstance(maybe_bytes, str):
return maybe_bytes
- return maybe_bytes.decode('utf-8', errors)
+ return maybe_bytes.decode("utf-8", errors)
diff --git a/judge/utils/views.py b/judge/utils/views.py
index 6abe796c0e..22ad617338 100644
--- a/judge/utils/views.py
+++ b/judge/utils/views.py
@@ -6,54 +6,65 @@
def generic_message(request, title, message, status=None):
- return render(request, 'generic-message.html', {
- 'message': message,
- 'title': title,
- }, status=status)
+ return render(
+ request,
+ "generic-message.html",
+ {
+ "message": message,
+ "title": title,
+ },
+ status=status,
+ )
def add_file_response(request, response, url_path, file_path, file_object=None):
- if url_path is not None and request.META.get('SERVER_SOFTWARE', '').startswith('nginx/'):
- response['X-Accel-Redirect'] = url_path
+ if url_path is not None and request.META.get("SERVER_SOFTWARE", "").startswith(
+ "nginx/"
+ ):
+ response["X-Accel-Redirect"] = url_path
else:
if file_object is None:
- with open(file_path, 'rb') as f:
+ with open(file_path, "rb") as f:
response.content = f.read()
else:
- with file_object.open(file_path, 'rb') as f:
+ with file_object.open(file_path, "rb") as f:
response.content = f.read()
def paginate_query_context(request):
query = request.GET.copy()
- query.setlist('page', [])
+ query.setlist("page", [])
query = query.urlencode()
if query:
- return {'page_prefix': '%s?%s&page=' % (request.path, query),
- 'first_page_href': '%s?%s' % (request.path, query)}
+ return {
+ "page_prefix": "%s?%s&page=" % (request.path, query),
+ "first_page_href": "%s?%s" % (request.path, query),
+ }
else:
- return {'page_prefix': '%s?page=' % request.path,
- 'first_page_href': request.path}
+ return {
+ "page_prefix": "%s?page=" % request.path,
+ "first_page_href": request.path,
+ }
class NoBatchDeleteMixin(object):
def get_actions(self, request):
actions = super(NoBatchDeleteMixin, self).get_actions(request)
- if 'delete_selected' in actions:
- del actions['delete_selected']
+ if "delete_selected" in actions:
+ del actions["delete_selected"]
return actions
class TitleMixin(object):
- title = '(untitled)'
+ title = "(untitled)"
content_title = None
def get_context_data(self, **kwargs):
context = super(TitleMixin, self).get_context_data(**kwargs)
- context['title'] = self.get_title()
+ context["title"] = self.get_title()
content_title = self.get_content_title()
if content_title is not None:
- context['content_title'] = content_title
+ context["content_title"] = content_title
return context
def get_content_title(self):
@@ -64,10 +75,18 @@ def get_title(self):
class DiggPaginatorMixin(object):
- def get_paginator(self, queryset, per_page, orphans=0,
- allow_empty_first_page=True, **kwargs):
- return DiggPaginator(queryset, per_page, body=6, padding=2,
- orphans=orphans, allow_empty_first_page=allow_empty_first_page, **kwargs)
+ def get_paginator(
+ self, queryset, per_page, orphans=0, allow_empty_first_page=True, **kwargs
+ ):
+ return DiggPaginator(
+ queryset,
+ per_page,
+ body=6,
+ padding=2,
+ orphans=orphans,
+ allow_empty_first_page=allow_empty_first_page,
+ **kwargs
+ )
class QueryStringSortMixin(object):
@@ -79,8 +98,11 @@ def get_default_sort_order(self, request):
return self.default_sort
def get(self, request, *args, **kwargs):
- order = request.GET.get('order', '')
- if not ((not order.startswith('-') or order.count('-') == 1) and (order.lstrip('-') in self.all_sorts)):
+ order = request.GET.get("order", "")
+ if not (
+ (not order.startswith("-") or order.count("-") == 1)
+ and (order.lstrip("-") in self.all_sorts)
+ ):
order = self.get_default_sort_order(request)
self.order = order
@@ -88,17 +110,26 @@ def get(self, request, *args, **kwargs):
def get_sort_context(self):
query = self.request.GET.copy()
- query.setlist('order', [])
+ query.setlist("order", [])
query = query.urlencode()
- sort_prefix = '%s?%s&order=' % (self.request.path, query) if query else '%s?order=' % self.request.path
- current = self.order.lstrip('-')
-
- links = {key: sort_prefix + ('-' if key in self.default_desc else '') + key for key in self.all_sorts}
- links[current] = sort_prefix + ('' if self.order.startswith('-') else '-') + current
-
- order = {key: '' for key in self.all_sorts}
- order[current] = ' \u25BE' if self.order.startswith('-') else ' \u25B4'
- return {'sort_links': links, 'sort_order': order}
+ sort_prefix = (
+ "%s?%s&order=" % (self.request.path, query)
+ if query
+ else "%s?order=" % self.request.path
+ )
+ current = self.order.lstrip("-")
+
+ links = {
+ key: sort_prefix + ("-" if key in self.default_desc else "") + key
+ for key in self.all_sorts
+ }
+ links[current] = (
+ sort_prefix + ("" if self.order.startswith("-") else "-") + current
+ )
+
+ order = {key: "" for key in self.all_sorts}
+ order[current] = " \u25BE" if self.order.startswith("-") else " \u25B4"
+ return {"sort_links": links, "sort_order": order}
def get_sort_paginate_context(self):
return paginate_query_context(self.request)
diff --git a/judge/views/__init__.py b/judge/views/__init__.py
index 37d173d4ec..94a01c5d0d 100644
--- a/judge/views/__init__.py
+++ b/judge/views/__init__.py
@@ -5,6 +5,6 @@ class TitledTemplateView(TemplateView):
title = None
def get_context_data(self, **kwargs):
- if 'title' not in kwargs and self.title is not None:
- kwargs['title'] = self.title
+ if "title" not in kwargs and self.title is not None:
+ kwargs["title"] = self.title
return super(TitledTemplateView, self).get_context_data(**kwargs)
diff --git a/judge/views/api/__init__.py b/judge/views/api/__init__.py
index f25bab089b..14f20e4088 100644
--- a/judge/views/api/__init__.py
+++ b/judge/views/api/__init__.py
@@ -1,4 +1,12 @@
from .api_v2 import (
- APIContestDetail, APIContestList, APIContestParticipationList, APIOrganizationList, APIProblemDetail,
- APIProblemList, APISubmissionDetail, APISubmissionList, APIUserDetail, APIUserList,
+ APIContestDetail,
+ APIContestList,
+ APIContestParticipationList,
+ APIOrganizationList,
+ APIProblemDetail,
+ APIProblemList,
+ APISubmissionDetail,
+ APISubmissionList,
+ APIUserDetail,
+ APIUserList,
)
diff --git a/judge/views/api/api_v2.py b/judge/views/api/api_v2.py
index dd45c284b9..3d680de52c 100644
--- a/judge/views/api/api_v2.py
+++ b/judge/views/api/api_v2.py
@@ -10,7 +10,16 @@
from django.views.generic.list import BaseListView
from judge.models import (
- Contest, ContestParticipation, ContestTag, Judge, Language, Organization, Problem, ProblemType, Profile, Rating,
+ Contest,
+ ContestParticipation,
+ ContestTag,
+ Judge,
+ Language,
+ Organization,
+ Problem,
+ ProblemType,
+ Profile,
+ Rating,
Submission,
)
from judge.utils.infinite_paginator import InfinitePaginationMixin
@@ -52,7 +61,11 @@ def __init__(self, lookup):
self.lookup = lookup
def to_filter(self, key_list):
- return {f'{self.lookup}_id__in': Language.objects.filter(key__in=key_list).values_list('id', flat=True)}
+ return {
+ f"{self.lookup}_id__in": Language.objects.filter(
+ key__in=key_list
+ ).values_list("id", flat=True)
+ }
class APILoginRequiredException(Exception):
@@ -79,9 +92,9 @@ def get_api_data(self, context):
def get_base_response(self, **kwargs):
resp = {
- 'api_version': '2.0',
- 'method': self.request.method.lower(),
- 'fetched': self._now.isoformat(),
+ "api_version": "2.0",
+ "method": self.request.method.lower(),
+ "fetched": self._now.isoformat(),
}
resp.update(kwargs)
return resp
@@ -91,20 +104,22 @@ def get_data(self, context):
def get_error(self, exception):
caught_exceptions = {
- ValueError: (400, 'invalid filter value type'),
- ValidationError: (400, 'invalid filter value type'),
- PermissionDenied: (403, 'permission denied'),
- APILoginRequiredException: (403, 'login required'),
- Http404: (404, 'page/object not found'),
+ ValueError: (400, "invalid filter value type"),
+ ValidationError: (400, "invalid filter value type"),
+ PermissionDenied: (403, "permission denied"),
+ APILoginRequiredException: (403, "login required"),
+ Http404: (404, "page/object not found"),
}
exception_type = type(exception)
if exception_type in caught_exceptions:
status_code, message = caught_exceptions[exception_type]
return JsonResponse(
- self.get_base_response(error={
- 'code': status_code,
- 'message': message,
- }),
+ self.get_base_response(
+ error={
+ "code": status_code,
+ "message": message,
+ }
+ ),
status=status_code,
)
else:
@@ -146,23 +161,31 @@ def filter_queryset(self, queryset):
for key, filter_name in self.basic_filters:
if key in self.request.GET:
if isinstance(filter_name, BaseSimpleFilter):
- queryset = queryset.filter(**filter_name.to_filter(self.request.GET.get(key)))
+ queryset = queryset.filter(
+ **filter_name.to_filter(self.request.GET.get(key))
+ )
else:
# May raise ValueError or ValidationError, but is caught in APIMixin
- queryset = queryset.filter(**{
- filter_name: self.request.GET.get(key),
- })
+ queryset = queryset.filter(
+ **{
+ filter_name: self.request.GET.get(key),
+ }
+ )
self.used_basic_filters.add(key)
for key, filter_name in self.list_filters:
if key in self.request.GET:
if isinstance(filter_name, BaseListFilter):
- queryset = queryset.filter(**filter_name.to_filter(self.request.GET.getlist(key)))
+ queryset = queryset.filter(
+ **filter_name.to_filter(self.request.GET.getlist(key))
+ )
else:
# May raise ValueError or ValidationError, but is caught in APIMixin
- queryset = queryset.filter(**{
- filter_name + '__in': self.request.GET.getlist(key),
- })
+ queryset = queryset.filter(
+ **{
+ filter_name + "__in": self.request.GET.getlist(key),
+ }
+ )
self.used_list_filters.add(key)
return queryset
@@ -171,37 +194,35 @@ def get_queryset(self):
return self.filter_queryset(self.get_unfiltered_queryset())
def get_api_data(self, context):
- page = context['page_obj']
- objects = context['object_list']
+ page = context["page_obj"]
+ objects = context["object_list"]
result = {
- 'current_object_count': len(objects),
- 'objects_per_page': page.paginator.per_page,
- 'page_index': page.number,
- 'has_more': page.has_next(),
- 'objects': [self.get_object_data(obj) for obj in objects],
+ "current_object_count": len(objects),
+ "objects_per_page": page.paginator.per_page,
+ "page_index": page.number,
+ "has_more": page.has_next(),
+ "objects": [self.get_object_data(obj) for obj in objects],
}
if not page.paginator.is_infinite:
- result['total_objects'] = page.paginator.count
- result['total_pages'] = page.paginator.num_pages
+ result["total_objects"] = page.paginator.count
+ result["total_pages"] = page.paginator.num_pages
return result
class APIDetailView(APIMixin, BaseDetailView):
def get_api_data(self, context):
return {
- 'object': self.get_object_data(context['object']),
+ "object": self.get_object_data(context["object"]),
}
class APIContestList(APIListView):
model = Contest
- basic_filters = (
- ('is_rated', 'is_rated'),
- )
+ basic_filters = (("is_rated", "is_rated"),)
list_filters = (
- ('key', 'key'),
- ('tag', 'tags__name'),
- ('organization', 'organizations'),
+ ("key", "key"),
+ ("tag", "tags__name"),
+ ("organization", "organizations"),
)
def get_unfiltered_queryset(self):
@@ -209,31 +230,31 @@ def get_unfiltered_queryset(self):
Contest.get_visible_contests(self.request.user)
.prefetch_related(
Prefetch(
- 'tags',
- queryset=ContestTag.objects.only('name'),
- to_attr='tag_list',
+ "tags",
+ queryset=ContestTag.objects.only("name"),
+ to_attr="tag_list",
),
)
- .order_by('id')
+ .order_by("id")
)
def get_object_data(self, contest):
return {
- 'key': contest.key,
- 'name': contest.name,
- 'start_time': contest.start_time.isoformat(),
- 'end_time': contest.end_time.isoformat(),
- 'time_limit': contest.time_limit and contest.time_limit.total_seconds(),
- 'is_rated': contest.is_rated,
- 'rate_all': contest.is_rated and contest.rate_all,
- 'tags': list(map(attrgetter('name'), contest.tag_list)),
+ "key": contest.key,
+ "name": contest.name,
+ "start_time": contest.start_time.isoformat(),
+ "end_time": contest.end_time.isoformat(),
+ "time_limit": contest.time_limit and contest.time_limit.total_seconds(),
+ "is_rated": contest.is_rated,
+ "rate_all": contest.is_rated and contest.rate_all,
+ "tags": list(map(attrgetter("name"), contest.tag_list)),
}
class APIContestDetail(APIDetailView):
model = Contest
- slug_field = 'key'
- slug_url_kwarg = 'contest'
+ slug_field = "key"
+ slug_url_kwarg = "contest"
def get_object(self, queryset=None):
contest = super().get_object(queryset)
@@ -244,30 +265,29 @@ def get_object(self, queryset=None):
def get_object_data(self, contest):
in_contest = contest.is_in_contest(self.request.user)
can_see_rankings = contest.can_see_full_scoreboard(self.request.user)
- can_see_problems = (in_contest or contest.ended or contest.is_editable_by(self.request.user))
+ can_see_problems = (
+ in_contest or contest.ended or contest.is_editable_by(self.request.user)
+ )
problems = list(
- contest.contest_problems
- .select_related('problem')
- .defer('problem__description')
- .order_by('order'),
+ contest.contest_problems.select_related("problem")
+ .defer("problem__description")
+ .order_by("order"),
)
- new_ratings_subquery = Rating.objects.filter(participation=OuterRef('pk'))
- old_ratings_subquery = (
- Rating.objects
- .filter(user=OuterRef('user__pk'), contest__end_time__lt=OuterRef('contest__end_time'))
- .order_by('-contest__end_time')
- )
+ new_ratings_subquery = Rating.objects.filter(participation=OuterRef("pk"))
+ old_ratings_subquery = Rating.objects.filter(
+ user=OuterRef("user__pk"),
+ contest__end_time__lt=OuterRef("contest__end_time"),
+ ).order_by("-contest__end_time")
participations = (
- contest.users
- .filter(virtual=ContestParticipation.LIVE)
+ contest.users.filter(virtual=ContestParticipation.LIVE)
.annotate(
- username=F('user__user__username'),
- old_rating=Subquery(old_ratings_subquery.values('rating')[:1]),
- new_rating=Subquery(new_ratings_subquery.values('rating')[:1]),
+ username=F("user__user__username"),
+ old_rating=Subquery(old_ratings_subquery.values("rating")[:1]),
+ new_rating=Subquery(new_ratings_subquery.values("rating")[:1]),
)
- .order_by('-score', 'cumtime', 'tiebreaker')
+ .order_by("-score", "cumtime", "tiebreaker")
)
# Setting contest attribute to reduce db queries in .start and .end_time
@@ -275,65 +295,78 @@ def get_object_data(self, contest):
participation.contest = contest
return {
- 'key': contest.key,
- 'name': contest.name,
- 'start_time': contest.start_time.isoformat(),
- 'end_time': contest.end_time.isoformat(),
- 'time_limit': contest.time_limit and contest.time_limit.total_seconds(),
- 'is_rated': contest.is_rated,
- 'rate_all': contest.is_rated and contest.rate_all,
- 'has_rating': contest.ratings.exists(),
- 'rating_floor': contest.rating_floor,
- 'rating_ceiling': contest.rating_ceiling,
- 'hidden_scoreboard': contest.scoreboard_visibility in (contest.SCOREBOARD_AFTER_CONTEST,
- contest.SCOREBOARD_AFTER_PARTICIPATION,
- contest.SCOREBOARD_HIDDEN),
- 'scoreboard_visibility': contest.scoreboard_visibility,
- 'is_organization_private': contest.is_organization_private,
- 'organizations': list(
- contest.organizations.values_list('id', flat=True) if contest.is_organization_private else [],
+ "key": contest.key,
+ "name": contest.name,
+ "start_time": contest.start_time.isoformat(),
+ "end_time": contest.end_time.isoformat(),
+ "time_limit": contest.time_limit and contest.time_limit.total_seconds(),
+ "is_rated": contest.is_rated,
+ "rate_all": contest.is_rated and contest.rate_all,
+ "has_rating": contest.ratings.exists(),
+ "rating_floor": contest.rating_floor,
+ "rating_ceiling": contest.rating_ceiling,
+ "hidden_scoreboard": contest.scoreboard_visibility
+ in (
+ contest.SCOREBOARD_AFTER_CONTEST,
+ contest.SCOREBOARD_AFTER_PARTICIPATION,
+ contest.SCOREBOARD_HIDDEN,
+ ),
+ "scoreboard_visibility": contest.scoreboard_visibility,
+ "is_organization_private": contest.is_organization_private,
+ "organizations": list(
+ contest.organizations.values_list("id", flat=True)
+ if contest.is_organization_private
+ else [],
),
- 'is_private': contest.is_private,
- 'tags': list(contest.tags.values_list('name', flat=True)),
- 'format': {
- 'name': contest.format_name,
- 'config': contest.format_config,
+ "is_private": contest.is_private,
+ "tags": list(contest.tags.values_list("name", flat=True)),
+ "format": {
+ "name": contest.format_name,
+ "config": contest.format_config,
},
- 'problems': [
+ "problems": [
{
- 'points': int(problem.points),
- 'partial': problem.partial,
- 'is_pretested': problem.is_pretested and contest.run_pretests_only,
- 'max_submissions': problem.max_submissions or None,
- 'label': contest.get_label_for_problem(index),
- 'name': problem.problem.name,
- 'code': problem.problem.code,
- } for index, problem in enumerate(problems)
- ] if can_see_problems else [],
- 'rankings': [
+ "points": int(problem.points),
+ "partial": problem.partial,
+ "is_pretested": problem.is_pretested and contest.run_pretests_only,
+ "max_submissions": problem.max_submissions or None,
+ "label": contest.get_label_for_problem(index),
+ "name": problem.problem.name,
+ "code": problem.problem.code,
+ }
+ for index, problem in enumerate(problems)
+ ]
+ if can_see_problems
+ else [],
+ "rankings": [
{
- 'user': participation.username,
- 'start_time': participation.start.isoformat(),
- 'end_time': participation.end_time.isoformat(),
- 'score': participation.score,
- 'cumulative_time': participation.cumtime,
- 'tiebreaker': participation.tiebreaker,
- 'old_rating': participation.old_rating,
- 'new_rating': participation.new_rating,
- 'is_disqualified': participation.is_disqualified,
- 'solutions': contest.format.get_problem_breakdown(participation, problems),
- } for participation in participations
- ] if can_see_rankings else [],
+ "user": participation.username,
+ "start_time": participation.start.isoformat(),
+ "end_time": participation.end_time.isoformat(),
+ "score": participation.score,
+ "cumulative_time": participation.cumtime,
+ "tiebreaker": participation.tiebreaker,
+ "old_rating": participation.old_rating,
+ "new_rating": participation.new_rating,
+ "is_disqualified": participation.is_disqualified,
+ "solutions": contest.format.get_problem_breakdown(
+ participation, problems
+ ),
+ }
+ for participation in participations
+ ]
+ if can_see_rankings
+ else [],
}
class APIContestParticipationList(APIListView):
model = ContestParticipation
basic_filters = (
- ('contest', 'contest__key'),
- ('user', 'user__user__username'),
- ('is_disqualified', 'is_disqualified'),
- ('virtual_participation_number', 'virtual'),
+ ("contest", "contest__key"),
+ ("user", "user__user__username"),
+ ("is_disqualified", "is_disqualified"),
+ ("virtual_participation_number", "virtual"),
)
def get_unfiltered_queryset(self):
@@ -345,232 +378,252 @@ def get_unfiltered_queryset(self):
# 1. Contest has ended
# 2. User is the organizer or curator of the contest
# 3. User is specified to be able to "view contest scoreboard"
- if not self.request.user.has_perm('judge.see_private_contest'):
+ if not self.request.user.has_perm("judge.see_private_contest"):
q = Q(end_time__lt=self._now)
if self.request.user.is_authenticated:
- if self.request.user.has_perm('judge.edit_own_contest'):
+ if self.request.user.has_perm("judge.edit_own_contest"):
q |= Q(authors=self.request.profile)
q |= Q(curators=self.request.profile)
q |= Q(view_contest_scoreboard=self.request.profile)
visible_contests = visible_contests.filter(q)
return (
- ContestParticipation.objects
- .filter(virtual__gte=0, contest__in=visible_contests)
- .select_related('user__user', 'contest')
- .order_by('id')
+ ContestParticipation.objects.filter(
+ virtual__gte=0, contest__in=visible_contests
+ )
+ .select_related("user__user", "contest")
+ .order_by("id")
.only(
- 'user__user__username',
- 'contest__key',
- 'contest__start_time',
- 'contest__end_time',
- 'contest__time_limit',
- 'real_start',
- 'score',
- 'cumtime',
- 'tiebreaker',
- 'is_disqualified',
- 'virtual',
+ "user__user__username",
+ "contest__key",
+ "contest__start_time",
+ "contest__end_time",
+ "contest__time_limit",
+ "real_start",
+ "score",
+ "cumtime",
+ "tiebreaker",
+ "is_disqualified",
+ "virtual",
)
)
def get_object_data(self, participation):
return {
- 'user': participation.user.username,
- 'contest': participation.contest.key,
- 'start_time': participation.start.isoformat(),
- 'end_time': participation.end_time.isoformat(),
- 'score': participation.score,
- 'cumulative_time': participation.cumtime,
- 'tiebreaker': participation.tiebreaker,
- 'is_disqualified': participation.is_disqualified,
- 'virtual_participation_number': participation.virtual,
+ "user": participation.user.username,
+ "contest": participation.contest.key,
+ "start_time": participation.start.isoformat(),
+ "end_time": participation.end_time.isoformat(),
+ "score": participation.score,
+ "cumulative_time": participation.cumtime,
+ "tiebreaker": participation.tiebreaker,
+ "is_disqualified": participation.is_disqualified,
+ "virtual_participation_number": participation.virtual,
}
class APIProblemList(APIListView):
model = Problem
- basic_filters = (
- ('partial', 'partial'),
- )
+ basic_filters = (("partial", "partial"),)
list_filters = (
- ('code', 'code'),
- ('group', 'group__full_name'),
- ('type', 'types__full_name'),
- ('organization', 'organizations'),
+ ("code", "code"),
+ ("group", "group__full_name"),
+ ("type", "types__full_name"),
+ ("organization", "organizations"),
)
def get_unfiltered_queryset(self):
return (
Problem.get_visible_problems(self.request.user)
- .select_related('group')
+ .select_related("group")
.prefetch_related(
Prefetch(
- 'types',
- queryset=ProblemType.objects.only('full_name'),
- to_attr='type_list',
+ "types",
+ queryset=ProblemType.objects.only("full_name"),
+ to_attr="type_list",
),
)
- .order_by('id')
+ .order_by("id")
.distinct()
)
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
- if settings.ENABLE_FTS and 'search' in self.request.GET:
- query = ' '.join(self.request.GET.getlist('search')).strip()
+ if settings.ENABLE_FTS and "search" in self.request.GET:
+ query = " ".join(self.request.GET.getlist("search")).strip()
if query:
queryset = queryset.search(query)
return queryset
def get_object_data(self, problem):
return {
- 'code': problem.code,
- 'name': problem.name,
- 'types': list(map(attrgetter('full_name'), problem.type_list)),
- 'group': problem.group.full_name,
- 'points': problem.points,
- 'partial': problem.partial,
- 'is_organization_private': problem.is_organization_private,
- 'is_public': problem.is_public,
+ "code": problem.code,
+ "name": problem.name,
+ "types": list(map(attrgetter("full_name"), problem.type_list)),
+ "group": problem.group.full_name,
+ "points": problem.points,
+ "partial": problem.partial,
+ "is_organization_private": problem.is_organization_private,
+ "is_public": problem.is_public,
}
class APIProblemDetail(APIDetailView):
model = Problem
- slug_field = 'code'
- slug_url_kwarg = 'problem'
+ slug_field = "code"
+ slug_url_kwarg = "problem"
def get_object(self, queryset=None):
problem = super().get_object(queryset)
- if not problem.is_accessible_by(self.request.user, skip_contest_problem_check=True):
+ if not problem.is_accessible_by(
+ self.request.user, skip_contest_problem_check=True
+ ):
raise Http404()
return problem
def get_object_data(self, problem):
return {
- 'code': problem.code,
- 'name': problem.name,
- 'authors': list(problem.authors.values_list('user__username', flat=True)),
- 'types': list(problem.types.values_list('full_name', flat=True)),
- 'group': problem.group.full_name,
- 'time_limit': problem.time_limit,
- 'memory_limit': problem.memory_limit,
- 'language_resource_limits': [
+ "code": problem.code,
+ "name": problem.name,
+ "authors": list(problem.authors.values_list("user__username", flat=True)),
+ "types": list(problem.types.values_list("full_name", flat=True)),
+ "group": problem.group.full_name,
+ "time_limit": problem.time_limit,
+ "memory_limit": problem.memory_limit,
+ "language_resource_limits": [
{
- 'language': key,
- 'time_limit': time_limit,
- 'memory_limit': memory_limit,
+ "language": key,
+ "time_limit": time_limit,
+ "memory_limit": memory_limit,
}
- for key, time_limit, memory_limit in
- problem.language_limits.values_list('language__key', 'time_limit', 'memory_limit')
+ for key, time_limit, memory_limit in problem.language_limits.values_list(
+ "language__key", "time_limit", "memory_limit"
+ )
],
- 'points': problem.points,
- 'partial': problem.partial,
- 'short_circuit': problem.short_circuit,
- 'languages': list(problem.allowed_languages.values_list('key', flat=True)),
- 'is_organization_private': problem.is_organization_private,
- 'organizations': list(
- problem.organizations.values_list('id', flat=True) if problem.is_organization_private else [],
+ "points": problem.points,
+ "partial": problem.partial,
+ "short_circuit": problem.short_circuit,
+ "languages": list(problem.allowed_languages.values_list("key", flat=True)),
+ "is_organization_private": problem.is_organization_private,
+ "organizations": list(
+ problem.organizations.values_list("id", flat=True)
+ if problem.is_organization_private
+ else [],
),
- 'is_public': problem.is_public,
+ "is_public": problem.is_public,
}
class APIUserList(APIListView):
model = Profile
list_filters = (
- ('id', 'id'),
- ('username', 'username'),
- ('organization', 'organizations'),
+ ("id", "id"),
+ ("username", "username"),
+ ("organization", "organizations"),
)
def get_unfiltered_queryset(self):
return (
- Profile.objects
- .filter(is_unlisted=False, user__is_active=True)
- .annotate(username=F('user__username'))
- .order_by('id')
- .only('id', 'points', 'performance_points', 'problem_count', 'display_rank', 'rating')
+ Profile.objects.filter(is_unlisted=False, user__is_active=True)
+ .annotate(username=F("user__username"))
+ .order_by("id")
+ .only(
+ "id",
+ "points",
+ "performance_points",
+ "problem_count",
+ "display_rank",
+ "rating",
+ )
)
def get_object_data(self, profile):
return {
- 'id': profile.id,
- 'username': profile.username,
- 'points': profile.points,
- 'performance_points': profile.performance_points,
- 'problem_count': profile.problem_count,
- 'rank': profile.display_rank,
- 'rating': profile.rating,
+ "id": profile.id,
+ "username": profile.username,
+ "points": profile.points,
+ "performance_points": profile.performance_points,
+ "problem_count": profile.problem_count,
+ "rank": profile.display_rank,
+ "rating": profile.rating,
}
class APIUserDetail(APIDetailView):
model = Profile
- slug_field = 'user__username'
- slug_url_kwarg = 'user'
+ slug_field = "user__username"
+ slug_url_kwarg = "user"
def get_object_data(self, profile):
solved_problems = list(
- Submission.objects
- .filter(
- result='AC',
+ Submission.objects.filter(
+ result="AC",
user=profile,
problem__is_public=True,
problem__is_organization_private=False,
)
- .values('problem').distinct()
- .values_list('problem__code', flat=True),
+ .values("problem")
+ .distinct()
+ .values_list("problem__code", flat=True),
)
contest_history = []
- participations = (
- ContestParticipation.objects
- .filter(
- user=profile,
- virtual=ContestParticipation.LIVE,
- contest__in=Contest.get_visible_contests(self.request.user),
- contest__end_time__lt=self._now,
- )
- .order_by('contest__end_time')
- )
- for contest_key, score, cumtime, rating, mean, performance in participations.values_list(
- 'contest__key', 'score', 'cumtime', 'rating__rating', 'rating__mean', 'rating__performance',
+ participations = ContestParticipation.objects.filter(
+ user=profile,
+ virtual=ContestParticipation.LIVE,
+ contest__in=Contest.get_visible_contests(self.request.user),
+ contest__end_time__lt=self._now,
+ ).order_by("contest__end_time")
+ for (
+ contest_key,
+ score,
+ cumtime,
+ rating,
+ mean,
+ performance,
+ ) in participations.values_list(
+ "contest__key",
+ "score",
+ "cumtime",
+ "rating__rating",
+ "rating__mean",
+ "rating__performance",
):
- contest_history.append({
- 'key': contest_key,
- 'score': score,
- 'cumulative_time': cumtime,
- 'rating': rating,
- 'raw_rating': mean,
- 'performance': performance,
- })
+ contest_history.append(
+ {
+ "key": contest_key,
+ "score": score,
+ "cumulative_time": cumtime,
+ "rating": rating,
+ "raw_rating": mean,
+ "performance": performance,
+ }
+ )
return {
- 'id': profile.id,
- 'username': profile.user.username,
- 'points': profile.points,
- 'performance_points': profile.performance_points,
- 'problem_count': profile.problem_count,
- 'solved_problems': solved_problems,
- 'rank': profile.display_rank,
- 'rating': profile.rating,
- 'organizations': list(profile.organizations.values_list('id', flat=True)),
- 'contests': contest_history,
+ "id": profile.id,
+ "username": profile.user.username,
+ "points": profile.points,
+ "performance_points": profile.performance_points,
+ "problem_count": profile.problem_count,
+ "solved_problems": solved_problems,
+ "rank": profile.display_rank,
+ "rating": profile.rating,
+ "organizations": list(profile.organizations.values_list("id", flat=True)),
+ "contests": contest_history,
}
class APISubmissionList(APIListView):
model = Submission
basic_filters = (
- ('user', ProfileSimpleFilter('user')),
- ('problem', ProblemSimpleFilter('problem')),
+ ("user", ProfileSimpleFilter("user")),
+ ("problem", ProblemSimpleFilter("problem")),
)
list_filters = (
- ('id', 'id'),
- ('language', LanguageListFilter('language')),
- ('result', 'result'),
+ ("id", "id"),
+ ("language", LanguageListFilter("language")),
+ ("result", "result"),
)
@property
@@ -582,47 +635,49 @@ def get_unfiltered_queryset(self):
use_straight_join(queryset)
join_sql_subquery(
queryset,
- subquery=Problem.get_visible_problems(self.request.user).distinct().only('id').query,
+ subquery=Problem.get_visible_problems(self.request.user)
+ .distinct()
+ .only("id")
+ .query,
params=[],
related_model=Problem,
- join_fields=[('problem_id', 'id')],
- alias='visible_problems',
+ join_fields=[("problem_id", "id")],
+ alias="visible_problems",
)
return (
- queryset
- .select_related('problem', 'user__user', 'language')
- .order_by('id')
+ queryset.select_related("problem", "user__user", "language")
+ .order_by("id")
.only(
- 'id',
- 'problem__code',
- 'user__user__username',
- 'date',
- 'language__key',
- 'time',
- 'memory',
- 'points',
- 'result',
+ "id",
+ "problem__code",
+ "user__user__username",
+ "date",
+ "language__key",
+ "time",
+ "memory",
+ "points",
+ "result",
)
)
def get_object_data(self, submission):
return {
- 'id': submission.id,
- 'problem': submission.problem.code,
- 'user': submission.user.user.username,
- 'date': submission.date.isoformat(),
- 'language': submission.language.key,
- 'time': submission.time,
- 'memory': submission.memory,
- 'points': submission.points,
- 'result': submission.result,
+ "id": submission.id,
+ "problem": submission.problem.code,
+ "user": submission.user.user.username,
+ "date": submission.date.isoformat(),
+ "language": submission.language.key,
+ "time": submission.time,
+ "memory": submission.memory,
+ "points": submission.points,
+ "result": submission.result,
}
class APISubmissionDetail(APILoginRequiredMixin, APIDetailView):
model = Submission
- slug_field = 'id'
- slug_url_kwarg = 'submission'
+ slug_field = "id"
+ slug_url_kwarg = "submission"
def get_object(self, queryset=None):
submission = super().get_object(queryset)
@@ -635,87 +690,86 @@ def get_object_data(self, submission):
for batch in group_test_cases(submission.test_cases.all())[0]:
batch_cases = [
{
- 'type': 'case',
- 'case_id': case.case,
- 'status': case.status,
- 'time': case.time,
- 'memory': case.memory,
- 'points': case.points,
- 'total': case.total,
- } for case in batch['cases']
+ "type": "case",
+ "case_id": case.case,
+ "status": case.status,
+ "time": case.time,
+ "memory": case.memory,
+ "points": case.points,
+ "total": case.total,
+ }
+ for case in batch["cases"]
]
# These are individual cases.
- if batch['id'] is None:
+ if batch["id"] is None:
cases.extend(batch_cases)
# This is one batch.
else:
- cases.append({
- 'type': 'batch',
- 'batch_id': batch['id'],
- 'cases': batch_cases,
- 'points': batch['points'],
- 'total': batch['total'],
- })
+ cases.append(
+ {
+ "type": "batch",
+ "batch_id": batch["id"],
+ "cases": batch_cases,
+ "points": batch["points"],
+ "total": batch["total"],
+ }
+ )
return {
- 'id': submission.id,
- 'problem': submission.problem.code,
- 'user': submission.user.user.username,
- 'date': submission.date.isoformat(),
- 'time': submission.time,
- 'memory': submission.memory,
- 'points': submission.points,
- 'language': submission.language.key,
- 'status': submission.status,
- 'result': submission.result,
- 'case_points': submission.case_points,
- 'case_total': submission.case_total,
- 'cases': cases,
+ "id": submission.id,
+ "problem": submission.problem.code,
+ "user": submission.user.user.username,
+ "date": submission.date.isoformat(),
+ "time": submission.time,
+ "memory": submission.memory,
+ "points": submission.points,
+ "language": submission.language.key,
+ "status": submission.status,
+ "result": submission.result,
+ "case_points": submission.case_points,
+ "case_total": submission.case_total,
+ "cases": cases,
}
class APIOrganizationList(APIListView):
model = Organization
- basic_filters = (
- ('is_open', 'is_open'),
- )
- list_filters = (
- ('id', 'id'),
- )
+ basic_filters = (("is_open", "is_open"),)
+ list_filters = (("id", "id"),)
def get_unfiltered_queryset(self):
- return Organization.objects.annotate(member_count=Count('member')).order_by('id')
+ return Organization.objects.annotate(member_count=Count("member")).order_by(
+ "id"
+ )
def get_object_data(self, organization):
return {
- 'id': organization.id,
- 'slug': organization.slug,
- 'short_name': organization.short_name,
- 'is_open': organization.is_open,
- 'member_count': organization.member_count,
+ "id": organization.id,
+ "slug": organization.slug,
+ "short_name": organization.short_name,
+ "is_open": organization.is_open,
+ "member_count": organization.member_count,
}
class APILanguageList(APIListView):
model = Language
- basic_filters = (
- ('common_name', 'common_name'),
- )
+ basic_filters = (("common_name", "common_name"),)
list_filters = (
- ('id', 'id'),
- ('key', 'key'),
+ ("id", "id"),
+ ("key", "key"),
)
def get_object_data(self, language):
return {
- 'id': language.id,
- 'key': language.key,
- 'short_name': language.short_name,
- 'common_name': language.common_name,
- 'ace_mode_name': language.ace,
- 'pygments_name': language.pygments,
- 'code_template': language.template,
+ "id": language.id,
+ "key": language.key,
+ "short_name": language.short_name,
+ "common_name": language.common_name,
+ "ace_mode_name": language.ace,
+ "pygments_name": language.pygments,
+ "code_template": language.template,
}
@@ -723,13 +777,17 @@ class APIJudgeList(APIListView):
model = Judge
def get_unfiltered_queryset(self):
- return Judge.objects.filter(online=True).prefetch_related('runtimes').order_by('id')
+ return (
+ Judge.objects.filter(online=True)
+ .prefetch_related("runtimes")
+ .order_by("id")
+ )
def get_object_data(self, judge):
return {
- 'name': judge.name,
- 'start_time': judge.start_time.isoformat(),
- 'ping': judge.ping_ms,
- 'load': judge.load,
- 'languages': list(judge.runtimes.values_list('key', flat=True)),
+ "name": judge.name,
+ "start_time": judge.start_time.isoformat(),
+ "ping": judge.ping_ms,
+ "load": judge.load,
+ "languages": list(judge.runtimes.values_list("key", flat=True)),
}
diff --git a/judge/views/blog.py b/judge/views/blog.py
index d71d435552..bae0bf57ba 100644
--- a/judge/views/blog.py
+++ b/judge/views/blog.py
@@ -7,8 +7,17 @@
from django.views.generic import ListView
from judge.comments import CommentedDetailView
-from judge.models import BlogPost, Comment, Contest, Language, Problem, ProblemClarification, Profile, Submission, \
- Ticket
+from judge.models import (
+ BlogPost,
+ Comment,
+ Contest,
+ Language,
+ Problem,
+ ProblemClarification,
+ Profile,
+ Submission,
+ Ticket,
+)
from judge.utils.cachedict import CacheDict
from judge.utils.diggpaginator import DiggPaginator
from judge.utils.opengraph import generate_opengraph
@@ -19,94 +28,131 @@
class PostList(ListView):
model = BlogPost
paginate_by = 10
- context_object_name = 'posts'
- template_name = 'blog/list.html'
+ context_object_name = "posts"
+ template_name = "blog/list.html"
title = None
- def get_paginator(self, queryset, per_page, orphans=0,
- allow_empty_first_page=True, **kwargs):
- return DiggPaginator(queryset, per_page, body=6, padding=2,
- orphans=orphans, allow_empty_first_page=allow_empty_first_page, **kwargs)
+ def get_paginator(
+ self, queryset, per_page, orphans=0, allow_empty_first_page=True, **kwargs
+ ):
+ return DiggPaginator(
+ queryset,
+ per_page,
+ body=6,
+ padding=2,
+ orphans=orphans,
+ allow_empty_first_page=allow_empty_first_page,
+ **kwargs
+ )
def get_queryset(self):
- return (BlogPost.objects.filter(visible=True, publish_on__lte=timezone.now()).order_by('-sticky', '-publish_on')
- .prefetch_related('authors__user'))
+ return (
+ BlogPost.objects.filter(visible=True, publish_on__lte=timezone.now())
+ .order_by("-sticky", "-publish_on")
+ .prefetch_related("authors__user")
+ )
def get_context_data(self, **kwargs):
context = super(PostList, self).get_context_data(**kwargs)
- context['title'] = self.title or _('Page %d of Posts') % context['page_obj'].number
- context['first_page_href'] = reverse('home')
- context['page_prefix'] = reverse('blog_post_list')
- context['comments'] = Comment.most_recent(self.request.user, 10)
- context['new_problems'] = Problem.get_public_problems() \
- .order_by('-date', 'code')[:settings.DMOJ_BLOG_NEW_PROBLEM_COUNT]
- context['page_titles'] = CacheDict(lambda page: Comment.get_page_title(page))
-
- context['has_clarifications'] = False
+ context["title"] = (
+ self.title or _("Page %d of Posts") % context["page_obj"].number
+ )
+ context["first_page_href"] = reverse("home")
+ context["page_prefix"] = reverse("blog_post_list")
+ context["comments"] = Comment.most_recent(self.request.user, 10)
+ context["new_problems"] = Problem.get_public_problems().order_by(
+ "-date", "code"
+ )[: settings.DMOJ_BLOG_NEW_PROBLEM_COUNT]
+ context["page_titles"] = CacheDict(lambda page: Comment.get_page_title(page))
+
+ context["has_clarifications"] = False
if self.request.user.is_authenticated:
participation = self.request.profile.current_contest
if participation:
- clarifications = ProblemClarification.objects.filter(problem__in=participation.contest.problems.all())
- context['has_clarifications'] = clarifications.count() > 0
- context['clarifications'] = clarifications.order_by('-date')
-
- context['user_count'] = Profile.objects.count
- context['problem_count'] = Problem.get_public_problems().count
- context['submission_count'] = lambda: Submission.objects.aggregate(max_id=Max('id'))['max_id'] or 0
- context['language_count'] = Language.objects.count
-
- context['post_comment_counts'] = {
- int(page[2:]): count for page, count in
- Comment.objects
- .filter(page__in=['b:%d' % post.id for post in context['posts']], hidden=False)
- .values_list('page').annotate(count=Count('page')).order_by()
+ clarifications = ProblemClarification.objects.filter(
+ problem__in=participation.contest.problems.all()
+ )
+ context["has_clarifications"] = clarifications.count() > 0
+ context["clarifications"] = clarifications.order_by("-date")
+
+ context["user_count"] = Profile.objects.count
+ context["problem_count"] = Problem.get_public_problems().count
+ context["submission_count"] = (
+ lambda: Submission.objects.aggregate(max_id=Max("id"))["max_id"] or 0
+ )
+ context["language_count"] = Language.objects.count
+
+ context["post_comment_counts"] = {
+ int(page[2:]): count
+ for page, count in Comment.objects.filter(
+ page__in=["b:%d" % post.id for post in context["posts"]], hidden=False
+ )
+ .values_list("page")
+ .annotate(count=Count("page"))
+ .order_by()
}
now = timezone.now()
- visible_contests = Contest.get_visible_contests(self.request.user).filter(is_visible=True) \
- .order_by('start_time')
+ visible_contests = (
+ Contest.get_visible_contests(self.request.user)
+ .filter(is_visible=True)
+ .order_by("start_time")
+ )
- context['current_contests'] = visible_contests.filter(start_time__lte=now, end_time__gt=now)
- context['future_contests'] = visible_contests.filter(start_time__gt=now)
+ context["current_contests"] = visible_contests.filter(
+ start_time__lte=now, end_time__gt=now
+ )
+ context["future_contests"] = visible_contests.filter(start_time__gt=now)
if self.request.user.is_authenticated:
- context['own_open_tickets'] = (
- Ticket.objects.filter(user=self.request.profile, is_open=True).order_by('-id')
- .prefetch_related('linked_item').select_related('user__user')
+ context["own_open_tickets"] = (
+ Ticket.objects.filter(user=self.request.profile, is_open=True)
+ .order_by("-id")
+ .prefetch_related("linked_item")
+ .select_related("user__user")
)
else:
- context['own_open_tickets'] = []
+ context["own_open_tickets"] = []
# Superusers better be staffs, not the spell-casting kind either.
if self.request.user.is_staff:
- tickets = (Ticket.objects.order_by('-id').filter(is_open=True).prefetch_related('linked_item')
- .select_related('user__user'))
- context['open_tickets'] = filter_visible_tickets(tickets, self.request.user)[:10]
+ tickets = (
+ Ticket.objects.order_by("-id")
+ .filter(is_open=True)
+ .prefetch_related("linked_item")
+ .select_related("user__user")
+ )
+ context["open_tickets"] = filter_visible_tickets(
+ tickets, self.request.user
+ )[:10]
else:
- context['open_tickets'] = []
+ context["open_tickets"] = []
return context
class PostView(TitleMixin, CommentedDetailView):
model = BlogPost
- pk_url_kwarg = 'id'
- context_object_name = 'post'
- template_name = 'blog/content.html'
+ pk_url_kwarg = "id"
+ context_object_name = "post"
+ template_name = "blog/content.html"
def get_title(self):
return self.object.title
def get_comment_page(self):
- return 'b:%s' % self.object.id
+ return "b:%s" % self.object.id
def get_context_data(self, **kwargs):
context = super(PostView, self).get_context_data(**kwargs)
- metadata = generate_opengraph('generated-meta-blog:%d' % self.object.id,
- self.object.summary or self.object.content, 'blog')
- context['meta_description'] = metadata[0]
- context['og_image'] = self.object.og_image or metadata[1]
+ metadata = generate_opengraph(
+ "generated-meta-blog:%d" % self.object.id,
+ self.object.summary or self.object.content,
+ "blog",
+ )
+ context["meta_description"] = metadata[0]
+ context["og_image"] = self.object.og_image or metadata[1]
return context
diff --git a/judge/views/comment.py b/judge/views/comment.py
index 7b2e086a7e..c55a084d87 100644
--- a/judge/views/comment.py
+++ b/judge/views/comment.py
@@ -4,8 +4,14 @@
from django.db import IntegrityError
from django.db.models import F
from django.forms.models import ModelForm
-from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseNotFound, \
- HttpResponseRedirect
+from django.http import (
+ Http404,
+ HttpResponse,
+ HttpResponseBadRequest,
+ HttpResponseForbidden,
+ HttpResponseNotFound,
+ HttpResponseRedirect,
+)
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext as _
from django.views.decorators.http import require_POST
@@ -18,40 +24,53 @@
from judge.utils.views import TitleMixin
from judge.widgets import MathJaxPagedownWidget
-__all__ = ['upvote_comment', 'downvote_comment', 'CommentEditAjax', 'CommentContent',
- 'CommentEdit']
+__all__ = [
+ "upvote_comment",
+ "downvote_comment",
+ "CommentEditAjax",
+ "CommentContent",
+ "CommentEdit",
+]
@login_required
def vote_comment(request, delta):
if abs(delta) != 1:
- return HttpResponseBadRequest(_('Messing around, are we?'), content_type='text/plain')
+ return HttpResponseBadRequest(
+ _("Messing around, are we?"), content_type="text/plain"
+ )
- if request.method != 'POST':
+ if request.method != "POST":
return HttpResponseForbidden()
- if 'id' not in request.POST or len(request.POST['id']) > 10:
+ if "id" not in request.POST or len(request.POST["id"]) > 10:
return HttpResponseBadRequest()
if not request.user.is_staff and not request.profile.has_any_solves:
- return HttpResponseBadRequest(_('You must solve at least one problem before you can vote.'),
- content_type='text/plain')
+ return HttpResponseBadRequest(
+ _("You must solve at least one problem before you can vote."),
+ content_type="text/plain",
+ )
if request.profile.mute:
- return HttpResponseBadRequest(_('Your part is silent, little toad.'), content_type='text/plain')
+ return HttpResponseBadRequest(
+ _("Your part is silent, little toad."), content_type="text/plain"
+ )
try:
- comment_id = int(request.POST['id'])
+ comment_id = int(request.POST["id"])
except ValueError:
return HttpResponseBadRequest()
comment = Comment.objects.filter(id=comment_id, hidden=False).first()
if not comment:
- return HttpResponseNotFound(_('Comment not found.'), content_type='text/plain')
+ return HttpResponseNotFound(_("Comment not found."), content_type="text/plain")
if comment.author == request.profile:
- return HttpResponseBadRequest(_('You cannot vote on your own comments.'), content_type='text/plain')
+ return HttpResponseBadRequest(
+ _("You cannot vote on your own comments."), content_type="text/plain"
+ )
vote = CommentVote()
vote.comment_id = comment_id
@@ -64,18 +83,22 @@ def vote_comment(request, delta):
except IntegrityError:
with LockModel(write=(CommentVote,)):
try:
- vote = CommentVote.objects.get(comment_id=comment_id, voter=request.profile)
+ vote = CommentVote.objects.get(
+ comment_id=comment_id, voter=request.profile
+ )
except CommentVote.DoesNotExist:
# We must continue racing in case this is exploited to manipulate votes.
continue
if -vote.score != delta:
- return HttpResponseBadRequest(_('You already voted.'), content_type='text/plain')
+ return HttpResponseBadRequest(
+ _("You already voted."), content_type="text/plain"
+ )
vote.delete()
- Comment.objects.filter(id=comment_id).update(score=F('score') - vote.score)
+ Comment.objects.filter(id=comment_id).update(score=F("score") - vote.score)
else:
- Comment.objects.filter(id=comment_id).update(score=F('score') + delta)
+ Comment.objects.filter(id=comment_id).update(score=F("score") + delta)
break
- return HttpResponse('success', content_type='text/plain')
+ return HttpResponse("success", content_type="text/plain")
def upvote_comment(request):
@@ -88,8 +111,8 @@ def downvote_comment(request):
class CommentMixin(object):
model = Comment
- pk_url_kwarg = 'id'
- context_object_name = 'comment'
+ pk_url_kwarg = "id"
+ context_object_name = "comment"
def get_object(self, queryset=None):
comment = super().get_object(queryset)
@@ -99,21 +122,23 @@ def get_object(self, queryset=None):
class CommentRevisionAjax(CommentMixin, DetailView):
- template_name = 'comments/revision-ajax.html'
+ template_name = "comments/revision-ajax.html"
def get_context_data(self, **kwargs):
context = super(CommentRevisionAjax, self).get_context_data(**kwargs)
- revisions = Version.objects.get_for_object(self.object).order_by('-revision')
+ revisions = Version.objects.get_for_object(self.object).order_by("-revision")
try:
- wanted = min(max(int(self.request.GET.get('revision', 0)), 0), len(revisions) - 1)
+ wanted = min(
+ max(int(self.request.GET.get("revision", 0)), 0), len(revisions) - 1
+ )
except ValueError:
raise Http404
- context['revision'] = revisions[wanted]
+ context["revision"] = revisions[wanted]
return context
def get_object(self, queryset=None):
comment = super(CommentRevisionAjax, self).get_object(queryset)
- if comment.hidden and not self.request.user.has_perm('judge.change_comment'):
+ if comment.hidden and not self.request.user.has_perm("judge.change_comment"):
raise Http404()
return comment
@@ -121,22 +146,24 @@ def get_object(self, queryset=None):
class CommentEditForm(ModelForm):
class Meta:
model = Comment
- fields = ['body']
+ fields = ["body"]
if MathJaxPagedownWidget is not None:
- widgets = {'body': MathJaxPagedownWidget(attrs={'id': 'id-edit-comment-body'})}
+ widgets = {
+ "body": MathJaxPagedownWidget(attrs={"id": "id-edit-comment-body"})
+ }
class CommentEditAjax(LoginRequiredMixin, CommentMixin, UpdateView):
- template_name = 'comments/edit-ajax.html'
+ template_name = "comments/edit-ajax.html"
form_class = CommentEditForm
def form_valid(self, form):
with revisions.create_revision(atomic=True):
- revisions.set_comment(_('Edited from site'))
+ revisions.set_comment(_("Edited from site"))
revisions.set_user(self.request.user)
self.object = comment = form.save(commit=False)
- comment.revisions = F('revisions') + 1
+ comment.revisions = F("revisions") + 1
comment.save()
return HttpResponseRedirect(self.get_success_url())
@@ -145,7 +172,7 @@ def get_success_url(self):
def get_object(self, queryset=None):
comment = super(CommentEditAjax, self).get_object(queryset)
- if self.request.user.has_perm('judge.change_comment'):
+ if self.request.user.has_perm("judge.change_comment"):
return comment
profile = self.request.profile
if profile != comment.author or profile.mute or comment.hidden:
@@ -154,36 +181,37 @@ def get_object(self, queryset=None):
class CommentEdit(TitleMixin, CommentEditAjax):
- template_name = 'comments/edit.html'
+ template_name = "comments/edit.html"
def get_title(self):
- return _('Editing comment')
+ return _("Editing comment")
class CommentContent(CommentMixin, DetailView):
- template_name = 'comments/content.html'
+ template_name = "comments/content.html"
class CommentVotesAjax(PermissionRequiredMixin, CommentMixin, DetailView):
- template_name = 'comments/votes.html'
- permission_required = 'judge.change_comment'
+ template_name = "comments/votes.html"
+ permission_required = "judge.change_comment"
def get_context_data(self, **kwargs):
context = super(CommentVotesAjax, self).get_context_data(**kwargs)
- context['votes'] = (self.object.votes.select_related('voter__user')
- .only('id', 'voter__display_rank', 'voter__user__username', 'score'))
+ context["votes"] = self.object.votes.select_related("voter__user").only(
+ "id", "voter__display_rank", "voter__user__username", "score"
+ )
return context
@require_POST
def comment_hide(request):
- if not request.user.has_perm('judge.change_comment'):
+ if not request.user.has_perm("judge.change_comment"):
raise PermissionDenied()
try:
- comment_id = int(request.POST['id'])
+ comment_id = int(request.POST["id"])
except ValueError:
return HttpResponseBadRequest()
comment = get_object_or_404(Comment, id=comment_id)
comment.get_descendants(include_self=True).update(hidden=True)
- return HttpResponse('ok')
+ return HttpResponse("ok")
diff --git a/judge/views/contests.py b/judge/views/contests.py
index 880e128f7e..cf5a40f4ae 100644
--- a/judge/views/contests.py
+++ b/judge/views/contests.py
@@ -11,9 +11,27 @@
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
from django.db import IntegrityError
-from django.db.models import BooleanField, Case, Count, F, FloatField, IntegerField, Max, Min, Q, Sum, Value, When
+from django.db.models import (
+ BooleanField,
+ Case,
+ Count,
+ F,
+ FloatField,
+ IntegerField,
+ Max,
+ Min,
+ Q,
+ Sum,
+ Value,
+ When,
+)
from django.db.models.expressions import CombinedExpression, Exists, OuterRef
-from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect
+from django.http import (
+ Http404,
+ HttpResponse,
+ HttpResponseBadRequest,
+ HttpResponseRedirect,
+)
from django.shortcuts import get_object_or_404, render
from django.template.defaultfilters import date as date_filter
from django.urls import reverse
@@ -32,21 +50,47 @@
from judge import event_poster as event
from judge.comments import CommentedDetailView
from judge.forms import ContestCloneForm
-from judge.models import Contest, ContestMoss, ContestParticipation, ContestProblem, ContestTag, \
- Problem, Profile, Submission
+from judge.models import (
+ Contest,
+ ContestMoss,
+ ContestParticipation,
+ ContestProblem,
+ ContestTag,
+ Problem,
+ Profile,
+ Submission,
+)
from judge.tasks import run_moss
from judge.utils.celery import redirect_to_task_status
from judge.utils.opengraph import generate_opengraph
from judge.utils.problems import _get_result_data
from judge.utils.ranker import ranker
from judge.utils.stats import get_bar_chart, get_pie_chart
-from judge.utils.views import DiggPaginatorMixin, QueryStringSortMixin, SingleObjectFormView, TitleMixin, \
- generic_message
+from judge.utils.views import (
+ DiggPaginatorMixin,
+ QueryStringSortMixin,
+ SingleObjectFormView,
+ TitleMixin,
+ generic_message,
+)
-__all__ = ['ContestList', 'ContestDetail', 'ContestRanking', 'ContestJoin', 'ContestLeave', 'ContestCalendar',
- 'ContestClone', 'ContestStats', 'ContestMossView', 'ContestMossDelete', 'contest_ranking_ajax',
- 'ContestParticipationList', 'ContestParticipationDisqualify', 'get_contest_ranking_list',
- 'base_contest_ranking_list']
+__all__ = [
+ "ContestList",
+ "ContestDetail",
+ "ContestRanking",
+ "ContestJoin",
+ "ContestLeave",
+ "ContestCalendar",
+ "ContestClone",
+ "ContestStats",
+ "ContestMossView",
+ "ContestMossDelete",
+ "contest_ranking_ajax",
+ "ContestParticipationList",
+ "ContestParticipationDisqualify",
+ "get_contest_ranking_list",
+ "base_contest_ranking_list",
+]
def _find_contest(request, key, private_check=True):
@@ -55,8 +99,15 @@ def _find_contest(request, key, private_check=True):
if private_check and not contest.is_accessible_by(request.user):
raise ObjectDoesNotExist()
except ObjectDoesNotExist:
- return generic_message(request, _('No such contest'),
- _('Could not find a contest with the key "%s".') % key, status=404), False
+ return (
+ generic_message(
+ request,
+ _("No such contest"),
+ _('Could not find a contest with the key "%s".') % key,
+ status=404,
+ ),
+ False,
+ )
return contest, True
@@ -65,29 +116,35 @@ def get_queryset(self):
return Contest.get_visible_contests(self.request.user)
-class ContestList(QueryStringSortMixin, DiggPaginatorMixin, TitleMixin, ContestListMixin, ListView):
+class ContestList(
+ QueryStringSortMixin, DiggPaginatorMixin, TitleMixin, ContestListMixin, ListView
+):
model = Contest
paginate_by = 20
- template_name = 'contest/list.html'
- title = gettext_lazy('Contests')
- context_object_name = 'past_contests'
- all_sorts = frozenset(('name', 'user_count', 'start_time'))
- default_desc = frozenset(('name', 'user_count'))
- default_sort = '-start_time'
+ template_name = "contest/list.html"
+ title = gettext_lazy("Contests")
+ context_object_name = "past_contests"
+ all_sorts = frozenset(("name", "user_count", "start_time"))
+ default_desc = frozenset(("name", "user_count"))
+ default_sort = "-start_time"
@cached_property
def _now(self):
return timezone.now()
def _get_queryset(self):
- queryset = super().get_queryset().prefetch_related(
- 'tags',
- 'organizations',
- 'authors',
- 'curators',
- 'testers',
- 'spectators',
- 'classes',
+ queryset = (
+ super()
+ .get_queryset()
+ .prefetch_related(
+ "tags",
+ "organizations",
+ "authors",
+ "curators",
+ "testers",
+ "spectators",
+ "classes",
+ )
)
profile = self.request.profile
@@ -95,19 +152,52 @@ def _get_queryset(self):
return queryset
return queryset.annotate(
- editor_or_tester=Exists(Contest.authors.through.objects.filter(contest=OuterRef('pk'), profile=profile))
- .bitor(Exists(Contest.curators.through.objects.filter(contest=OuterRef('pk'), profile=profile)))
- .bitor(Exists(Contest.testers.through.objects.filter(contest=OuterRef('pk'), profile=profile))),
- completed_contest=Exists(ContestParticipation.objects.filter(contest=OuterRef('pk'), user=profile,
- virtual=ContestParticipation.LIVE)),
+ editor_or_tester=Exists(
+ Contest.authors.through.objects.filter(
+ contest=OuterRef("pk"), profile=profile
+ )
+ )
+ .bitor(
+ Exists(
+ Contest.curators.through.objects.filter(
+ contest=OuterRef("pk"), profile=profile
+ )
+ )
+ )
+ .bitor(
+ Exists(
+ Contest.testers.through.objects.filter(
+ contest=OuterRef("pk"), profile=profile
+ )
+ )
+ ),
+ completed_contest=Exists(
+ ContestParticipation.objects.filter(
+ contest=OuterRef("pk"),
+ user=profile,
+ virtual=ContestParticipation.LIVE,
+ )
+ ),
)
def get_queryset(self):
- return self._get_queryset().order_by(self.order, 'key').filter(end_time__lt=self._now)
+ return (
+ self._get_queryset()
+ .order_by(self.order, "key")
+ .filter(end_time__lt=self._now)
+ )
- def get_paginator(self, queryset, per_page, orphans=0, allow_empty_first_page=True, **kwargs):
- return super().get_paginator(queryset, per_page, orphans, allow_empty_first_page,
- count=self.get_queryset().values('id').count(), **kwargs)
+ def get_paginator(
+ self, queryset, per_page, orphans=0, allow_empty_first_page=True, **kwargs
+ ):
+ return super().get_paginator(
+ queryset,
+ per_page,
+ orphans,
+ allow_empty_first_page,
+ count=self.get_queryset().values("id").count(),
+ **kwargs,
+ )
def get_context_data(self, **kwargs):
context = super(ContestList, self).get_context_data(**kwargs)
@@ -121,10 +211,17 @@ def get_context_data(self, **kwargs):
if self.request.user.is_authenticated:
for participation in (
- ContestParticipation.objects.filter(virtual=0, user=self.request.profile, contest_id__in=present)
- .select_related('contest')
- .prefetch_related('contest__authors', 'contest__curators', 'contest__testers', 'contest__spectators')
- .annotate(key=F('contest__key'))
+ ContestParticipation.objects.filter(
+ virtual=0, user=self.request.profile, contest_id__in=present
+ )
+ .select_related("contest")
+ .prefetch_related(
+ "contest__authors",
+ "contest__curators",
+ "contest__testers",
+ "contest__spectators",
+ )
+ .annotate(key=F("contest__key"))
):
if participation.ended:
finished.add(participation.contest.key)
@@ -132,16 +229,16 @@ def get_context_data(self, **kwargs):
active.append(participation)
present.remove(participation.contest)
- active.sort(key=attrgetter('end_time', 'key'))
- present.sort(key=attrgetter('end_time', 'key'))
- future.sort(key=attrgetter('start_time'))
- context['active_participations'] = active
- context['current_contests'] = present
- context['future_contests'] = future
- context['finished_contests'] = finished
- context['now'] = self._now
- context['first_page_href'] = '.'
- context['page_suffix'] = '#past-contests'
+ active.sort(key=attrgetter("end_time", "key"))
+ present.sort(key=attrgetter("end_time", "key"))
+ future.sort(key=attrgetter("start_time"))
+ context["active_participations"] = active
+ context["current_contests"] = present
+ context["future_contests"] = future
+ context["finished_contests"] = finished
+ context["now"] = self._now
+ context["first_page_href"] = "."
+ context["page_suffix"] = "#past-contests"
context.update(self.get_sort_context())
context.update(self.get_sort_paginate_context())
return context
@@ -157,10 +254,10 @@ def __init__(self, name, is_private, is_organization_private, orgs, classes):
class ContestMixin(object):
- context_object_name = 'contest'
+ context_object_name = "contest"
model = Contest
- slug_field = 'key'
- slug_url_kwarg = 'contest'
+ slug_field = "key"
+ slug_url_kwarg = "contest"
@cached_property
def is_editor(self):
@@ -188,36 +285,44 @@ def get_context_data(self, **kwargs):
context = super(ContestMixin, self).get_context_data(**kwargs)
if self.request.user.is_authenticated:
try:
- context['live_participation'] = (
- self.request.profile.contest_history.get(
- contest=self.object,
- virtual=ContestParticipation.LIVE,
- )
+ context[
+ "live_participation"
+ ] = self.request.profile.contest_history.get(
+ contest=self.object,
+ virtual=ContestParticipation.LIVE,
)
except ContestParticipation.DoesNotExist:
- context['live_participation'] = None
- context['has_joined'] = False
+ context["live_participation"] = None
+ context["has_joined"] = False
else:
- context['has_joined'] = True
+ context["has_joined"] = True
else:
- context['live_participation'] = None
- context['has_joined'] = False
+ context["live_participation"] = None
+ context["has_joined"] = False
- context['now'] = timezone.now()
- context['is_editor'] = self.is_editor
- context['is_tester'] = self.is_tester
- context['is_spectator'] = self.is_spectator
- context['can_edit'] = self.can_edit
+ context["now"] = timezone.now()
+ context["is_editor"] = self.is_editor
+ context["is_tester"] = self.is_tester
+ context["is_spectator"] = self.is_spectator
+ context["can_edit"] = self.can_edit
if not self.object.og_image or not self.object.summary:
- metadata = generate_opengraph('generated-meta-contest:%d' % self.object.id,
- self.object.description, 'contest')
- context['meta_description'] = self.object.summary or metadata[0]
- context['og_image'] = self.object.og_image or metadata[1]
- context['has_moss_api_key'] = settings.MOSS_API_KEY is not None
- context['logo_override_image'] = self.object.logo_override_image
- if not context['logo_override_image'] and self.object.organizations.count() == 1:
- context['logo_override_image'] = self.object.organizations.first().logo_override_image
+ metadata = generate_opengraph(
+ "generated-meta-contest:%d" % self.object.id,
+ self.object.description,
+ "contest",
+ )
+ context["meta_description"] = self.object.summary or metadata[0]
+ context["og_image"] = self.object.og_image or metadata[1]
+ context["has_moss_api_key"] = settings.MOSS_API_KEY is not None
+ context["logo_override_image"] = self.object.logo_override_image
+ if (
+ not context["logo_override_image"]
+ and self.object.organizations.count() == 1
+ ):
+ context[
+ "logo_override_image"
+ ] = self.object.organizations.first().logo_override_image
return context
@@ -225,15 +330,24 @@ def get_object(self, queryset=None):
contest = super(ContestMixin, self).get_object(queryset)
profile = self.request.profile
- if (profile is not None and
- ContestParticipation.objects.filter(id=profile.current_contest_id, contest_id=contest.id).exists()):
+ if (
+ profile is not None
+ and ContestParticipation.objects.filter(
+ id=profile.current_contest_id, contest_id=contest.id
+ ).exists()
+ ):
return contest
try:
contest.access_check(self.request.user)
except Contest.PrivateContest:
- raise PrivateContestError(contest.name, contest.is_private, contest.is_organization_private,
- contest.organizations.all(), contest.classes.all())
+ raise PrivateContestError(
+ contest.name,
+ contest.is_private,
+ contest.is_organization_private,
+ contest.organizations.all(),
+ contest.classes.all(),
+ )
except Contest.Inaccessible:
raise Http404()
else:
@@ -245,62 +359,84 @@ def dispatch(self, request, *args, **kwargs):
except Http404:
key = kwargs.get(self.slug_url_kwarg, None)
if key:
- return generic_message(request, _('No such contest'),
- _('Could not find a contest with the key "%s".') % key)
+ return generic_message(
+ request,
+ _("No such contest"),
+ _('Could not find a contest with the key "%s".') % key,
+ )
else:
- return generic_message(request, _('No such contest'),
- _('Could not find such contest.'))
+ return generic_message(
+ request, _("No such contest"), _("Could not find such contest.")
+ )
except PrivateContestError as e:
- return render(request, 'contest/private.html', {
- 'error': e, 'title': _('Access to contest "%s" denied') % e.name,
- }, status=403)
+ return render(
+ request,
+ "contest/private.html",
+ {
+ "error": e,
+ "title": _('Access to contest "%s" denied') % e.name,
+ },
+ status=403,
+ )
class ContestDetail(ContestMixin, TitleMixin, CommentedDetailView):
- template_name = 'contest/contest.html'
+ template_name = "contest/contest.html"
def get_comment_page(self):
- return 'c:%s' % self.object.key
+ return "c:%s" % self.object.key
def get_title(self):
return self.object.name
def get_context_data(self, **kwargs):
context = super(ContestDetail, self).get_context_data(**kwargs)
- context['contest_problems'] = Problem.objects.filter(contests__contest=self.object) \
- .order_by('contests__order').defer('description') \
- .annotate(has_public_editorial=Case(
- When(solution__is_public=True, solution__publish_on__lte=timezone.now(), then=True),
- default=False,
- output_field=BooleanField(),
- )) \
+ context["contest_problems"] = (
+ Problem.objects.filter(contests__contest=self.object)
+ .order_by("contests__order")
+ .defer("description")
+ .annotate(
+ has_public_editorial=Case(
+ When(
+ solution__is_public=True,
+ solution__publish_on__lte=timezone.now(),
+ then=True,
+ ),
+ default=False,
+ output_field=BooleanField(),
+ )
+ )
.add_i18n_name(self.request.LANGUAGE_CODE)
- context['metadata'] = {
- 'has_public_editorials': any(
- problem.is_public and problem.has_public_editorial for problem in context['contest_problems']
+ )
+ context["metadata"] = {
+ "has_public_editorials": any(
+ problem.is_public and problem.has_public_editorial
+ for problem in context["contest_problems"]
),
}
- context['metadata'].update(
- **self.object.contest_problems
- .annotate(
- partials_enabled=F('partial').bitand(F('problem__partial')),
- pretests_enabled=F('is_pretested').bitand(F('contest__run_pretests_only')),
- )
- .aggregate(
- has_partials=Sum('partials_enabled'),
- has_pretests=Sum('pretests_enabled'),
- has_submission_cap=Sum('max_submissions'),
- problem_count=Count('id'),
+ context["metadata"].update(
+ **self.object.contest_problems.annotate(
+ partials_enabled=F("partial").bitand(F("problem__partial")),
+ pretests_enabled=F("is_pretested").bitand(
+ F("contest__run_pretests_only")
+ ),
+ ).aggregate(
+ has_partials=Sum("partials_enabled"),
+ has_pretests=Sum("pretests_enabled"),
+ has_submission_cap=Sum("max_submissions"),
+ problem_count=Count("id"),
),
)
return context
-class ContestClone(ContestMixin, PermissionRequiredMixin, TitleMixin, SingleObjectFormView):
- title = gettext_lazy('Clone Contest')
- template_name = 'contest/clone.html'
+class ContestClone(
+ ContestMixin, PermissionRequiredMixin, TitleMixin, SingleObjectFormView
+):
+ title = gettext_lazy("Clone Contest")
+ template_name = "contest/clone.html"
form_class = ContestCloneForm
- permission_required = 'judge.clone_contest'
+ permission_required = "judge.clone_contest"
def form_valid(self, form):
contest = self.object
@@ -316,7 +452,7 @@ def form_valid(self, form):
contest.is_visible = False
contest.user_count = 0
contest.locked_after = None
- contest.key = form.cleaned_data['key']
+ contest.key = form.cleaned_data["key"]
with revisions.create_revision(atomic=True):
contest.save()
contest.tags.set(tags)
@@ -331,9 +467,11 @@ def form_valid(self, form):
ContestProblem.objects.bulk_create(contest_problems)
revisions.set_user(self.request.user)
- revisions.set_comment(_('Cloned contest from %s') % old_key)
+ revisions.set_comment(_("Cloned contest from %s") % old_key)
- return HttpResponseRedirect(reverse('admin:judge_contest_change', args=(contest.id,)))
+ return HttpResponseRedirect(
+ reverse("admin:judge_contest_change", args=(contest.id,))
+ )
class ContestAccessDenied(Exception):
@@ -345,7 +483,7 @@ class ContestAccessCodeForm(forms.Form):
def __init__(self, *args, **kwargs):
super(ContestAccessCodeForm, self).__init__(*args, **kwargs)
- self.fields['access_code'].widget.attrs.update({'autocomplete': 'off'})
+ self.fields["access_code"].widget.attrs.update({"autocomplete": "off"})
class ContestJoin(LoginRequiredMixin, ContestMixin, SingleObjectMixin, View):
@@ -358,7 +496,7 @@ def post(self, request, *args, **kwargs):
try:
return self.join_contest(request)
except ContestAccessDenied:
- if request.POST.get('access_code'):
+ if request.POST.get("access_code"):
return self.ask_for_access_code(ContestAccessCodeForm(request.POST))
else:
return HttpResponseRedirect(request.path)
@@ -367,27 +505,52 @@ def join_contest(self, request, access_code=None):
contest = self.object
if not contest.started and not (self.is_editor or self.is_tester):
- return generic_message(request, _('Contest not ongoing'),
- _('"%s" is not currently ongoing.') % contest.name)
+ return generic_message(
+ request,
+ _("Contest not ongoing"),
+ _('"%s" is not currently ongoing.') % contest.name,
+ )
profile = request.profile
- if not request.user.is_superuser and contest.banned_users.filter(id=profile.id).exists():
- return generic_message(request, _('Banned from joining'),
- _('You have been declared persona non grata for this contest. '
- 'You are permanently barred from joining this contest.'))
+ if (
+ not request.user.is_superuser
+ and contest.banned_users.filter(id=profile.id).exists()
+ ):
+ return generic_message(
+ request,
+ _("Banned from joining"),
+ _(
+ "You have been declared persona non grata for this contest. "
+ "You are permanently barred from joining this contest."
+ ),
+ )
- requires_access_code = (not self.can_edit and contest.access_code and access_code != contest.access_code)
+ requires_access_code = (
+ not self.can_edit
+ and contest.access_code
+ and access_code != contest.access_code
+ )
if contest.ended:
if requires_access_code:
raise ContestAccessDenied()
while True:
- virtual_id = max((ContestParticipation.objects.filter(contest=contest, user=profile)
- .aggregate(virtual_id=Max('virtual'))['virtual_id'] or 0) + 1, 1)
+ virtual_id = max(
+ (
+ ContestParticipation.objects.filter(
+ contest=contest, user=profile
+ ).aggregate(virtual_id=Max("virtual"))["virtual_id"]
+ or 0
+ )
+ + 1,
+ 1,
+ )
try:
participation = ContestParticipation.objects.create(
- contest=contest, user=profile, virtual=virtual_id,
+ contest=contest,
+ user=profile,
+ virtual=virtual_id,
real_start=timezone.now(),
)
# There is obviously a race condition here, so we keep trying until we win the race.
@@ -404,47 +567,63 @@ def join_contest(self, request, access_code=None):
elif contest.is_spectatable_by(request.user):
participation_type = SPECTATE
else:
- return generic_message(request, _('Cannot enter'),
- _('You are not able to join this contest.'))
+ return generic_message(
+ request,
+ _("Cannot enter"),
+ _("You are not able to join this contest."),
+ )
try:
participation = ContestParticipation.objects.get(
- contest=contest, user=profile, virtual=participation_type,
+ contest=contest,
+ user=profile,
+ virtual=participation_type,
)
except ContestParticipation.DoesNotExist:
if requires_access_code:
raise ContestAccessDenied()
participation = ContestParticipation.objects.create(
- contest=contest, user=profile, virtual=participation_type,
+ contest=contest,
+ user=profile,
+ virtual=participation_type,
real_start=timezone.now(),
)
else:
if participation.ended:
participation = ContestParticipation.objects.get_or_create(
- contest=contest, user=profile, virtual=SPECTATE,
- defaults={'real_start': timezone.now()},
+ contest=contest,
+ user=profile,
+ virtual=SPECTATE,
+ defaults={"real_start": timezone.now()},
)[0]
profile.current_contest = participation
profile.save()
contest._updating_stats_only = True
contest.update_user_count()
- return HttpResponseRedirect(reverse('problem_list'))
+ return HttpResponseRedirect(reverse("problem_list"))
def ask_for_access_code(self, form=None):
contest = self.object
wrong_code = False
if form:
if form.is_valid():
- if form.cleaned_data['access_code'] == contest.access_code:
- return self.join_contest(self.request, form.cleaned_data['access_code'])
+ if form.cleaned_data["access_code"] == contest.access_code:
+ return self.join_contest(
+ self.request, form.cleaned_data["access_code"]
+ )
wrong_code = True
else:
form = ContestAccessCodeForm()
- return render(self.request, 'contest/access_code.html', {
- 'form': form, 'wrong_code': wrong_code,
- 'title': _('Enter access code for "%s"') % contest.name,
- })
+ return render(
+ self.request,
+ "contest/access_code.html",
+ {
+ "form": form,
+ "wrong_code": wrong_code,
+ "title": _('Enter access code for "%s"') % contest.name,
+ },
+ )
class ContestLeave(LoginRequiredMixin, ContestMixin, SingleObjectMixin, View):
@@ -452,27 +631,36 @@ def post(self, request, *args, **kwargs):
contest = self.get_object()
profile = request.profile
- if profile.current_contest is None or profile.current_contest.contest_id != contest.id:
- return generic_message(request, _('No such contest'),
- _('You are not in contest "%s".') % contest.key, 404)
+ if (
+ profile.current_contest is None
+ or profile.current_contest.contest_id != contest.id
+ ):
+ return generic_message(
+ request,
+ _("No such contest"),
+ _('You are not in contest "%s".') % contest.key,
+ 404,
+ )
profile.remove_contest()
- return HttpResponseRedirect(reverse('contest_view', args=(contest.key,)))
+ return HttpResponseRedirect(reverse("contest_view", args=(contest.key,)))
-ContestDay = namedtuple('ContestDay', 'date is_pad is_today starts ends oneday')
+ContestDay = namedtuple("ContestDay", "date is_pad is_today starts ends oneday")
class ContestCalendar(TitleMixin, ContestListMixin, TemplateView):
firstweekday = SUNDAY
- template_name = 'contest/calendar.html'
+ template_name = "contest/calendar.html"
def get(self, request, *args, **kwargs):
try:
- self.year = int(kwargs['year'])
- self.month = int(kwargs['month'])
+ self.year = int(kwargs["year"])
+ self.month = int(kwargs["month"])
except (KeyError, ValueError):
- raise ImproperlyConfigured('ContestCalendar requires integer year and month')
+ raise ImproperlyConfigured(
+ "ContestCalendar requires integer year and month"
+ )
self.today = timezone.now().date()
return self.render()
@@ -482,12 +670,16 @@ def render(self):
def get_contest_data(self, start, end):
end += timedelta(days=1)
- contests = self.get_queryset().filter(Q(start_time__gte=start, start_time__lt=end) |
- Q(end_time__gte=start, end_time__lt=end))
+ contests = self.get_queryset().filter(
+ Q(start_time__gte=start, start_time__lt=end)
+ | Q(end_time__gte=start, end_time__lt=end)
+ )
starts, ends, oneday = (defaultdict(list) for i in range(3))
for contest in contests:
start_date = timezone.localtime(contest.start_time).date()
- end_date = timezone.localtime(contest.end_time - timedelta(seconds=1)).date()
+ end_date = timezone.localtime(
+ contest.end_time - timedelta(seconds=1)
+ ).date()
if start_date == end_date:
oneday[start_date].append(contest)
else:
@@ -497,12 +689,24 @@ def get_contest_data(self, start, end):
def get_table(self):
calendar = Calendar(self.firstweekday).monthdatescalendar(self.year, self.month)
- starts, ends, oneday = self.get_contest_data(make_aware(datetime.combine(calendar[0][0], time.min)),
- make_aware(datetime.combine(calendar[-1][-1], time.min)))
- return [[ContestDay(
- date=date, is_pad=date.month != self.month,
- is_today=date == self.today, starts=starts[date], ends=ends[date], oneday=oneday[date],
- ) for date in week] for week in calendar]
+ starts, ends, oneday = self.get_contest_data(
+ make_aware(datetime.combine(calendar[0][0], time.min)),
+ make_aware(datetime.combine(calendar[-1][-1], time.min)),
+ )
+ return [
+ [
+ ContestDay(
+ date=date,
+ is_pad=date.month != self.month,
+ is_today=date == self.today,
+ starts=starts[date],
+ ends=ends[date],
+ oneday=oneday[date],
+ )
+ for date in week
+ ]
+ for week in calendar
+ ]
def get_context_data(self, **kwargs):
context = super(ContestCalendar, self).get_context_data(**kwargs)
@@ -512,65 +716,80 @@ def get_context_data(self, **kwargs):
except ValueError:
raise Http404()
else:
- context['title'] = _('Contests in %(month)s') % {'month': date_filter(month, _('F Y'))}
+ context["title"] = _("Contests in %(month)s") % {
+ "month": date_filter(month, _("F Y"))
+ }
- dates = Contest.objects.aggregate(min=Min('start_time'), max=Max('end_time'))
+ dates = Contest.objects.aggregate(min=Min("start_time"), max=Max("end_time"))
min_month = (self.today.year, self.today.month)
- if dates['min'] is not None:
- min_month = dates['min'].year, dates['min'].month
+ if dates["min"] is not None:
+ min_month = dates["min"].year, dates["min"].month
max_month = (self.today.year, self.today.month)
- if dates['max'] is not None:
- max_month = max((dates['max'].year, dates['max'].month), (self.today.year, self.today.month))
+ if dates["max"] is not None:
+ max_month = max(
+ (dates["max"].year, dates["max"].month),
+ (self.today.year, self.today.month),
+ )
month = (self.year, self.month)
if month < min_month or month > max_month:
# 404 is valid because it merely declares the lack of existence, without any reason
raise Http404()
- context['now'] = timezone.now()
- context['calendar'] = self.get_table()
- context['curr_month'] = date(self.year, self.month, 1)
+ context["now"] = timezone.now()
+ context["calendar"] = self.get_table()
+ context["curr_month"] = date(self.year, self.month, 1)
if month > min_month:
- context['prev_month'] = date(self.year - (self.month == 1), 12 if self.month == 1 else self.month - 1, 1)
+ context["prev_month"] = date(
+ self.year - (self.month == 1),
+ 12 if self.month == 1 else self.month - 1,
+ 1,
+ )
else:
- context['prev_month'] = None
+ context["prev_month"] = None
if month < max_month:
- context['next_month'] = date(self.year + (self.month == 12), 1 if self.month == 12 else self.month + 1, 1)
+ context["next_month"] = date(
+ self.year + (self.month == 12),
+ 1 if self.month == 12 else self.month + 1,
+ 1,
+ )
else:
- context['next_month'] = None
+ context["next_month"] = None
return context
class ContestICal(TitleMixin, ContestListMixin, BaseListView):
def generate_ical(self):
cal = ICalendar()
- cal.add('prodid', '-//DMOJ//NONSGML Contests Calendar//')
- cal.add('version', '2.0')
+ cal.add("prodid", "-//DMOJ//NONSGML Contests Calendar//")
+ cal.add("version", "2.0")
now = timezone.now().astimezone(timezone.utc)
domain = self.request.get_host()
for contest in self.get_queryset():
event = Event()
- event.add('uid', f'contest-{contest.key}@{domain}')
- event.add('summary', contest.name)
- event.add('location', self.request.build_absolute_uri(contest.get_absolute_url()))
- event.add('dtstart', contest.start_time.astimezone(timezone.utc))
- event.add('dtend', contest.end_time.astimezone(timezone.utc))
- event.add('dtstamp', now)
+ event.add("uid", f"contest-{contest.key}@{domain}")
+ event.add("summary", contest.name)
+ event.add(
+ "location", self.request.build_absolute_uri(contest.get_absolute_url())
+ )
+ event.add("dtstart", contest.start_time.astimezone(timezone.utc))
+ event.add("dtend", contest.end_time.astimezone(timezone.utc))
+ event.add("dtstamp", now)
cal.add_component(event)
return cal.to_ical()
def render_to_response(self, context, **kwargs):
- return HttpResponse(self.generate_ical(), content_type='text/calendar')
+ return HttpResponse(self.generate_ical(), content_type="text/calendar")
class ContestStats(TitleMixin, ContestMixin, DetailView):
- template_name = 'contest/stats.html'
+ template_name = "contest/stats.html"
def get_title(self):
- return _('%s Statistics') % self.object.name
+ return _("%s Statistics") % self.object.name
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@@ -580,15 +799,22 @@ def get_context_data(self, **kwargs):
queryset = Submission.objects.filter(contest_object=self.object)
- ac_count = Count(Case(When(result='AC', then=Value(1)), output_field=IntegerField()))
- ac_rate = CombinedExpression(ac_count / Count('problem'), '*', Value(100.0), output_field=FloatField())
+ ac_count = Count(
+ Case(When(result="AC", then=Value(1)), output_field=IntegerField())
+ )
+ ac_rate = CombinedExpression(
+ ac_count / Count("problem"), "*", Value(100.0), output_field=FloatField()
+ )
status_count_queryset = list(
- queryset.values('problem__code', 'result').annotate(count=Count('result'))
- .values_list('problem__code', 'result', 'count'),
+ queryset.values("problem__code", "result")
+ .annotate(count=Count("result"))
+ .values_list("problem__code", "result", "count"),
)
labels, codes = [], []
- contest_problems = self.object.contest_problems.order_by('order').values_list('problem__name', 'problem__code')
+ contest_problems = self.object.contest_problems.order_by("order").values_list(
+ "problem__name", "problem__code"
+ )
if contest_problems:
labels, codes = zip(*contest_problems)
num_problems = len(labels)
@@ -599,47 +825,58 @@ def get_context_data(self, **kwargs):
result_data = defaultdict(partial(list, [0] * num_problems))
for i in range(num_problems):
- for category in _get_result_data(defaultdict(int, status_counts[i]))['categories']:
- result_data[category['code']][i] = category['count']
+ for category in _get_result_data(defaultdict(int, status_counts[i]))[
+ "categories"
+ ]:
+ result_data[category["code"]][i] = category["count"]
stats = {
- 'problem_status_count': {
- 'labels': labels,
- 'datasets': [
+ "problem_status_count": {
+ "labels": labels,
+ "datasets": [
{
- 'label': name,
- 'backgroundColor': settings.DMOJ_STATS_SUBMISSION_RESULT_COLORS[name],
- 'data': data,
+ "label": name,
+ "backgroundColor": settings.DMOJ_STATS_SUBMISSION_RESULT_COLORS[
+ name
+ ],
+ "data": data,
}
for name, data in result_data.items()
],
},
- 'problem_ac_rate': get_bar_chart(
- queryset.values('contest__problem__order', 'problem__name').annotate(ac_rate=ac_rate)
- .order_by('contest__problem__order').values_list('problem__name', 'ac_rate'),
+ "problem_ac_rate": get_bar_chart(
+ queryset.values("contest__problem__order", "problem__name")
+ .annotate(ac_rate=ac_rate)
+ .order_by("contest__problem__order")
+ .values_list("problem__name", "ac_rate"),
),
- 'language_count': get_pie_chart(
- queryset.values('language__name').annotate(count=Count('language__name'))
- .filter(count__gt=0).order_by('-count').values_list('language__name', 'count'),
+ "language_count": get_pie_chart(
+ queryset.values("language__name")
+ .annotate(count=Count("language__name"))
+ .filter(count__gt=0)
+ .order_by("-count")
+ .values_list("language__name", "count"),
),
- 'language_ac_rate': get_bar_chart(
- queryset.values('language__name').annotate(ac_rate=ac_rate)
- .filter(ac_rate__gt=0).values_list('language__name', 'ac_rate'),
+ "language_ac_rate": get_bar_chart(
+ queryset.values("language__name")
+ .annotate(ac_rate=ac_rate)
+ .filter(ac_rate__gt=0)
+ .values_list("language__name", "ac_rate"),
),
}
- context['stats'] = mark_safe(json.dumps(stats))
+ context["stats"] = mark_safe(json.dumps(stats))
return context
ContestRankingProfile = namedtuple(
- 'ContestRankingProfile',
- 'id user css_class username points cumtime tiebreaker organization participation '
- 'participation_rating problem_cells result_cell display_name',
+ "ContestRankingProfile",
+ "id user css_class username points cumtime tiebreaker organization participation "
+ "participation_rating problem_cells result_cell display_name",
)
-BestSolutionData = namedtuple('BestSolutionData', 'code points time state is_pretested')
+BestSolutionData = namedtuple("BestSolutionData", "code points time state is_pretested")
def make_contest_ranking_profile(contest, participation, contest_problems):
@@ -649,7 +886,7 @@ def display_user_problem(contest_problem):
try:
return contest.format.display_user_problem(participation, contest_problem)
except (KeyError, TypeError, ValueError):
- return mark_safe('??? ')
+ return mark_safe("??? ")
user = participation.user
return ContestRankingProfile(
@@ -661,8 +898,13 @@ def display_user_problem(contest_problem):
cumtime=participation.cumtime,
tiebreaker=participation.tiebreaker,
organization=user.organization,
- participation_rating=participation.rating.rating if hasattr(participation, 'rating') else None,
- problem_cells=[display_user_problem(contest_problem) for contest_problem in contest_problems],
+ participation_rating=participation.rating.rating
+ if hasattr(participation, "rating")
+ else None,
+ problem_cells=[
+ display_user_problem(contest_problem)
+ for contest_problem in contest_problems
+ ],
result_cell=contest.format.display_participation_result(participation),
participation=participation,
display_name=user.display_name,
@@ -670,22 +912,45 @@ def display_user_problem(contest_problem):
def base_contest_ranking_list(contest, problems, queryset):
- return [make_contest_ranking_profile(contest, participation, problems) for participation in
- queryset.select_related('user__user', 'rating').defer('user__about', 'user__organizations__about')]
+ return [
+ make_contest_ranking_profile(contest, participation, problems)
+ for participation in queryset.select_related("user__user", "rating").defer(
+ "user__about", "user__organizations__about"
+ )
+ ]
def contest_ranking_list(contest, problems):
- return base_contest_ranking_list(contest, problems, contest.users.filter(virtual=0)
- .prefetch_related('user__organizations')
- .annotate(submission_cnt=Count('submission'))
- .order_by('is_disqualified', '-score', 'cumtime', 'tiebreaker', '-submission_cnt'))
+ return base_contest_ranking_list(
+ contest,
+ problems,
+ contest.users.filter(virtual=0)
+ .prefetch_related("user__organizations")
+ .annotate(submission_cnt=Count("submission"))
+ .order_by(
+ "is_disqualified", "-score", "cumtime", "tiebreaker", "-submission_cnt"
+ ),
+ )
-def get_contest_ranking_list(request, contest, participation=None, ranking_list=contest_ranking_list,
- show_current_virtual=True, ranker=ranker):
- problems = list(contest.contest_problems.select_related('problem').defer('problem__description').order_by('order'))
+def get_contest_ranking_list(
+ request,
+ contest,
+ participation=None,
+ ranking_list=contest_ranking_list,
+ show_current_virtual=True,
+ ranker=ranker,
+):
+ problems = list(
+ contest.contest_problems.select_related("problem")
+ .defer("problem__description")
+ .order_by("order")
+ )
- users = ranker(ranking_list(contest, problems), key=attrgetter('points', 'cumtime', 'tiebreaker'))
+ users = ranker(
+ ranking_list(contest, problems),
+ key=attrgetter("points", "cumtime", "tiebreaker"),
+ )
if show_current_virtual:
if participation is None and request.user.is_authenticated:
@@ -693,29 +958,36 @@ def get_contest_ranking_list(request, contest, participation=None, ranking_list=
if participation is None or participation.contest_id != contest.id:
participation = None
if participation is not None and participation.virtual:
- users = chain([('-', make_contest_ranking_profile(contest, participation, problems))], users)
+ users = chain(
+ [("-", make_contest_ranking_profile(contest, participation, problems))],
+ users,
+ )
return users, problems
def contest_ranking_ajax(request, contest, participation=None):
contest, exists = _find_contest(request, contest)
if not exists:
- return HttpResponseBadRequest('Invalid contest', content_type='text/plain')
+ return HttpResponseBadRequest("Invalid contest", content_type="text/plain")
if not contest.can_see_full_scoreboard(request.user):
raise Http404()
users, problems = get_contest_ranking_list(request, contest, participation)
- return render(request, 'contest/ranking-table.html', {
- 'users': users,
- 'problems': problems,
- 'contest': contest,
- 'has_rating': contest.ratings.exists(),
- })
+ return render(
+ request,
+ "contest/ranking-table.html",
+ {
+ "users": users,
+ "problems": problems,
+ "contest": contest,
+ "has_rating": contest.ratings.exists(),
+ },
+ )
class ContestRankingBase(ContestMixin, TitleMixin, DetailView):
- template_name = 'contest/ranking.html'
+ template_name = "contest/ranking.html"
tab = None
def get_title(self):
@@ -734,69 +1006,89 @@ def get_context_data(self, **kwargs):
raise Http404()
users, problems = self.get_ranking_list()
- context['users'] = users
- context['problems'] = problems
- context['last_msg'] = event.last()
- context['tab'] = self.tab
+ context["users"] = users
+ context["problems"] = problems
+ context["last_msg"] = event.last()
+ context["tab"] = self.tab
return context
class ContestRanking(ContestRankingBase):
- tab = 'ranking'
+ tab = "ranking"
def get_title(self):
- return _('%s Rankings') % self.object.name
+ return _("%s Rankings") % self.object.name
def get_ranking_list(self):
if not self.object.can_see_full_scoreboard(self.request.user):
- queryset = self.object.users.filter(user=self.request.profile, virtual=ContestParticipation.LIVE)
+ queryset = self.object.users.filter(
+ user=self.request.profile, virtual=ContestParticipation.LIVE
+ )
return get_contest_ranking_list(
- self.request, self.object,
+ self.request,
+ self.object,
ranking_list=partial(base_contest_ranking_list, queryset=queryset),
- ranker=lambda users, key: ((_('???'), user) for user in users),
+ ranker=lambda users, key: ((_("???"), user) for user in users),
)
return get_contest_ranking_list(self.request, self.object)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
- context['has_rating'] = self.object.ratings.exists()
+ context["has_rating"] = self.object.ratings.exists()
return context
class ContestParticipationList(LoginRequiredMixin, ContestRankingBase):
- tab = 'participation'
+ tab = "participation"
def get_title(self):
if self.profile == self.request.profile:
- return _('Your participation in %(contest)s') % {'contest': self.object.name}
+ return _("Your participation in %(contest)s") % {
+ "contest": self.object.name
+ }
return _("%(user)s's participation in %(contest)s") % {
- 'user': self.profile.username, 'contest': self.object.name,
+ "user": self.profile.username,
+ "contest": self.object.name,
}
def get_ranking_list(self):
- if not self.object.can_see_full_scoreboard(self.request.user) and self.profile != self.request.profile:
+ if (
+ not self.object.can_see_full_scoreboard(self.request.user)
+ and self.profile != self.request.profile
+ ):
raise Http404()
- queryset = self.object.users.filter(user=self.profile, virtual__gte=0).order_by('-virtual')
- live_link = format_html('{0} ', _('Live'), self.profile.username,
- reverse('contest_ranking', args=[self.object.key]))
+ queryset = self.object.users.filter(user=self.profile, virtual__gte=0).order_by(
+ "-virtual"
+ )
+ live_link = format_html(
+ '{0} ',
+ _("Live"),
+ self.profile.username,
+ reverse("contest_ranking", args=[self.object.key]),
+ )
return get_contest_ranking_list(
- self.request, self.object, show_current_virtual=False,
+ self.request,
+ self.object,
+ show_current_virtual=False,
ranking_list=partial(base_contest_ranking_list, queryset=queryset),
- ranker=lambda users, key: ((user.participation.virtual or live_link, user) for user in users))
+ ranker=lambda users, key: (
+ (user.participation.virtual or live_link, user) for user in users
+ ),
+ )
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
- context['has_rating'] = False
- context['now'] = timezone.now()
- context['rank_header'] = _('Participation')
+ context["has_rating"] = False
+ context["now"] = timezone.now()
+ context["rank_header"] = _("Participation")
return context
def get(self, request, *args, **kwargs):
- if 'user' in kwargs:
- self.profile = get_object_or_404(Profile, user__username=kwargs['user'])
+ if "user" in kwargs:
+ self.profile = get_object_or_404(Profile, user__username=kwargs["user"])
else:
self.profile = self.request.profile
return super().get(request, *args, **kwargs)
@@ -813,35 +1105,43 @@ def post(self, request, *args, **kwargs):
self.object = self.get_object()
try:
- participation = self.object.users.get(pk=request.POST.get('participation'))
+ participation = self.object.users.get(pk=request.POST.get("participation"))
except ObjectDoesNotExist:
pass
else:
participation.set_disqualified(not participation.is_disqualified)
- return HttpResponseRedirect(reverse('contest_ranking', args=(self.object.key,)))
+ return HttpResponseRedirect(reverse("contest_ranking", args=(self.object.key,)))
class ContestMossMixin(ContestMixin, PermissionRequiredMixin):
- permission_required = 'judge.moss_contest'
+ permission_required = "judge.moss_contest"
def get_object(self, queryset=None):
contest = super().get_object(queryset)
- if settings.MOSS_API_KEY is None or not contest.is_editable_by(self.request.user):
+ if settings.MOSS_API_KEY is None or not contest.is_editable_by(
+ self.request.user
+ ):
raise Http404()
return contest
class ContestMossView(ContestMossMixin, TitleMixin, DetailView):
- template_name = 'contest/moss.html'
+ template_name = "contest/moss.html"
def get_title(self):
- return _('%s MOSS Results') % self.object.name
+ return _("%s MOSS Results") % self.object.name
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
- problems = list(map(attrgetter('problem'), self.object.contest_problems.order_by('order')
- .select_related('problem')))
+ problems = list(
+ map(
+ attrgetter("problem"),
+ self.object.contest_problems.order_by("order").select_related(
+ "problem"
+ ),
+ )
+ )
languages = list(map(itemgetter(0), ContestMoss.LANG_MAPPING))
results = ContestMoss.objects.filter(contest=self.object)
@@ -852,9 +1152,11 @@ def get_context_data(self, **kwargs):
for result_list in moss_results.values():
result_list.sort(key=lambda x: languages.index(x.language))
- context['languages'] = languages
- context['has_results'] = results.exists()
- context['moss_results'] = [(problem, moss_results[problem]) for problem in problems]
+ context["languages"] = languages
+ context["has_results"] = results.exists()
+ context["moss_results"] = [
+ (problem, moss_results[problem]) for problem in problems
+ ]
return context
@@ -862,8 +1164,9 @@ def post(self, request, *args, **kwargs):
self.object = self.get_object()
status = run_moss.delay(self.object.key)
return redirect_to_task_status(
- status, message=_('Running MOSS for %s...') % (self.object.name,),
- redirect=reverse('contest_moss', args=(self.object.key,)),
+ status,
+ message=_("Running MOSS for %s...") % (self.object.name,),
+ redirect=reverse("contest_moss", args=(self.object.key,)),
)
@@ -871,18 +1174,18 @@ class ContestMossDelete(ContestMossMixin, SingleObjectMixin, View):
def post(self, request, *args, **kwargs):
self.object = self.get_object()
ContestMoss.objects.filter(contest=self.object).delete()
- return HttpResponseRedirect(reverse('contest_moss', args=(self.object.key,)))
+ return HttpResponseRedirect(reverse("contest_moss", args=(self.object.key,)))
class ContestTagDetailAjax(DetailView):
model = ContestTag
- slug_field = slug_url_kwarg = 'name'
- context_object_name = 'tag'
- template_name = 'contest/tag-ajax.html'
+ slug_field = slug_url_kwarg = "name"
+ context_object_name = "tag"
+ template_name = "contest/tag-ajax.html"
class ContestTagDetail(TitleMixin, ContestTagDetailAjax):
- template_name = 'contest/tag.html'
+ template_name = "contest/tag.html"
def get_title(self):
- return _('Contest tag: %s') % self.object.name
+ return _("Contest tag: %s") % self.object.name
diff --git a/judge/views/error.py b/judge/views/error.py
index 113422d74e..cffed7edda 100644
--- a/judge/views/error.py
+++ b/judge/views/error.py
@@ -5,29 +5,42 @@
def error(request, context, status):
- return render(request, 'error.html', context=context, status=status)
+ return render(request, "error.html", context=context, status=status)
def error404(request, exception=None):
# TODO: "panic: go back"
- return render(request, 'generic-message.html', {
- 'title': _('404 error'),
- 'message': _('Could not find page "%s"') % request.path,
- }, status=404)
+ return render(
+ request,
+ "generic-message.html",
+ {
+ "title": _("404 error"),
+ "message": _('Could not find page "%s"') % request.path,
+ },
+ status=404,
+ )
def error403(request, exception=None):
- return error(request, {
- 'id': 'unauthorized_access',
- 'description': _('no permission for %s') % request.path,
- 'code': 403,
- }, 403)
+ return error(
+ request,
+ {
+ "id": "unauthorized_access",
+ "description": _("no permission for %s") % request.path,
+ "code": 403,
+ },
+ 403,
+ )
def error500(request):
- return error(request, {
- 'id': 'invalid_state',
- 'description': _('corrupt page %s') % request.path,
- 'traceback': traceback.format_exc(),
- 'code': 500,
- }, 500)
+ return error(
+ request,
+ {
+ "id": "invalid_state",
+ "description": _("corrupt page %s") % request.path,
+ "traceback": traceback.format_exc(),
+ "code": 500,
+ },
+ 500,
+ )
diff --git a/judge/views/language.py b/judge/views/language.py
index 87ab8cb84e..bbdc4bdc50 100644
--- a/judge/views/language.py
+++ b/judge/views/language.py
@@ -7,12 +7,12 @@
class LanguageList(TitleMixin, ListView):
model = Language
- context_object_name = 'languages'
- template_name = 'status/language-list.html'
- title = gettext_lazy('Runtimes')
+ context_object_name = "languages"
+ template_name = "status/language-list.html"
+ title = gettext_lazy("Runtimes")
def get_queryset(self):
- queryset = super().get_queryset().prefetch_related('runtimeversion_set')
+ queryset = super().get_queryset().prefetch_related("runtimeversion_set")
if not self.request.user.is_superuser and not self.request.user.is_staff:
queryset = queryset.filter(judges__online=True).distinct()
return queryset
diff --git a/judge/views/license.py b/judge/views/license.py
index 77208e67fa..0e208b7702 100644
--- a/judge/views/license.py
+++ b/judge/views/license.py
@@ -6,9 +6,9 @@
class LicenseDetail(TitleMixin, DetailView):
model = License
- slug_field = slug_url_kwarg = 'key'
- context_object_name = 'license'
- template_name = 'license.html'
+ slug_field = slug_url_kwarg = "key"
+ context_object_name = "license"
+ template_name = "license.html"
def get_title(self):
return self.object.name
diff --git a/judge/views/mailgun.py b/judge/views/mailgun.py
index 3bd8321125..f28589ad28 100644
--- a/judge/views/mailgun.py
+++ b/judge/views/mailgun.py
@@ -15,49 +15,77 @@
from judge.utils.unicode import utf8bytes
-logger = logging.getLogger('judge.mail.activate')
+logger = logging.getLogger("judge.mail.activate")
class MailgunActivationView(View):
- if hasattr(settings, 'MAILGUN_ACCESS_KEY'):
+ if hasattr(settings, "MAILGUN_ACCESS_KEY"):
+
def post(self, request, *args, **kwargs):
params = request.POST
- timestamp = params.get('timestamp', '')
- token = params.get('token', '')
- signature = params.get('signature', '')
+ timestamp = params.get("timestamp", "")
+ token = params.get("token", "")
+ signature = params.get("signature", "")
- logger.debug('Received request: %s', params)
+ logger.debug("Received request: %s", params)
- if signature != hmac.new(key=utf8bytes(settings.MAILGUN_ACCESS_KEY),
- msg=utf8bytes('%s%s' % (timestamp, token)), digestmod=hashlib.sha256).hexdigest():
- logger.info('Rejected request: signature: %s, timestamp: %s, token: %s', signature, timestamp, token)
+ if (
+ signature
+ != hmac.new(
+ key=utf8bytes(settings.MAILGUN_ACCESS_KEY),
+ msg=utf8bytes("%s%s" % (timestamp, token)),
+ digestmod=hashlib.sha256,
+ ).hexdigest()
+ ):
+ logger.info(
+ "Rejected request: signature: %s, timestamp: %s, token: %s",
+ signature,
+ timestamp,
+ token,
+ )
raise PermissionDenied()
- _, sender = parseaddr(params.get('from'))
+ _, sender = parseaddr(params.get("from"))
if not sender:
- logger.info('Rejected invalid sender: %s', params.get('from'))
+ logger.info("Rejected invalid sender: %s", params.get("from"))
return HttpResponse(status=406)
try:
user = User.objects.get(email__iexact=sender)
except (User.DoesNotExist, User.MultipleObjectsReturned):
- logger.info('Rejected unknown sender: %s: %s', sender, params.get('from'))
+ logger.info(
+ "Rejected unknown sender: %s: %s", sender, params.get("from")
+ )
return HttpResponse(status=406)
try:
registration = RegistrationProfile.objects.get(user=user)
except RegistrationProfile.DoesNotExist:
- logger.info('Rejected sender without RegistrationProfile: %s: %s', sender, params.get('from'))
+ logger.info(
+ "Rejected sender without RegistrationProfile: %s: %s",
+ sender,
+ params.get("from"),
+ )
return HttpResponse(status=406)
if registration.activated:
- logger.info('Rejected activated sender: %s: %s', sender, params.get('from'))
+ logger.info(
+ "Rejected activated sender: %s: %s", sender, params.get("from")
+ )
return HttpResponse(status=406)
key = registration.activation_key
- if key in params.get('body-plain', '') or key in params.get('body-html', ''):
- if RegistrationProfile.objects.activate_user(key, get_current_site(request)):
- logger.info('Activated sender: %s: %s', sender, params.get('from'))
- return HttpResponse('Activated', status=200)
- logger.info('Failed to activate sender: %s: %s', sender, params.get('from'))
+ if key in params.get("body-plain", "") or key in params.get(
+ "body-html", ""
+ ):
+ if RegistrationProfile.objects.activate_user(
+ key, get_current_site(request)
+ ):
+ logger.info("Activated sender: %s: %s", sender, params.get("from"))
+ return HttpResponse("Activated", status=200)
+ logger.info(
+ "Failed to activate sender: %s: %s", sender, params.get("from")
+ )
else:
- logger.info('Activation key not found: %s: %s', sender, params.get('from'))
+ logger.info(
+ "Activation key not found: %s: %s", sender, params.get("from")
+ )
return HttpResponse(status=406)
@method_decorator(csrf_exempt)
diff --git a/judge/views/organization.py b/judge/views/organization.py
index dcf0a50746..66c96f99b1 100644
--- a/judge/views/organization.py
+++ b/judge/views/organization.py
@@ -16,32 +16,56 @@
from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _, gettext_lazy, ngettext
from django.views.generic import DetailView, FormView, ListView, UpdateView, View
-from django.views.generic.detail import SingleObjectMixin, SingleObjectTemplateResponseMixin
+from django.views.generic.detail import (
+ SingleObjectMixin,
+ SingleObjectTemplateResponseMixin,
+)
from reversion import revisions
from judge.forms import EditOrganizationForm
from judge.models import Class, Organization, OrganizationRequest, Profile
from judge.utils.ranker import ranker
-from judge.utils.views import DiggPaginatorMixin, QueryStringSortMixin, TitleMixin, generic_message
-
-__all__ = ['OrganizationList', 'OrganizationHome', 'OrganizationUsers', 'OrganizationMembershipChange',
- 'JoinOrganization', 'LeaveOrganization', 'EditOrganization', 'RequestJoinOrganization',
- 'OrganizationRequestDetail', 'OrganizationRequestView', 'OrganizationRequestLog',
- 'KickUserWidgetView', 'ClassHome', 'RequestJoinClass']
+from judge.utils.views import (
+ DiggPaginatorMixin,
+ QueryStringSortMixin,
+ TitleMixin,
+ generic_message,
+)
+
+__all__ = [
+ "OrganizationList",
+ "OrganizationHome",
+ "OrganizationUsers",
+ "OrganizationMembershipChange",
+ "JoinOrganization",
+ "LeaveOrganization",
+ "EditOrganization",
+ "RequestJoinOrganization",
+ "OrganizationRequestDetail",
+ "OrganizationRequestView",
+ "OrganizationRequestLog",
+ "KickUserWidgetView",
+ "ClassHome",
+ "RequestJoinClass",
+]
def users_for_template(users, order):
- return ranker(users.filter(is_unlisted=False).order_by(order)
- .select_related('user').defer('about', 'user_script', 'notes'))
+ return ranker(
+ users.filter(is_unlisted=False)
+ .order_by(order)
+ .select_related("user")
+ .defer("about", "user_script", "notes")
+ )
class OrganizationMixin(object):
- context_object_name = 'organization'
+ context_object_name = "organization"
model = Organization
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
- context['logo_override_image'] = self.object.logo_override_image
+ context["logo_override_image"] = self.object.logo_override_image
return context
def dispatch(self, request, *args, **kwargs):
@@ -50,11 +74,17 @@ def dispatch(self, request, *args, **kwargs):
except Http404:
key = kwargs.get(self.slug_url_kwarg, None)
if key:
- return generic_message(request, _('No such organization'),
- _('Could not find an organization with the key "%s".') % key)
+ return generic_message(
+ request,
+ _("No such organization"),
+ _('Could not find an organization with the key "%s".') % key,
+ )
else:
- return generic_message(request, _('No such organization'),
- _('Could not find such organization.'))
+ return generic_message(
+ request,
+ _("No such organization"),
+ _("Could not find such organization."),
+ )
def can_edit_organization(self, org=None):
if org is None:
@@ -68,10 +98,10 @@ def can_edit_organization(self, org=None):
class BaseOrganizationListView(OrganizationMixin, ListView):
model = None
context_object_name = None
- slug_url_kwarg = 'slug'
+ slug_url_kwarg = "slug"
def get_object(self):
- return get_object_or_404(Organization, id=self.kwargs.get('pk'))
+ return get_object_or_404(Organization, id=self.kwargs.get("pk"))
def get_context_data(self, **kwargs):
return super().get_context_data(organization=self.object, **kwargs)
@@ -84,72 +114,97 @@ def get(self, request, *args, **kwargs):
class OrganizationDetailView(OrganizationMixin, DetailView):
def get(self, request, *args, **kwargs):
self.object = self.get_object()
- if self.object.slug != kwargs['slug']:
- return HttpResponsePermanentRedirect(reverse(
- request.resolver_match.url_name, args=(self.object.id, self.object.slug)))
+ if self.object.slug != kwargs["slug"]:
+ return HttpResponsePermanentRedirect(
+ reverse(
+ request.resolver_match.url_name,
+ args=(self.object.id, self.object.slug),
+ )
+ )
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
class OrganizationList(TitleMixin, ListView):
model = Organization
- context_object_name = 'organizations'
- template_name = 'organization/list.html'
- title = gettext_lazy('Organizations')
+ context_object_name = "organizations"
+ template_name = "organization/list.html"
+ title = gettext_lazy("Organizations")
def get_queryset(self):
- return super(OrganizationList, self).get_queryset().annotate(member_count=Count('member')).order_by('name')
+ return (
+ super(OrganizationList, self)
+ .get_queryset()
+ .annotate(member_count=Count("member"))
+ .order_by("name")
+ )
class OrganizationHome(OrganizationDetailView):
- template_name = 'organization/home.html'
+ template_name = "organization/home.html"
def get_context_data(self, **kwargs):
context = super(OrganizationHome, self).get_context_data(**kwargs)
- context['title'] = self.object.name
- context['can_edit'] = self.can_edit_organization()
- context['can_review_requests'] = not self.object.is_open and self.request.user.is_authenticated and (
- self.object.can_review_all_requests(self.request.profile) or
- self.object.can_review_class_requests(self.request.profile)
+ context["title"] = self.object.name
+ context["can_edit"] = self.can_edit_organization()
+ context["can_review_requests"] = (
+ not self.object.is_open
+ and self.request.user.is_authenticated
+ and (
+ self.object.can_review_all_requests(self.request.profile)
+ or self.object.can_review_class_requests(self.request.profile)
+ )
)
classes = self.object.classes.filter(is_active=True)
if self.request.user.is_authenticated:
- classes = classes.annotate(joined=Subquery(
- self.request.profile.classes.filter(id=OuterRef('id')).values('id'),
- )).order_by('-joined', 'name')
+ classes = classes.annotate(
+ joined=Subquery(
+ self.request.profile.classes.filter(id=OuterRef("id")).values("id"),
+ )
+ ).order_by("-joined", "name")
else:
classes = classes.annotate(joined=Value(0, output_field=IntegerField()))
- context['classes'] = classes
+ context["classes"] = classes
return context
-class OrganizationUsers(QueryStringSortMixin, DiggPaginatorMixin, BaseOrganizationListView):
- template_name = 'organization/users.html'
- all_sorts = frozenset(('problem_count', 'rating', 'performance_points'))
+class OrganizationUsers(
+ QueryStringSortMixin, DiggPaginatorMixin, BaseOrganizationListView
+):
+ template_name = "organization/users.html"
+ all_sorts = frozenset(("problem_count", "rating", "performance_points"))
default_desc = all_sorts
- default_sort = '-performance_points'
+ default_sort = "-performance_points"
paginate_by = 100
- context_object_name = 'users'
+ context_object_name = "users"
def get_queryset(self):
- return self.object.members.filter(is_unlisted=False).order_by(self.order).select_related('user') \
- .defer('about', 'user_script', 'notes')
+ return (
+ self.object.members.filter(is_unlisted=False)
+ .order_by(self.order)
+ .select_related("user")
+ .defer("about", "user_script", "notes")
+ )
def get_context_data(self, **kwargs):
context = super(OrganizationUsers, self).get_context_data(**kwargs)
- context['title'] = _('%s Members') % self.object.name
- context['users'] = ranker(context['users'])
- context['partial'] = True
- context['is_admin'] = self.can_edit_organization()
- context['kick_url'] = reverse('organization_user_kick', args=[self.object.id, self.object.slug])
- context['first_page_href'] = '.'
+ context["title"] = _("%s Members") % self.object.name
+ context["users"] = ranker(context["users"])
+ context["partial"] = True
+ context["is_admin"] = self.can_edit_organization()
+ context["kick_url"] = reverse(
+ "organization_user_kick", args=[self.object.id, self.object.slug]
+ )
+ context["first_page_href"] = "."
context.update(self.get_sort_context())
context.update(self.get_sort_paginate_context())
return context
-class OrganizationMembershipChange(LoginRequiredMixin, OrganizationMixin, SingleObjectMixin, View):
+class OrganizationMembershipChange(
+ LoginRequiredMixin, OrganizationMixin, SingleObjectMixin, View
+):
def post(self, request, *args, **kwargs):
org = self.get_object()
response = self.handle(request, org, request.profile)
@@ -164,31 +219,44 @@ def handle(self, request, org, profile):
class JoinOrganization(OrganizationMembershipChange):
def handle(self, request, org, profile):
if profile.organizations.filter(id=org.id).exists():
- return generic_message(request, _('Joining organization'), _('You are already in the organization.'))
+ return generic_message(
+ request,
+ _("Joining organization"),
+ _("You are already in the organization."),
+ )
if not org.is_open:
- return generic_message(request, _('Joining organization'), _('This organization is not open.'))
+ return generic_message(
+ request, _("Joining organization"), _("This organization is not open.")
+ )
max_orgs = settings.DMOJ_USER_MAX_ORGANIZATION_COUNT
if profile.organizations.filter(is_open=True).count() >= max_orgs:
return generic_message(
- request, _('Joining organization'),
- ngettext('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),
+ request,
+ _("Joining organization"),
+ ngettext(
+ "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),
)
profile.organizations.add(org)
profile.save()
- cache.delete(make_template_fragment_key('org_member_count', (org.id,)))
+ cache.delete(make_template_fragment_key("org_member_count", (org.id,)))
class LeaveOrganization(OrganizationMembershipChange):
def handle(self, request, org, profile):
if not profile.organizations.filter(id=org.id).exists():
- return generic_message(request, _('Leaving organization'), _('You are not in "%s".') % org.short_name)
+ return generic_message(
+ request,
+ _("Leaving organization"),
+ _('You are not in "%s".') % org.short_name,
+ )
profile.organizations.remove(org)
- cache.delete(make_template_fragment_key('org_member_count', (org.id,)))
+ cache.delete(make_template_fragment_key("org_member_count", (org.id,)))
class OrganizationRequestForm(Form):
@@ -197,74 +265,94 @@ class OrganizationRequestForm(Form):
def __init__(self, *args, class_required: bool, class_queryset, **kwargs) -> None:
super().__init__(*args, **kwargs)
- self.fields['class_'].required = class_required
- self.fields['class_'].queryset = class_queryset
- self.fields['class_'].label_from_instance = attrgetter('name')
+ self.fields["class_"].required = class_required
+ self.fields["class_"].queryset = class_queryset
+ self.fields["class_"].label_from_instance = attrgetter("name")
self.show_classes = class_required or bool(class_queryset)
class RequestJoinOrganization(LoginRequiredMixin, SingleObjectMixin, FormView):
model = Organization
- slug_field = 'key'
- slug_url_kwarg = 'key'
- template_name = 'organization/requests/request.html'
+ slug_field = "key"
+ slug_url_kwarg = "key"
+ template_name = "organization/requests/request.html"
form_class = OrganizationRequestForm
def dispatch(self, request, *args, **kwargs):
self.object = self.get_object()
- if self.object.requests.filter(user=self.request.profile, state='P').exists():
- return generic_message(self.request, _("Can't request to join %s") % self.object.name,
- _('You already have a pending request to join %s.') % self.object.name)
+ if self.object.requests.filter(user=self.request.profile, state="P").exists():
+ return generic_message(
+ self.request,
+ _("Can't request to join %s") % self.object.name,
+ _("You already have a pending request to join %s.") % self.object.name,
+ )
if self.object.is_open:
raise Http404()
return super(RequestJoinOrganization, self).dispatch(request, *args, **kwargs)
def get_form_kwargs(self) -> dict:
kwargs = super().get_form_kwargs()
- kwargs['class_required'] = self.object.class_required
- kwargs['class_queryset'] = self.object.classes.filter(is_active=True)
+ kwargs["class_required"] = self.object.class_required
+ kwargs["class_queryset"] = self.object.classes.filter(is_active=True)
return kwargs
def get_context_data(self, **kwargs):
context = super(RequestJoinOrganization, self).get_context_data(**kwargs)
- context['title'] = _('Request to join %s') % self.object.name
+ context["title"] = _("Request to join %s") % self.object.name
return context
def form_valid(self, form):
request = OrganizationRequest()
request.organization = self.get_object()
request.user = self.request.profile
- request.reason = form.cleaned_data['reason']
- request.request_class = form.cleaned_data['class_']
- request.state = 'P'
+ request.reason = form.cleaned_data["reason"]
+ request.request_class = form.cleaned_data["class_"]
+ request.state = "P"
request.save()
- return HttpResponseRedirect(reverse('request_organization_detail', args=(
- request.organization.id, request.organization.slug, request.id,
- )))
+ return HttpResponseRedirect(
+ reverse(
+ "request_organization_detail",
+ args=(
+ request.organization.id,
+ request.organization.slug,
+ request.id,
+ ),
+ )
+ )
class OrganizationRequestDetail(LoginRequiredMixin, TitleMixin, DetailView):
model = OrganizationRequest
- template_name = 'organization/requests/detail.html'
- title = gettext_lazy('Join request detail')
- pk_url_kwarg = 'rpk'
+ template_name = "organization/requests/detail.html"
+ title = gettext_lazy("Join request detail")
+ pk_url_kwarg = "rpk"
def get_object(self, queryset=None):
object = super(OrganizationRequestDetail, self).get_object(queryset)
profile = self.request.profile
- if object.user_id != profile.id and not object.organization.admins.filter(id=profile.id).exists() and (
- not object.request_class or not object.request_class.admins.filter(id=profile.id).exists()):
+ if (
+ object.user_id != profile.id
+ and not object.organization.admins.filter(id=profile.id).exists()
+ and (
+ not object.request_class
+ or not object.request_class.admins.filter(id=profile.id).exists()
+ )
+ ):
raise PermissionDenied()
return object
-OrganizationRequestFormSet = modelformset_factory(OrganizationRequest, extra=0, fields=('state',), can_delete=True)
+OrganizationRequestFormSet = modelformset_factory(
+ OrganizationRequest, extra=0, fields=("state",), can_delete=True
+)
-class OrganizationRequestBaseView(LoginRequiredMixin, SingleObjectTemplateResponseMixin, SingleObjectMixin, View):
+class OrganizationRequestBaseView(
+ LoginRequiredMixin, SingleObjectTemplateResponseMixin, SingleObjectMixin, View
+):
model = Organization
- slug_field = 'key'
- slug_url_kwarg = 'key'
+ slug_field = "key"
+ slug_url_kwarg = "key"
tab = None
def get_object(self, queryset=None):
@@ -278,27 +366,33 @@ def get_object(self, queryset=None):
return organization
def get_requests(self):
- queryset = self.object.requests.select_related('user__user').defer(
- 'user__about', 'user__notes', 'user__user_script',
+ queryset = self.object.requests.select_related("user__user").defer(
+ "user__about",
+ "user__notes",
+ "user__user_script",
)
if not self.edit_all:
- queryset = queryset.filter(request_class__in=self.object.classes.filter(admins__id=self.request.profile.id))
+ queryset = queryset.filter(
+ request_class__in=self.object.classes.filter(
+ admins__id=self.request.profile.id
+ )
+ )
return queryset
def get_context_data(self, **kwargs):
context = super(OrganizationRequestBaseView, self).get_context_data(**kwargs)
- context['title'] = _('Managing join requests for %s') % self.object.name
- context['tab'] = self.tab
+ context["title"] = _("Managing join requests for %s") % self.object.name
+ context["tab"] = self.tab
return context
class OrganizationRequestView(OrganizationRequestBaseView):
- template_name = 'organization/requests/pending.html'
- tab = 'pending'
+ template_name = "organization/requests/pending.html"
+ tab = "pending"
def get_context_data(self, **kwargs):
context = super(OrganizationRequestView, self).get_context_data(**kwargs)
- context['formset'] = self.formset
+ context["formset"] = self.formset
return context
def get(self, request, *args, **kwargs):
@@ -308,37 +402,63 @@ def get(self, request, *args, **kwargs):
return self.render_to_response(context)
def get_requests(self):
- return super().get_requests().filter(state='P')
+ return super().get_requests().filter(state="P")
def post(self, request, *args, **kwargs):
self.object = organization = self.get_object()
- self.formset = formset = OrganizationRequestFormSet(request.POST, request.FILES, queryset=self.get_requests())
+ self.formset = formset = OrganizationRequestFormSet(
+ request.POST, request.FILES, queryset=self.get_requests()
+ )
if formset.is_valid():
if organization.slots is not None:
deleted_set = set(formset.deleted_forms)
- to_approve = sum(form.cleaned_data['state'] == 'A' for form in formset.forms if form not in deleted_set)
+ to_approve = sum(
+ form.cleaned_data["state"] == "A"
+ for form in formset.forms
+ if form not in deleted_set
+ )
can_add = organization.slots - organization.members.count()
if to_approve > can_add:
- msg1 = ngettext('Your organization can only receive %d more member.',
- 'Your organization can only receive %d more members.', can_add) % can_add
- msg2 = ngettext('You cannot approve %d user.',
- 'You cannot approve %d users.', to_approve) % to_approve
- messages.error(request, msg1 + '\n' + msg2)
- return self.render_to_response(self.get_context_data(object=organization))
+ msg1 = (
+ ngettext(
+ "Your organization can only receive %d more member.",
+ "Your organization can only receive %d more members.",
+ can_add,
+ )
+ % can_add
+ )
+ msg2 = (
+ ngettext(
+ "You cannot approve %d user.",
+ "You cannot approve %d users.",
+ to_approve,
+ )
+ % to_approve
+ )
+ messages.error(request, msg1 + "\n" + msg2)
+ return self.render_to_response(
+ self.get_context_data(object=organization)
+ )
approved, rejected = 0, 0
for obj in formset.save():
- if obj.state == 'A':
+ if obj.state == "A":
obj.user.organizations.add(obj.organization)
if obj.request_class:
obj.user.classes.add(obj.request_class)
approved += 1
- elif obj.state == 'R':
+ elif obj.state == "R":
rejected += 1
- messages.success(request,
- ngettext('Approved %d user.', 'Approved %d users.', approved) % approved + '\n' +
- ngettext('Rejected %d user.', 'Rejected %d users.', rejected) % rejected)
- cache.delete(make_template_fragment_key('org_member_count', (organization.id,)))
+ messages.success(
+ request,
+ ngettext("Approved %d user.", "Approved %d users.", approved) % approved
+ + "\n"
+ + ngettext("Rejected %d user.", "Rejected %d users.", rejected)
+ % rejected,
+ )
+ cache.delete(
+ make_template_fragment_key("org_member_count", (organization.id,))
+ )
return HttpResponseRedirect(request.get_full_path())
return self.render_to_response(self.get_context_data(object=organization))
@@ -346,9 +466,9 @@ def post(self, request, *args, **kwargs):
class OrganizationRequestLog(OrganizationRequestBaseView):
- states = ('A', 'R')
- tab = 'log'
- template_name = 'organization/requests/log.html'
+ states = ("A", "R")
+ tab = "log"
+ template_name = "organization/requests/log.html"
def get(self, request, *args, **kwargs):
self.object = self.get_object()
@@ -357,17 +477,17 @@ def get(self, request, *args, **kwargs):
def get_context_data(self, **kwargs):
context = super(OrganizationRequestLog, self).get_context_data(**kwargs)
- context['requests'] = self.get_requests().filter(state__in=self.states)
+ context["requests"] = self.get_requests().filter(state__in=self.states)
return context
class EditOrganization(LoginRequiredMixin, TitleMixin, OrganizationMixin, UpdateView):
- template_name = 'organization/edit.html'
+ template_name = "organization/edit.html"
model = Organization
form_class = EditOrganizationForm
def get_title(self):
- return _('Editing %s') % self.object.name
+ return _("Editing %s") % self.object.name
def get_object(self, queryset=None):
object = super(EditOrganization, self).get_object()
@@ -377,13 +497,14 @@ def get_object(self, queryset=None):
def get_form(self, form_class=None):
form = super(EditOrganization, self).get_form(form_class)
- form.fields['admins'].queryset = \
- Profile.objects.filter(Q(organizations=self.object) | Q(admin_of=self.object)).distinct()
+ form.fields["admins"].queryset = Profile.objects.filter(
+ Q(organizations=self.object) | Q(admin_of=self.object)
+ ).distinct()
return form
def form_valid(self, form):
with revisions.create_revision(atomic=True):
- revisions.set_comment(_('Edited from site'))
+ revisions.set_comment(_("Edited from site"))
revisions.set_user(self.request.user)
return super(EditOrganization, self).form_valid(form)
@@ -391,70 +512,97 @@ def dispatch(self, request, *args, **kwargs):
try:
return super(EditOrganization, self).dispatch(request, *args, **kwargs)
except PermissionDenied:
- return generic_message(request, _("Can't edit organization"),
- _('You are not allowed to edit this organization.'), status=403)
+ return generic_message(
+ request,
+ _("Can't edit organization"),
+ _("You are not allowed to edit this organization."),
+ status=403,
+ )
-class KickUserWidgetView(LoginRequiredMixin, OrganizationMixin, SingleObjectMixin, View):
+class KickUserWidgetView(
+ LoginRequiredMixin, OrganizationMixin, SingleObjectMixin, View
+):
def post(self, request, *args, **kwargs):
organization = self.get_object()
if not self.can_edit_organization(organization):
- return generic_message(request, _("Can't edit organization"),
- _('You are not allowed to kick people from this organization.'), status=403)
+ return generic_message(
+ request,
+ _("Can't edit organization"),
+ _("You are not allowed to kick people from this organization."),
+ status=403,
+ )
try:
- user = Profile.objects.get(id=request.POST.get('user', None))
+ user = Profile.objects.get(id=request.POST.get("user", None))
except Profile.DoesNotExist:
- return generic_message(request, _("Can't kick user"),
- _('The user you are trying to kick does not exist!'), status=400)
+ return generic_message(
+ request,
+ _("Can't kick user"),
+ _("The user you are trying to kick does not exist!"),
+ status=400,
+ )
if not organization.members.filter(id=user.id).exists():
- return generic_message(request, _("Can't kick user"),
- _('The user you are trying to kick is not in organization: %s') %
- organization.name, status=400)
+ return generic_message(
+ request,
+ _("Can't kick user"),
+ _("The user you are trying to kick is not in organization: %s")
+ % organization.name,
+ status=400,
+ )
organization.members.remove(user)
return HttpResponseRedirect(organization.get_users_url())
class ClassMixin(TitleMixin, SingleObjectTemplateResponseMixin, SingleObjectMixin):
- context_object_name = 'class'
+ context_object_name = "class"
model = Class
- pk_url_kwarg = 'cpk'
+ pk_url_kwarg = "cpk"
def get(self, request, *args, **kwargs):
self.object = self.get_object()
org = self.object.organization
- if self.object.slug != kwargs['cslug'] or org.id != kwargs['pk'] or org.slug != kwargs['slug']:
+ if (
+ self.object.slug != kwargs["cslug"]
+ or org.id != kwargs["pk"]
+ or org.slug != kwargs["slug"]
+ ):
return HttpResponsePermanentRedirect(self.object.get_absolute_url())
context = self.get_context_data()
return self.render_to_response(context)
class ClassHome(QueryStringSortMixin, ClassMixin, DetailView):
- template_name = 'organization/class.html'
- all_sorts = frozenset(('problem_count', 'rating', 'performance_points'))
+ template_name = "organization/class.html"
+ all_sorts = frozenset(("problem_count", "rating", "performance_points"))
default_desc = all_sorts
- default_sort = '-performance_points'
+ default_sort = "-performance_points"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
- context['logo_override_image'] = self.object.organization.logo_override_image
- context['users'] = users_for_template(self.object.members, self.order)
- context['is_admin'] = False # Don't allow kicking here
+ context["logo_override_image"] = self.object.organization.logo_override_image
+ context["users"] = users_for_template(self.object.members, self.order)
+ context["is_admin"] = False # Don't allow kicking here
context.update(self.get_sort_context())
return context
def get_content_title(self):
org = self.object.organization
- return mark_safe(escape(_('Class {name} in {organization}')).format(
- name=escape(self.object.name),
- organization=format_html('{1} ', org.get_absolute_url(), org.name),
- ))
+ return mark_safe(
+ escape(_("Class {name} in {organization}")).format(
+ name=escape(self.object.name),
+ organization=format_html(
+ '{1} ', org.get_absolute_url(), org.name
+ ),
+ )
+ )
def get_title(self):
- return _('Class {name} - {organization}').format(
- name=self.object.name, organization=self.object.organization.name,
+ return _("Class {name} - {organization}").format(
+ name=self.object.name,
+ organization=self.object.organization.name,
)
@@ -463,7 +611,7 @@ class ClassRequestForm(Form):
class RequestJoinClass(LoginRequiredMixin, ClassMixin, FormView):
- template_name = 'organization/requests/request.html'
+ template_name = "organization/requests/request.html"
form_class = ClassRequestForm
def dispatch(self, request, *args, **kwargs):
@@ -473,16 +621,24 @@ def dispatch(self, request, *args, **kwargs):
self.object = self.get_object()
org = self.object.organization
if not org.members.filter(id=self.request.profile.id).exists():
- return HttpResponseRedirect(reverse('request_organization', args=(org.id, org.slug)))
- if org.requests.filter(user=self.request.profile, state='P', request_class=self.object).exists():
- return generic_message(self.request, _("Can't request to join %s") % self.object.name,
- _('You already have a pending request to join %s.') % self.object.name)
+ return HttpResponseRedirect(
+ reverse("request_organization", args=(org.id, org.slug))
+ )
+ if org.requests.filter(
+ user=self.request.profile, state="P", request_class=self.object
+ ).exists():
+ return generic_message(
+ self.request,
+ _("Can't request to join %s") % self.object.name,
+ _("You already have a pending request to join %s.") % self.object.name,
+ )
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
- context['title'] = _('Request to join {name} in {organization}').format(
- name=self.object.name, organization=self.object.organization.name,
+ context["title"] = _("Request to join {name} in {organization}").format(
+ name=self.object.name,
+ organization=self.object.organization.name,
)
return context
@@ -490,10 +646,17 @@ def form_valid(self, form):
request = OrganizationRequest()
request.organization = self.object.organization
request.user = self.request.profile
- request.reason = form.cleaned_data['reason']
+ request.reason = form.cleaned_data["reason"]
request.request_class = self.object
- request.state = 'P'
+ request.state = "P"
request.save()
- return HttpResponseRedirect(reverse('request_organization_detail', args=(
- request.organization.id, request.organization.slug, request.id,
- )))
+ return HttpResponseRedirect(
+ reverse(
+ "request_organization_detail",
+ args=(
+ request.organization.id,
+ request.organization.slug,
+ request.id,
+ ),
+ )
+ )
diff --git a/judge/views/preview.py b/judge/views/preview.py
index 2b3d2046b0..f9bcf597e7 100644
--- a/judge/views/preview.py
+++ b/judge/views/preview.py
@@ -5,54 +5,56 @@
class MarkdownPreviewView(TemplateResponseMixin, ContextMixin, View):
def post(self, request, *args, **kwargs):
try:
- self.preview_data = data = request.POST['content']
+ self.preview_data = data = request.POST["content"]
except KeyError:
- return HttpResponseBadRequest('No preview data specified.')
+ return HttpResponseBadRequest("No preview data specified.")
- return self.render_to_response(self.get_context_data(
- preview_data=data,
- ))
+ return self.render_to_response(
+ self.get_context_data(
+ preview_data=data,
+ )
+ )
class ProblemMarkdownPreviewView(MarkdownPreviewView):
- template_name = 'problem/preview.html'
+ template_name = "problem/preview.html"
class BlogMarkdownPreviewView(MarkdownPreviewView):
- template_name = 'blog/preview.html'
+ template_name = "blog/preview.html"
class ContestMarkdownPreviewView(MarkdownPreviewView):
- template_name = 'contest/preview.html'
+ template_name = "contest/preview.html"
class CommentMarkdownPreviewView(MarkdownPreviewView):
- template_name = 'comments/preview.html'
+ template_name = "comments/preview.html"
class FlatPageMarkdownPreviewView(MarkdownPreviewView):
- template_name = 'flatpage-preview.html'
+ template_name = "flatpage-preview.html"
class ProfileMarkdownPreviewView(MarkdownPreviewView):
- template_name = 'user/preview.html'
+ template_name = "user/preview.html"
class OrganizationMarkdownPreviewView(MarkdownPreviewView):
- template_name = 'organization/preview.html'
+ template_name = "organization/preview.html"
class SolutionMarkdownPreviewView(MarkdownPreviewView):
- template_name = 'solution-preview.html'
+ template_name = "solution-preview.html"
class LicenseMarkdownPreviewView(MarkdownPreviewView):
- template_name = 'license-preview.html'
+ template_name = "license-preview.html"
class TicketMarkdownPreviewView(MarkdownPreviewView):
- template_name = 'ticket/preview.html'
+ template_name = "ticket/preview.html"
class DefaultMarkdownPreviewView(MarkdownPreviewView):
- template_name = 'default-preview.html'
+ template_name = "default-preview.html"
diff --git a/judge/views/problem.py b/judge/views/problem.py
index 3fb2365f47..97232fc4cf 100644
--- a/judge/views/problem.py
+++ b/judge/views/problem.py
@@ -10,10 +10,26 @@
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.db import transaction
-from django.db.models import BooleanField, Case, CharField, Count, F, FilteredRelation, Prefetch, Q, When
+from django.db.models import (
+ BooleanField,
+ Case,
+ CharField,
+ Count,
+ F,
+ FilteredRelation,
+ Prefetch,
+ Q,
+ When,
+)
from django.db.models.functions import Coalesce
from django.db.utils import ProgrammingError
-from django.http import Http404, HttpResponse, HttpResponseForbidden, HttpResponseRedirect, JsonResponse
+from django.http import (
+ Http404,
+ HttpResponse,
+ HttpResponseForbidden,
+ HttpResponseRedirect,
+ JsonResponse,
+)
from django.shortcuts import get_object_or_404
from django.template.loader import get_template
from django.urls import reverse
@@ -28,19 +44,44 @@
from judge.comments import CommentedDetailView
from judge.forms import ProblemCloneForm, ProblemPointsVoteForm, ProblemSubmitForm
-from judge.models import ContestSubmission, Judge, Language, Problem, ProblemGroup, ProblemPointsVote, \
- ProblemTranslation, ProblemType, RuntimeVersion, Solution, Submission, SubmissionSource
+from judge.models import (
+ ContestSubmission,
+ Judge,
+ Language,
+ Problem,
+ ProblemGroup,
+ ProblemPointsVote,
+ ProblemTranslation,
+ ProblemType,
+ RuntimeVersion,
+ Solution,
+ Submission,
+ SubmissionSource,
+)
from judge.utils.diggpaginator import DiggPaginator
from judge.utils.opengraph import generate_opengraph
from judge.utils.pdfoid import PDF_RENDERING_ENABLED, render_pdf
-from judge.utils.problems import contest_attempted_ids, contest_completed_ids, hot_problems, user_attempted_ids, \
- user_completed_ids
+from judge.utils.problems import (
+ contest_attempted_ids,
+ contest_completed_ids,
+ hot_problems,
+ user_attempted_ids,
+ user_completed_ids,
+)
from judge.utils.strings import safe_float_or_none, safe_int_or_none
from judge.utils.tickets import own_ticket_filter
-from judge.utils.views import QueryStringSortMixin, SingleObjectFormView, TitleMixin, add_file_response, generic_message
+from judge.utils.views import (
+ QueryStringSortMixin,
+ SingleObjectFormView,
+ TitleMixin,
+ add_file_response,
+ generic_message,
+)
-recjk = re.compile(r'[\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u3005\u3007\u3021-\u3029\u3038-\u303A\u303B\u3400-\u4DB5'
- r'\u4E00-\u9FC3\uF900-\uFA2D\uFA30-\uFA6A\uFA70-\uFAD9\U00020000-\U0002A6D6\U0002F800-\U0002FA1D]')
+recjk = re.compile(
+ r"[\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u3005\u3007\u3021-\u3029\u3038-\u303A\u303B\u3400-\u4DB5"
+ r"\u4E00-\u9FC3\uF900-\uFA2D\uFA30-\uFA6A\uFA70-\uFAD9\U00020000-\U0002A6D6\U0002F800-\U0002FA1D]"
+)
def get_contest_problem(problem, profile):
@@ -51,14 +92,17 @@ def get_contest_problem(problem, profile):
def get_contest_submission_count(problem, profile, virtual):
- return profile.current_contest.submissions.exclude(submission__status__in=['IE']) \
- .filter(problem__problem=problem, participation__virtual=virtual).count()
+ return (
+ profile.current_contest.submissions.exclude(submission__status__in=["IE"])
+ .filter(problem__problem=problem, participation__virtual=virtual)
+ .count()
+ )
class ProblemMixin(object):
model = Problem
- slug_url_kwarg = 'problem'
- slug_field = 'code'
+ slug_url_kwarg = "problem"
+ slug_field = "code"
def get_object(self, queryset=None):
problem = super(ProblemMixin, self).get_object(queryset)
@@ -68,8 +112,12 @@ def get_object(self, queryset=None):
def no_such_problem(self):
code = self.kwargs.get(self.slug_url_kwarg, None)
- return generic_message(self.request, _('No such problem'),
- _('Could not find a problem with the code "%s".') % code, status=404)
+ return generic_message(
+ self.request,
+ _("No such problem"),
+ _('Could not find a problem with the code "%s".') % code,
+ status=404,
+ )
def get(self, request, *args, **kwargs):
try:
@@ -106,17 +154,25 @@ def profile(self):
return self.request.profile
-class ProblemSolution(SolvedProblemMixin, ProblemMixin, TitleMixin, CommentedDetailView):
- context_object_name = 'problem'
- template_name = 'problem/editorial.html'
+class ProblemSolution(
+ SolvedProblemMixin, ProblemMixin, TitleMixin, CommentedDetailView
+):
+ context_object_name = "problem"
+ template_name = "problem/editorial.html"
def get_title(self):
- return _('Editorial for {0}').format(self.object.name)
+ return _("Editorial for {0}").format(self.object.name)
def get_content_title(self):
- return mark_safe(escape(_('Editorial for {0}')).format(
- format_html('{0} ', self.object.name, reverse('problem_detail', args=[self.object.code])),
- ))
+ return mark_safe(
+ escape(_("Editorial for {0}")).format(
+ format_html(
+ '{0} ',
+ self.object.name,
+ reverse("problem_detail", args=[self.object.code]),
+ ),
+ )
+ )
def get_context_data(self, **kwargs):
context = super(ProblemSolution, self).get_context_data(**kwargs)
@@ -125,98 +181,126 @@ def get_context_data(self, **kwargs):
if not solution.is_accessible_by(self.request.user) or self.request.in_contest:
raise Http404()
- context['solution'] = solution
- context['has_solved_problem'] = self.object.id in self.get_completed_problems()
+ context["solution"] = solution
+ context["has_solved_problem"] = self.object.id in self.get_completed_problems()
return context
def get_comment_page(self):
- return 's:' + self.object.code
+ return "s:" + self.object.code
def no_such_problem(self):
code = self.kwargs.get(self.slug_url_kwarg, None)
- return generic_message(self.request, _('No such editorial'),
- _('Could not find an editorial with the code "%s".') % code, status=404)
+ return generic_message(
+ self.request,
+ _("No such editorial"),
+ _('Could not find an editorial with the code "%s".') % code,
+ status=404,
+ )
class ProblemDetail(ProblemMixin, SolvedProblemMixin, CommentedDetailView):
- context_object_name = 'problem'
- template_name = 'problem/problem.html'
+ context_object_name = "problem"
+ template_name = "problem/problem.html"
def get_comment_page(self):
- return 'p:%s' % self.object.code
+ return "p:%s" % self.object.code
def get_context_data(self, **kwargs):
context = super(ProblemDetail, self).get_context_data(**kwargs)
user = self.request.user
authed = user.is_authenticated
- context['has_submissions'] = authed and Submission.objects.filter(user=user.profile,
- problem=self.object).exists()
- contest_problem = (None if not authed or user.profile.current_contest is None else
- get_contest_problem(self.object, user.profile))
- context['contest_problem'] = contest_problem
+ context["has_submissions"] = (
+ authed
+ and Submission.objects.filter(
+ user=user.profile, problem=self.object
+ ).exists()
+ )
+ contest_problem = (
+ None
+ if not authed or user.profile.current_contest is None
+ else get_contest_problem(self.object, user.profile)
+ )
+ context["contest_problem"] = contest_problem
if contest_problem:
clarifications = self.object.clarifications
- context['has_clarifications'] = clarifications.count() > 0
- context['clarifications'] = clarifications.order_by('-date')
- context['submission_limit'] = contest_problem.max_submissions
+ context["has_clarifications"] = clarifications.count() > 0
+ context["clarifications"] = clarifications.order_by("-date")
+ context["submission_limit"] = contest_problem.max_submissions
if contest_problem.max_submissions:
- context['submissions_left'] = max(contest_problem.max_submissions -
- get_contest_submission_count(self.object, user.profile,
- user.profile.current_contest.virtual), 0)
+ context["submissions_left"] = max(
+ contest_problem.max_submissions
+ - get_contest_submission_count(
+ self.object, user.profile, user.profile.current_contest.virtual
+ ),
+ 0,
+ )
- context['available_judges'] = Judge.objects.filter(online=True, problems=self.object)
- context['show_languages'] = self.object.allowed_languages.count() != Language.objects.count()
- context['has_pdf_render'] = PDF_RENDERING_ENABLED
- context['completed_problem_ids'] = self.get_completed_problems()
- context['attempted_problems'] = self.get_attempted_problems()
+ context["available_judges"] = Judge.objects.filter(
+ online=True, problems=self.object
+ )
+ context["show_languages"] = (
+ self.object.allowed_languages.count() != Language.objects.count()
+ )
+ context["has_pdf_render"] = PDF_RENDERING_ENABLED
+ context["completed_problem_ids"] = self.get_completed_problems()
+ context["attempted_problems"] = self.get_attempted_problems()
can_edit = self.object.is_editable_by(user)
- context['can_edit_problem'] = can_edit
+ context["can_edit_problem"] = can_edit
if user.is_authenticated:
tickets = self.object.tickets
if not can_edit:
tickets = tickets.filter(own_ticket_filter(user.profile.id))
- context['has_tickets'] = tickets.exists()
- context['num_open_tickets'] = tickets.filter(is_open=True).values('id').distinct().count()
+ context["has_tickets"] = tickets.exists()
+ context["num_open_tickets"] = (
+ tickets.filter(is_open=True).values("id").distinct().count()
+ )
try:
- context['editorial'] = Solution.objects.get(problem=self.object)
+ context["editorial"] = Solution.objects.get(problem=self.object)
except ObjectDoesNotExist:
pass
try:
- translation = self.object.translations.get(language=self.request.LANGUAGE_CODE)
+ translation = self.object.translations.get(
+ language=self.request.LANGUAGE_CODE
+ )
except ProblemTranslation.DoesNotExist:
- context['title'] = self.object.name
- context['language'] = settings.LANGUAGE_CODE
- context['description'] = self.object.description
- context['translated'] = False
+ context["title"] = self.object.name
+ context["language"] = settings.LANGUAGE_CODE
+ context["description"] = self.object.description
+ context["translated"] = False
else:
- context['title'] = translation.name
- context['language'] = self.request.LANGUAGE_CODE
- context['description'] = translation.description
- context['translated'] = True
+ context["title"] = translation.name
+ context["language"] = self.request.LANGUAGE_CODE
+ context["description"] = translation.description
+ context["translated"] = True
if not self.object.og_image or not self.object.summary:
- metadata = generate_opengraph('generated-meta-problem:%s:%d' % (context['language'], self.object.id),
- context['description'], 'problem')
- context['meta_description'] = self.object.summary or metadata[0]
- context['og_image'] = self.object.og_image or metadata[1]
+ metadata = generate_opengraph(
+ "generated-meta-problem:%s:%d" % (context["language"], self.object.id),
+ context["description"],
+ "problem",
+ )
+ context["meta_description"] = self.object.summary or metadata[0]
+ context["og_image"] = self.object.og_image or metadata[1]
- context['vote_perm'] = self.object.vote_permission_for_user(user)
- if context['vote_perm'].can_vote():
+ context["vote_perm"] = self.object.vote_permission_for_user(user)
+ if context["vote_perm"].can_vote():
try:
- context['vote'] = ProblemPointsVote.objects.get(voter=user.profile, problem=self.object)
+ context["vote"] = ProblemPointsVote.objects.get(
+ voter=user.profile, problem=self.object
+ )
except ObjectDoesNotExist:
- context['vote'] = None
+ context["vote"] = None
else:
- context['vote'] = None
+ context["vote"] = None
return context
class ProblemVote(ProblemMixin, DetailView):
- context_object_name = 'problem'
- template_name = 'problem/vote-ajax.html'
+ context_object_name = "problem"
+ template_name = "problem/vote-ajax.html"
def get_context_data(self, **kwargs):
if not self.object.vote_permission_for_user(self.request.user).can_vote():
@@ -225,18 +309,22 @@ def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
try:
- context['vote'] = ProblemPointsVote.objects.get(voter=self.request.profile, problem=self.object)
+ context["vote"] = ProblemPointsVote.objects.get(
+ voter=self.request.profile, problem=self.object
+ )
except ObjectDoesNotExist:
- context['vote'] = None
+ context["vote"] = None
- context['max_possible_vote'] = settings.DMOJ_PROBLEM_MAX_USER_POINTS_VOTE
- context['min_possible_vote'] = settings.DMOJ_PROBLEM_MIN_USER_POINTS_VOTE
+ context["max_possible_vote"] = settings.DMOJ_PROBLEM_MAX_USER_POINTS_VOTE
+ context["min_possible_vote"] = settings.DMOJ_PROBLEM_MIN_USER_POINTS_VOTE
return context
def post(self, request, *args, **kwargs):
problem = self.get_object()
if not problem.vote_permission_for_user(request.user).can_vote():
- return JsonResponse({'message': _('Not allowed to vote on this problem.')}, status=403)
+ return JsonResponse(
+ {"message": _("Not allowed to vote on this problem.")}, status=403
+ )
form = ProblemPointsVoteForm(request.POST)
if not form.is_valid():
@@ -244,30 +332,40 @@ def post(self, request, *args, **kwargs):
with transaction.atomic():
# Delete any pre-existing votes.
- ProblemPointsVote.objects.filter(voter=request.profile, problem=problem).delete()
+ ProblemPointsVote.objects.filter(
+ voter=request.profile, problem=problem
+ ).delete()
vote = form.save(commit=False)
vote.voter = request.profile
vote.problem = problem
vote.save()
- return JsonResponse({'points': vote.points})
+ return JsonResponse({"points": vote.points})
class DeleteProblemVote(ProblemMixin, SingleObjectMixin, View):
- http_method_names = ['options', 'post'] # This disables GET requests, even though ProblemMixin.get exists.
+ http_method_names = [
+ "options",
+ "post",
+ ] # This disables GET requests, even though ProblemMixin.get exists.
def post(self, request, *args, **kwargs):
problem = self.get_object()
if not problem.vote_permission_for_user(request.user).can_vote():
- return JsonResponse({'message': _('Not allowed to delete votes on this problem.')}, status=403)
+ return JsonResponse(
+ {"message": _("Not allowed to delete votes on this problem.")},
+ status=403,
+ )
- ProblemPointsVote.objects.filter(voter=request.profile, problem=problem).delete()
- return JsonResponse({'message': _('success')})
+ ProblemPointsVote.objects.filter(
+ voter=request.profile, problem=problem
+ ).delete()
+ return JsonResponse({"message": _("success")})
class ProblemVoteStats(ProblemMixin, DetailView):
- context_object_name = 'problem'
- template_name = 'problem/vote-stats-ajax.html'
+ context_object_name = "problem"
+ template_name = "problem/vote-stats-ajax.html"
def get_context_data(self, **kwargs):
if not self.object.vote_permission_for_user(self.request.user).can_view():
@@ -275,15 +373,19 @@ def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
- votes = list(self.object.problem_points_votes.order_by('points').values_list('points', flat=True))
- context['votes'] = votes
+ votes = list(
+ self.object.problem_points_votes.order_by("points").values_list(
+ "points", flat=True
+ )
+ )
+ context["votes"] = votes
if votes:
- context['mean'] = mean(votes)
- context['median'] = median(votes)
+ context["mean"] = mean(votes)
+ context["median"] = median(votes)
- context['max_possible_vote'] = settings.DMOJ_PROBLEM_MAX_USER_POINTS_VOTE
- context['min_possible_vote'] = settings.DMOJ_PROBLEM_MIN_USER_POINTS_VOTE
+ context["max_possible_vote"] = settings.DMOJ_PROBLEM_MAX_USER_POINTS_VOTE
+ context["min_possible_vote"] = settings.DMOJ_PROBLEM_MIN_USER_POINTS_VOTE
return context
@@ -292,22 +394,22 @@ class LatexError(Exception):
class ProblemPdfView(ProblemMixin, SingleObjectMixin, View):
- logger = logging.getLogger('judge.problem.pdf')
+ logger = logging.getLogger("judge.problem.pdf")
languages = set(map(itemgetter(0), settings.LANGUAGES))
def get(self, request, *args, **kwargs):
if not PDF_RENDERING_ENABLED:
raise Http404()
- language = kwargs.get('language', self.request.LANGUAGE_CODE)
+ language = kwargs.get("language", self.request.LANGUAGE_CODE)
if language not in self.languages:
raise Http404()
problem = self.get_object()
- pdf_basename = '%s.%s.pdf' % (problem.code, language)
+ pdf_basename = "%s.%s.pdf" % (problem.code, language)
def render_problem_pdf():
- self.logger.info('Rendering PDF in %s: %s', language, problem.code)
+ self.logger.info("Rendering PDF in %s: %s", language, problem.code)
with translation.override(language):
try:
@@ -317,27 +419,34 @@ def render_problem_pdf():
problem_name = trans.name if trans else problem.name
return render_pdf(
- html=get_template('problem/raw.html').render({
- 'problem': problem,
- 'problem_name': problem_name,
- 'description': trans.description if trans else problem.description,
- 'url': request.build_absolute_uri(),
- }).replace('"//', '"https://').replace("'//", "'https://"),
+ html=get_template("problem/raw.html")
+ .render(
+ {
+ "problem": problem,
+ "problem_name": problem_name,
+ "description": trans.description
+ if trans
+ else problem.description,
+ "url": request.build_absolute_uri(),
+ }
+ )
+ .replace('"//', '"https://')
+ .replace("'//", "'https://"),
title=problem_name,
)
response = HttpResponse()
- response['Content-Type'] = 'application/pdf'
- response['Content-Disposition'] = f'inline; filename={pdf_basename}'
+ response["Content-Type"] = "application/pdf"
+ response["Content-Disposition"] = f"inline; filename={pdf_basename}"
if settings.DMOJ_PDF_PROBLEM_CACHE:
pdf_filename = os.path.join(settings.DMOJ_PDF_PROBLEM_CACHE, pdf_basename)
if not os.path.exists(pdf_filename):
- with open(pdf_filename, 'wb') as f:
+ with open(pdf_filename, "wb") as f:
f.write(render_problem_pdf())
if settings.DMOJ_PDF_PROBLEM_INTERNAL:
- url_path = f'{settings.DMOJ_PDF_PROBLEM_INTERNAL}/{pdf_basename}'
+ url_path = f"{settings.DMOJ_PDF_PROBLEM_INTERNAL}/{pdf_basename}"
else:
url_path = None
@@ -350,33 +459,45 @@ def render_problem_pdf():
class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView):
model = Problem
- title = gettext_lazy('Problems')
- context_object_name = 'problems'
- template_name = 'problem/list.html'
+ title = gettext_lazy("Problems")
+ context_object_name = "problems"
+ template_name = "problem/list.html"
paginate_by = 50
- sql_sort = frozenset(('points', 'ac_rate', 'user_count', 'code'))
- manual_sort = frozenset(('name', 'group', 'solved', 'type', 'editorial'))
+ sql_sort = frozenset(("points", "ac_rate", "user_count", "code"))
+ manual_sort = frozenset(("name", "group", "solved", "type", "editorial"))
all_sorts = sql_sort | manual_sort
- default_desc = frozenset(('points', 'ac_rate', 'user_count'))
- default_sort = 'code'
-
- def get_paginator(self, queryset, per_page, orphans=0,
- allow_empty_first_page=True, **kwargs):
- paginator = DiggPaginator(queryset, per_page, body=6, padding=2, orphans=orphans,
- count=queryset.values('pk').count() if not self.in_contest else None,
- allow_empty_first_page=allow_empty_first_page, **kwargs)
+ default_desc = frozenset(("points", "ac_rate", "user_count"))
+ default_sort = "code"
+
+ def get_paginator(
+ self, queryset, per_page, orphans=0, allow_empty_first_page=True, **kwargs
+ ):
+ paginator = DiggPaginator(
+ queryset,
+ per_page,
+ body=6,
+ padding=2,
+ orphans=orphans,
+ count=queryset.values("pk").count() if not self.in_contest else None,
+ allow_empty_first_page=allow_empty_first_page,
+ **kwargs,
+ )
if not self.in_contest:
queryset = queryset.add_i18n_name(self.request.LANGUAGE_CODE)
- sort_key = self.order.lstrip('-')
+ sort_key = self.order.lstrip("-")
if sort_key in self.sql_sort:
- queryset = queryset.order_by(self.order, 'id')
- elif sort_key == 'name':
- queryset = queryset.order_by(self.order.replace('name', 'i18n_name'), 'id')
- elif sort_key == 'group':
- queryset = queryset.order_by(self.order + '__name', 'id')
- elif sort_key == 'editorial':
- queryset = queryset.order_by(self.order.replace('editorial', 'has_public_editorial'), 'id')
- elif sort_key == 'solved':
+ queryset = queryset.order_by(self.order, "id")
+ elif sort_key == "name":
+ queryset = queryset.order_by(
+ self.order.replace("name", "i18n_name"), "id"
+ )
+ elif sort_key == "group":
+ queryset = queryset.order_by(self.order + "__name", "id")
+ elif sort_key == "editorial":
+ queryset = queryset.order_by(
+ self.order.replace("editorial", "has_public_editorial"), "id"
+ )
+ elif sort_key == "solved":
if self.request.user.is_authenticated:
profile = self.request.profile
solved = user_completed_ids(profile)
@@ -390,12 +511,18 @@ def _solved_sort_order(problem):
return -1
queryset = list(queryset)
- queryset.sort(key=_solved_sort_order, reverse=self.order.startswith('-'))
- elif sort_key == 'type':
+ queryset.sort(
+ key=_solved_sort_order, reverse=self.order.startswith("-")
+ )
+ elif sort_key == "type":
if self.show_types:
queryset = list(queryset)
- queryset.sort(key=lambda problem: problem.types_list[0] if problem.types_list else '',
- reverse=self.order.startswith('-'))
+ queryset.sort(
+ key=lambda problem: problem.types_list[0]
+ if problem.types_list
+ else "",
+ reverse=self.order.startswith("-"),
+ )
paginator.object_list = queryset
return paginator
@@ -406,72 +533,121 @@ def profile(self):
return self.request.profile
def get_contest_queryset(self):
- queryset = self.profile.current_contest.contest.contest_problems.select_related('problem__group') \
- .defer('problem__description').order_by('problem__code') \
- .annotate(user_count=Count('submission__participation', distinct=True)) \
- .annotate(i18n_translation=FilteredRelation(
- 'problem__translations', condition=Q(problem__translations__language=self.request.LANGUAGE_CODE),
- )).annotate(i18n_name=Coalesce(
- F('i18n_translation__name'), F('problem__name'), output_field=CharField(),
- )).order_by('order')
- return [{
- 'id': p['problem_id'],
- 'code': p['problem__code'],
- 'name': p['problem__name'],
- 'i18n_name': p['i18n_name'],
- 'group': {'full_name': p['problem__group__full_name']},
- 'points': p['points'],
- 'partial': p['partial'],
- 'user_count': p['user_count'],
- } for p in queryset.values('problem_id', 'problem__code', 'problem__name', 'i18n_name',
- 'problem__group__full_name', 'points', 'partial', 'user_count')]
+ queryset = (
+ self.profile.current_contest.contest.contest_problems.select_related(
+ "problem__group"
+ )
+ .defer("problem__description")
+ .order_by("problem__code")
+ .annotate(user_count=Count("submission__participation", distinct=True))
+ .annotate(
+ i18n_translation=FilteredRelation(
+ "problem__translations",
+ condition=Q(
+ problem__translations__language=self.request.LANGUAGE_CODE
+ ),
+ )
+ )
+ .annotate(
+ i18n_name=Coalesce(
+ F("i18n_translation__name"),
+ F("problem__name"),
+ output_field=CharField(),
+ )
+ )
+ .order_by("order")
+ )
+ return [
+ {
+ "id": p["problem_id"],
+ "code": p["problem__code"],
+ "name": p["problem__name"],
+ "i18n_name": p["i18n_name"],
+ "group": {"full_name": p["problem__group__full_name"]},
+ "points": p["points"],
+ "partial": p["partial"],
+ "user_count": p["user_count"],
+ }
+ for p in queryset.values(
+ "problem_id",
+ "problem__code",
+ "problem__name",
+ "i18n_name",
+ "problem__group__full_name",
+ "points",
+ "partial",
+ "user_count",
+ )
+ ]
@staticmethod
def apply_full_text(queryset, query):
if recjk.search(query):
# MariaDB can't tokenize CJK properly, fallback to LIKE '%term%' for each term.
for term in query.split():
- queryset = queryset.filter(Q(code__icontains=term) | Q(name__icontains=term) |
- Q(description__icontains=term))
+ queryset = queryset.filter(
+ Q(code__icontains=term)
+ | Q(name__icontains=term)
+ | Q(description__icontains=term)
+ )
return queryset
- return queryset.search(query, queryset.BOOLEAN).extra(order_by=['-relevance'])
+ return queryset.search(query, queryset.BOOLEAN).extra(order_by=["-relevance"])
def get_normal_queryset(self):
filter = Q(is_public=True)
- if not self.request.user.has_perm('see_organization_problem'):
+ if not self.request.user.has_perm("see_organization_problem"):
org_filter = Q(is_organization_private=False)
if self.profile is not None:
org_filter |= Q(organizations__in=self.profile.organizations.all())
filter &= org_filter
if self.profile is not None:
filter = Problem.q_add_author_curator_tester(filter, self.profile)
- queryset = Problem.objects.filter(filter).select_related('group').defer('description', 'summary')
+ queryset = (
+ Problem.objects.filter(filter)
+ .select_related("group")
+ .defer("description", "summary")
+ )
if self.profile is not None and self.hide_solved:
- queryset = queryset.exclude(id__in=Submission.objects
- .filter(user=self.profile, result='AC', case_points__gte=F('case_total'))
- .values_list('problem_id', flat=True))
+ queryset = queryset.exclude(
+ id__in=Submission.objects.filter(
+ user=self.profile, result="AC", case_points__gte=F("case_total")
+ ).values_list("problem_id", flat=True)
+ )
if self.show_types:
- queryset = queryset.prefetch_related('types')
- queryset = queryset.annotate(has_public_editorial=Case(
- When(solution__is_public=True, solution__publish_on__lte=timezone.now(), then=True),
- default=False,
- output_field=BooleanField(),
- ))
+ queryset = queryset.prefetch_related("types")
+ queryset = queryset.annotate(
+ has_public_editorial=Case(
+ When(
+ solution__is_public=True,
+ solution__publish_on__lte=timezone.now(),
+ then=True,
+ ),
+ default=False,
+ output_field=BooleanField(),
+ )
+ )
if self.has_public_editorial:
queryset = queryset.filter(has_public_editorial=True)
if self.category is not None:
queryset = queryset.filter(group__id=self.category)
if self.selected_types:
queryset = queryset.filter(types__in=self.selected_types)
- if 'search' in self.request.GET:
- self.search_query = query = ' '.join(self.request.GET.getlist('search')).strip()
+ if "search" in self.request.GET:
+ self.search_query = query = " ".join(
+ self.request.GET.getlist("search")
+ ).strip()
if query:
if settings.ENABLE_FTS and self.full_text:
queryset = self.apply_full_text(queryset, query)
else:
queryset = queryset.filter(
- Q(code__icontains=query) | Q(name__icontains=query) |
- Q(translations__name__icontains=query, translations__language=self.request.LANGUAGE_CODE))
+ Q(code__icontains=query)
+ | Q(name__icontains=query)
+ | Q(
+ translations__name__icontains=query,
+ translations__language=self.request.LANGUAGE_CODE,
+ )
+ )
self.prepoint_queryset = queryset
if self.point_start is not None:
queryset = queryset.filter(points__gte=self.point_start)
@@ -487,29 +663,41 @@ def get_queryset(self):
def get_context_data(self, **kwargs):
context = super(ProblemList, self).get_context_data(**kwargs)
- context['hide_solved'] = 0 if self.in_contest else int(self.hide_solved)
- context['show_types'] = 0 if self.in_contest else int(self.show_types)
- context['has_public_editorial'] = 0 if self.in_contest else int(self.has_public_editorial)
- context['full_text'] = 0 if self.in_contest else int(self.full_text)
- context['category'] = self.category
- context['categories'] = ProblemGroup.objects.all()
+ context["hide_solved"] = 0 if self.in_contest else int(self.hide_solved)
+ context["show_types"] = 0 if self.in_contest else int(self.show_types)
+ context["has_public_editorial"] = (
+ 0 if self.in_contest else int(self.has_public_editorial)
+ )
+ context["full_text"] = 0 if self.in_contest else int(self.full_text)
+ context["category"] = self.category
+ context["categories"] = ProblemGroup.objects.all()
if self.show_types:
- context['selected_types'] = self.selected_types
- context['problem_types'] = ProblemType.objects.all()
- context['has_fts'] = settings.ENABLE_FTS
- context['search_query'] = self.search_query
- context['completed_problem_ids'] = self.get_completed_problems()
- context['attempted_problems'] = self.get_attempted_problems()
+ context["selected_types"] = self.selected_types
+ context["problem_types"] = ProblemType.objects.all()
+ context["has_fts"] = settings.ENABLE_FTS
+ context["search_query"] = self.search_query
+ context["completed_problem_ids"] = self.get_completed_problems()
+ context["attempted_problems"] = self.get_attempted_problems()
context.update(self.get_sort_paginate_context())
if not self.in_contest:
context.update(self.get_sort_context())
- context['hot_problems'] = hot_problems(timedelta(days=1), settings.DMOJ_PROBLEM_HOT_PROBLEM_COUNT)
- context['point_start'], context['point_end'], context['point_values'] = self.get_noui_slider_points()
+ context["hot_problems"] = hot_problems(
+ timedelta(days=1), settings.DMOJ_PROBLEM_HOT_PROBLEM_COUNT
+ )
+ (
+ context["point_start"],
+ context["point_end"],
+ context["point_values"],
+ ) = self.get_noui_slider_points()
else:
- context['hot_problems'] = None
- context['point_start'], context['point_end'], context['point_values'] = 0, 0, {}
- context['hide_contest_scoreboard'] = self.contest.scoreboard_visibility in (
+ context["hot_problems"] = None
+ context["point_start"], context["point_end"], context["point_values"] = (
+ 0,
+ 0,
+ {},
+ )
+ context["hide_contest_scoreboard"] = self.contest.scoreboard_visibility in (
self.contest.SCOREBOARD_AFTER_CONTEST,
self.contest.SCOREBOARD_AFTER_PARTICIPATION,
self.contest.SCOREBOARD_HIDDEN,
@@ -517,35 +705,50 @@ def get_context_data(self, **kwargs):
return context
def get_noui_slider_points(self):
- points = sorted(self.prepoint_queryset.values_list('points', flat=True).distinct())
+ points = sorted(
+ self.prepoint_queryset.values_list("points", flat=True).distinct()
+ )
if not points:
return 0, 0, {}
if len(points) == 1:
- return points[0] - 1, points[0] + 1, {
- 'min': points[0] - 1,
- '50%': points[0],
- 'max': points[0] + 1,
- }
+ return (
+ points[0] - 1,
+ points[0] + 1,
+ {
+ "min": points[0] - 1,
+ "50%": points[0],
+ "max": points[0] + 1,
+ },
+ )
start, end = points[0], points[-1]
if self.point_start is not None:
start = self.point_start
if self.point_end is not None:
end = self.point_end
- points_map = {0.0: 'min', 1.0: 'max'}
+ points_map = {0.0: "min", 1.0: "max"}
size = len(points) - 1
- return start, end, {points_map.get(i / size, '%.2f%%' % (100 * i / size,)): j for i, j in enumerate(points)}
+ return (
+ start,
+ end,
+ {
+ points_map.get(i / size, "%.2f%%" % (100 * i / size,)): j
+ for i, j in enumerate(points)
+ },
+ )
def GET_with_session(self, request, key):
if not request.GET:
return request.session.get(key, False)
- return request.GET.get(key, None) == '1'
+ return request.GET.get(key, None) == "1"
def setup_problem_list(self, request):
- self.hide_solved = self.GET_with_session(request, 'hide_solved')
- self.show_types = self.GET_with_session(request, 'show_types')
- self.full_text = self.GET_with_session(request, 'full_text')
- self.has_public_editorial = self.GET_with_session(request, 'has_public_editorial')
+ self.hide_solved = self.GET_with_session(request, "hide_solved")
+ self.show_types = self.GET_with_session(request, "show_types")
+ self.full_text = self.GET_with_session(request, "full_text")
+ self.has_public_editorial = self.GET_with_session(
+ request, "has_public_editorial"
+ )
self.search_query = None
self.category = None
@@ -554,17 +757,17 @@ def setup_problem_list(self, request):
# This actually copies into the instance dictionary...
self.all_sorts = set(self.all_sorts)
if not self.show_types:
- self.all_sorts.discard('type')
+ self.all_sorts.discard("type")
- self.category = safe_int_or_none(request.GET.get('category'))
- if 'type' in request.GET:
+ self.category = safe_int_or_none(request.GET.get("category"))
+ if "type" in request.GET:
try:
- self.selected_types = list(map(int, request.GET.getlist('type')))
+ self.selected_types = list(map(int, request.GET.getlist("type")))
except ValueError:
pass
- self.point_start = safe_float_or_none(request.GET.get('point_start'))
- self.point_end = safe_float_or_none(request.GET.get('point_end'))
+ self.point_start = safe_float_or_none(request.GET.get("point_start"))
+ self.point_end = safe_float_or_none(request.GET.get("point_end"))
def get(self, request, *args, **kwargs):
self.setup_problem_list(request)
@@ -572,13 +775,13 @@ def get(self, request, *args, **kwargs):
try:
return super(ProblemList, self).get(request, *args, **kwargs)
except ProgrammingError as e:
- return generic_message(request, 'FTS syntax error', e.args[1], status=400)
+ return generic_message(request, "FTS syntax error", e.args[1], status=400)
def post(self, request, *args, **kwargs):
- to_update = ('hide_solved', 'show_types', 'has_public_editorial', 'full_text')
+ to_update = ("hide_solved", "show_types", "has_public_editorial", "full_text")
for key in to_update:
if key in request.GET:
- val = request.GET.get(key) == '1'
+ val = request.GET.get(key) == "1"
request.session[key] = val
else:
request.session.pop(key, None)
@@ -588,10 +791,10 @@ def post(self, request, *args, **kwargs):
class LanguageTemplateAjax(View):
def get(self, request, *args, **kwargs):
try:
- language = get_object_or_404(Language, id=int(request.GET.get('id', 0)))
+ language = get_object_or_404(Language, id=int(request.GET.get("id", 0)))
except ValueError:
raise Http404()
- return HttpResponse(language.template, content_type='text/plain')
+ return HttpResponse(language.template, content_type="text/plain")
class RandomProblem(ProblemList):
@@ -603,16 +806,22 @@ def get(self, request, *args, **kwargs):
queryset = self.get_normal_queryset()
count = queryset.count()
if not count:
- return HttpResponseRedirect('%s%s%s' % (reverse('problem_list'), request.META['QUERY_STRING'] and '?',
- request.META['QUERY_STRING']))
+ return HttpResponseRedirect(
+ "%s%s%s"
+ % (
+ reverse("problem_list"),
+ request.META["QUERY_STRING"] and "?",
+ request.META["QUERY_STRING"],
+ )
+ )
return HttpResponseRedirect(queryset[randrange(count)].get_absolute_url())
-user_logger = logging.getLogger('judge.user')
+user_logger = logging.getLogger("judge.user")
class ProblemSubmit(LoginRequiredMixin, ProblemMixin, TitleMixin, SingleObjectFormView):
- template_name = 'problem/submit.html'
+ template_name = "problem/submit.html"
form_class = ProblemSubmitForm
@cached_property
@@ -631,8 +840,11 @@ def remaining_submission_count(self):
# a non-negative integer, which is required for future checks in this view.
return max(
0,
- max_subs - get_contest_submission_count(
- self.object, self.request.profile, self.request.profile.current_contest.virtual,
+ max_subs
+ - get_contest_submission_count(
+ self.object,
+ self.request.profile,
+ self.request.profile.current_contest.virtual,
),
)
@@ -645,70 +857,96 @@ def default_language(self):
def get_content_title(self):
return mark_safe(
- escape(_('Submit to %s')) % format_html(
+ escape(_("Submit to %s"))
+ % format_html(
'{1} ',
- reverse('problem_detail', args=[self.object.code]),
+ reverse("problem_detail", args=[self.object.code]),
self.object.translated_name(self.request.LANGUAGE_CODE),
),
)
def get_title(self):
- return _('Submit to %s') % self.object.translated_name(self.request.LANGUAGE_CODE)
+ return _("Submit to %s") % self.object.translated_name(
+ self.request.LANGUAGE_CODE
+ )
def get_initial(self):
- initial = {'language': self.default_language}
+ initial = {"language": self.default_language}
if self.old_submission is not None:
- initial['source'] = self.old_submission.source.source
+ initial["source"] = self.old_submission.source.source
return initial
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
- kwargs['instance'] = Submission(user=self.request.profile, problem=self.object)
+ kwargs["instance"] = Submission(user=self.request.profile, problem=self.object)
if self.object.is_editable_by(self.request.user):
- kwargs['judge_choices'] = tuple(
- Judge.objects.filter(online=True, problems=self.object).values_list('name', 'name'),
+ kwargs["judge_choices"] = tuple(
+ Judge.objects.filter(online=True, problems=self.object).values_list(
+ "name", "name"
+ ),
)
else:
- kwargs['judge_choices'] = ()
+ kwargs["judge_choices"] = ()
return kwargs
def get_form(self, form_class=None):
form = super().get_form(form_class)
- form.fields['language'].queryset = (
- self.object.usable_languages.order_by('name', 'key')
- .prefetch_related(Prefetch('runtimeversion_set', RuntimeVersion.objects.order_by('priority')))
+ form.fields["language"].queryset = self.object.usable_languages.order_by(
+ "name", "key"
+ ).prefetch_related(
+ Prefetch("runtimeversion_set", RuntimeVersion.objects.order_by("priority"))
)
- form_data = getattr(form, 'cleaned_data', form.initial)
- if 'language' in form_data:
- form.fields['source'].widget.mode = form_data['language'].ace
- form.fields['source'].widget.theme = self.request.profile.resolved_ace_theme
+ form_data = getattr(form, "cleaned_data", form.initial)
+ if "language" in form_data:
+ form.fields["source"].widget.mode = form_data["language"].ace
+ form.fields["source"].widget.theme = self.request.profile.resolved_ace_theme
return form
def get_success_url(self):
- return reverse('submission_status', args=(self.new_submission.id,))
+ return reverse("submission_status", args=(self.new_submission.id,))
def form_valid(self, form):
if (
- not self.request.user.has_perm('judge.spam_submission') and
- Submission.objects.filter(user=self.request.profile, rejudged_date__isnull=True)
- .exclude(status__in=['D', 'IE', 'CE', 'AB']).count() >= settings.DMOJ_SUBMISSION_LIMIT
+ not self.request.user.has_perm("judge.spam_submission")
+ and Submission.objects.filter(
+ user=self.request.profile, rejudged_date__isnull=True
+ )
+ .exclude(status__in=["D", "IE", "CE", "AB"])
+ .count()
+ >= settings.DMOJ_SUBMISSION_LIMIT
):
- return HttpResponse(format_html('{0} ', _('You submitted too many submissions.')), status=429)
- if not self.object.allowed_languages.filter(id=form.cleaned_data['language'].id).exists():
+ return HttpResponse(
+ format_html("{0} ", _("You submitted too many submissions.")),
+ status=429,
+ )
+ if not self.object.allowed_languages.filter(
+ id=form.cleaned_data["language"].id
+ ).exists():
raise PermissionDenied()
- if not self.request.user.is_superuser and self.object.banned_users.filter(id=self.request.profile.id).exists():
- return generic_message(self.request, _('Banned from submitting'),
- _('You have been declared persona non grata for this problem. '
- 'You are permanently barred from submitting to this problem.'))
+ if (
+ not self.request.user.is_superuser
+ and self.object.banned_users.filter(id=self.request.profile.id).exists()
+ ):
+ return generic_message(
+ self.request,
+ _("Banned from submitting"),
+ _(
+ "You have been declared persona non grata for this problem. "
+ "You are permanently barred from submitting to this problem."
+ ),
+ )
# Must check for zero and not None. None means infinite submissions remaining.
if self.remaining_submission_count == 0:
- return generic_message(self.request, _('Too many submissions'),
- _('You have exceeded the submission limit for this problem.'))
+ return generic_message(
+ self.request,
+ _("Too many submissions"),
+ _("You have exceeded the submission limit for this problem."),
+ )
with transaction.atomic():
self.new_submission = form.save(commit=False)
@@ -717,9 +955,13 @@ def form_valid(self, form):
if contest_problem is not None:
# Use the contest object from current_contest.contest because we already use it
# in profile.update_contest().
- self.new_submission.contest_object = self.request.profile.current_contest.contest
+ self.new_submission.contest_object = (
+ self.request.profile.current_contest.contest
+ )
if self.request.profile.current_contest.live:
- self.new_submission.locked_after = self.new_submission.contest_object.locked_after
+ self.new_submission.locked_after = (
+ self.new_submission.contest_object.locked_after
+ )
self.new_submission.save()
ContestSubmission(
submission=self.new_submission,
@@ -729,23 +971,27 @@ def form_valid(self, form):
else:
self.new_submission.save()
- source = SubmissionSource(submission=self.new_submission, source=form.cleaned_data['source'])
+ source = SubmissionSource(
+ submission=self.new_submission, source=form.cleaned_data["source"]
+ )
source.save()
# Save a query.
self.new_submission.source = source
- self.new_submission.judge(force_judge=True, judge_id=form.cleaned_data['judge'])
+ self.new_submission.judge(force_judge=True, judge_id=form.cleaned_data["judge"])
return super().form_valid(form)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
- context['langs'] = Language.objects.all()
- context['no_judges'] = not context['form'].fields['language'].queryset
- context['submission_limit'] = self.contest_problem and self.contest_problem.max_submissions
- context['submissions_left'] = self.remaining_submission_count
- context['ACE_URL'] = settings.ACE_URL
- context['default_lang'] = self.default_language
+ context["langs"] = Language.objects.all()
+ context["no_judges"] = not context["form"].fields["language"].queryset
+ context["submission_limit"] = (
+ self.contest_problem and self.contest_problem.max_submissions
+ )
+ context["submissions_left"] = self.remaining_submission_count
+ context["ACE_URL"] = settings.ACE_URL
+ context["default_lang"] = self.default_language
return context
def post(self, request, *args, **kwargs):
@@ -754,20 +1000,25 @@ def post(self, request, *args, **kwargs):
except Http404:
# Is this really necessary? This entire post() method could be removed if we don't log this.
user_logger.info(
- 'Naughty user %s wants to submit to %s without permission',
+ "Naughty user %s wants to submit to %s without permission",
request.user.username,
kwargs.get(self.slug_url_kwarg),
)
- return HttpResponseForbidden(format_html('{0} ', _('Do you want me to ban you?')))
+ return HttpResponseForbidden(
+ format_html("{0} ", _("Do you want me to ban you?"))
+ )
def dispatch(self, request, *args, **kwargs):
- submission_id = kwargs.get('submission')
+ submission_id = kwargs.get("submission")
if submission_id is not None:
self.old_submission = get_object_or_404(
- Submission.objects.select_related('source', 'language'),
+ Submission.objects.select_related("source", "language"),
id=submission_id,
)
- if not request.user.has_perm('judge.resubmit_other') and self.old_submission.user != request.profile:
+ if (
+ not request.user.has_perm("judge.resubmit_other")
+ and self.old_submission.user != request.profile
+ ):
raise PermissionDenied()
else:
self.old_submission = None
@@ -775,11 +1026,13 @@ def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
-class ProblemClone(ProblemMixin, PermissionRequiredMixin, TitleMixin, SingleObjectFormView):
- title = gettext_lazy('Clone Problem')
- template_name = 'problem/clone.html'
+class ProblemClone(
+ ProblemMixin, PermissionRequiredMixin, TitleMixin, SingleObjectFormView
+):
+ title = gettext_lazy("Clone Problem")
+ template_name = "problem/clone.html"
form_class = ProblemCloneForm
- permission_required = 'judge.clone_problem'
+ permission_required = "judge.clone_problem"
def form_valid(self, form):
problem = self.object
@@ -794,7 +1047,7 @@ def form_valid(self, form):
problem.is_public = False
problem.ac_rate = 0
problem.user_count = 0
- problem.code = form.cleaned_data['code']
+ problem.code = form.cleaned_data["code"]
with revisions.create_revision(atomic=True):
problem.save()
problem.authors.add(self.request.profile)
@@ -803,6 +1056,8 @@ def form_valid(self, form):
problem.organizations.set(organizations)
problem.types.set(types)
revisions.set_user(self.request.user)
- revisions.set_comment(_('Cloned problem from %s') % old_code)
+ revisions.set_comment(_("Cloned problem from %s") % old_code)
- return HttpResponseRedirect(reverse('admin:judge_problem_change', args=(problem.id,)))
+ return HttpResponseRedirect(
+ reverse("admin:judge_problem_change", args=(problem.id,))
+ )
diff --git a/judge/views/problem_data.py b/judge/views/problem_data.py
index 87aac8b84b..7679e27af9 100644
--- a/judge/views/problem_data.py
+++ b/judge/views/problem_data.py
@@ -8,7 +8,14 @@
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import ValidationError
-from django.forms import BaseModelFormSet, HiddenInput, ModelForm, NumberInput, Select, formset_factory
+from django.forms import (
+ BaseModelFormSet,
+ HiddenInput,
+ ModelForm,
+ NumberInput,
+ Select,
+ formset_factory,
+)
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
@@ -18,43 +25,49 @@
from django.views.generic import DetailView
from judge.highlight_code import highlight_code
-from judge.models import Problem, ProblemData, ProblemTestCase, Submission, problem_data_storage
+from judge.models import (
+ Problem,
+ ProblemData,
+ ProblemTestCase,
+ Submission,
+ problem_data_storage,
+)
from judge.utils.problem_data import ProblemDataCompiler
from judge.utils.unicode import utf8text
from judge.utils.views import TitleMixin, add_file_response
from judge.views.problem import ProblemMixin
mimetypes.init()
-mimetypes.add_type('application/x-yaml', '.yml')
+mimetypes.add_type("application/x-yaml", ".yml")
def checker_args_cleaner(self):
- data = self.cleaned_data['checker_args']
+ data = self.cleaned_data["checker_args"]
if not data or data.isspace():
- return ''
+ return ""
try:
if not isinstance(json.loads(data), dict):
- raise ValidationError(_('Checker arguments must be a JSON object.'))
+ raise ValidationError(_("Checker arguments must be a JSON object."))
except ValueError:
- raise ValidationError(_('Checker arguments is invalid JSON.'))
+ raise ValidationError(_("Checker arguments is invalid JSON."))
return data
class ProblemDataForm(ModelForm):
def clean_zipfile(self):
- if hasattr(self, 'zip_valid') and not self.zip_valid:
- raise ValidationError(_('Your zip file is invalid!'))
+ if hasattr(self, "zip_valid") and not self.zip_valid:
+ raise ValidationError(_("Your zip file is invalid!"))
- zipfile = self.cleaned_data['zipfile']
- if zipfile and not zipfile.name.endswith('.zip'):
+ zipfile = self.cleaned_data["zipfile"]
+ if zipfile and not zipfile.name.endswith(".zip"):
raise ValidationError(_("Zip files must end in '.zip'"))
return zipfile
def clean_generator(self):
- generator = self.cleaned_data['generator']
- if generator and generator.name == 'init.yml':
- raise ValidationError(_('Generators must not be named init.yml.'))
+ generator = self.cleaned_data["generator"]
+ if generator and generator.name == "init.yml":
+ raise ValidationError(_("Generators must not be named init.yml."))
return generator
@@ -62,10 +75,18 @@ def clean_generator(self):
class Meta:
model = ProblemData
- fields = ['zipfile', 'generator', 'unicode', 'nobigmath', 'output_limit', 'output_prefix',
- 'checker', 'checker_args']
+ fields = [
+ "zipfile",
+ "generator",
+ "unicode",
+ "nobigmath",
+ "output_limit",
+ "output_prefix",
+ "checker",
+ "checker_args",
+ ]
widgets = {
- 'checker_args': HiddenInput,
+ "checker_args": HiddenInput,
}
@@ -74,24 +95,38 @@ class ProblemCaseForm(ModelForm):
class Meta:
model = ProblemTestCase
- fields = ('order', 'type', 'input_file', 'output_file', 'points',
- 'is_pretest', 'output_limit', 'output_prefix', 'checker', 'checker_args', 'generator_args')
+ fields = (
+ "order",
+ "type",
+ "input_file",
+ "output_file",
+ "points",
+ "is_pretest",
+ "output_limit",
+ "output_prefix",
+ "checker",
+ "checker_args",
+ "generator_args",
+ )
widgets = {
- 'generator_args': HiddenInput,
- 'type': Select(attrs={'style': 'width: 100%'}),
- 'points': NumberInput(attrs={'style': 'width: 4em'}),
- 'output_prefix': NumberInput(attrs={'style': 'width: 4.5em'}),
- 'output_limit': NumberInput(attrs={'style': 'width: 6em'}),
- 'checker_args': HiddenInput,
+ "generator_args": HiddenInput,
+ "type": Select(attrs={"style": "width: 100%"}),
+ "points": NumberInput(attrs={"style": "width: 4em"}),
+ "output_prefix": NumberInput(attrs={"style": "width: 4.5em"}),
+ "output_limit": NumberInput(attrs={"style": "width: 6em"}),
+ "checker_args": HiddenInput,
}
-class ProblemCaseFormSet(formset_factory(ProblemCaseForm, formset=BaseModelFormSet, extra=1, max_num=1,
- can_delete=True)):
+class ProblemCaseFormSet(
+ formset_factory(
+ ProblemCaseForm, formset=BaseModelFormSet, extra=1, max_num=1, can_delete=True
+ )
+):
model = ProblemTestCase
def __init__(self, *args, **kwargs):
- self.valid_files = kwargs.pop('valid_files', None)
+ self.valid_files = kwargs.pop("valid_files", None)
super(ProblemCaseFormSet, self).__init__(*args, **kwargs)
def _construct_form(self, i, **kwargs):
@@ -111,15 +146,21 @@ def get_object(self, queryset=None):
class ProblemSubmissionDiff(TitleMixin, ProblemMixin, DetailView):
- template_name = 'problem/submission-diff.html'
+ template_name = "problem/submission-diff.html"
def get_title(self):
- return _('Comparing submissions for {0}').format(self.object.name)
+ return _("Comparing submissions for {0}").format(self.object.name)
def get_content_title(self):
- return mark_safe(escape(_('Comparing submissions for {0}')).format(
- format_html('{0} ', self.object.name, reverse('problem_detail', args=[self.object.code])),
- ))
+ return mark_safe(
+ escape(_("Comparing submissions for {0}")).format(
+ format_html(
+ '{0} ',
+ self.object.name,
+ reverse("problem_detail", args=[self.object.code]),
+ ),
+ )
+ )
def get_object(self, queryset=None):
problem = super(ProblemSubmissionDiff, self).get_object(queryset)
@@ -130,51 +171,67 @@ def get_object(self, queryset=None):
def get_context_data(self, **kwargs):
context = super(ProblemSubmissionDiff, self).get_context_data(**kwargs)
try:
- ids = self.request.GET.getlist('id')
+ ids = self.request.GET.getlist("id")
subs = Submission.objects.filter(id__in=ids)
except ValueError:
raise Http404
if not subs:
raise Http404
- context['submissions'] = subs
+ context["submissions"] = subs
# If we have associated data we can do better than just guess
- data = ProblemTestCase.objects.filter(dataset=self.object, type='C')
+ data = ProblemTestCase.objects.filter(dataset=self.object, type="C")
if data:
num_cases = data.count()
else:
num_cases = subs.first().test_cases.count()
- context['num_cases'] = num_cases
+ context["num_cases"] = num_cases
return context
class ProblemDataView(TitleMixin, ProblemManagerMixin):
- template_name = 'problem/data.html'
+ template_name = "problem/data.html"
def get_title(self):
- return _('Editing data for {0}').format(self.object.name)
+ return _("Editing data for {0}").format(self.object.name)
def get_content_title(self):
- return mark_safe(escape(_('Editing data for %s')) % (
- format_html('{0} ', self.object.name,
- reverse('problem_detail', args=[self.object.code]))))
+ return mark_safe(
+ escape(_("Editing data for %s"))
+ % (
+ format_html(
+ '{0} ',
+ self.object.name,
+ reverse("problem_detail", args=[self.object.code]),
+ )
+ )
+ )
def get_data_form(self, post=False):
- return ProblemDataForm(data=self.request.POST if post else None, prefix='problem-data',
- files=self.request.FILES if post else None,
- instance=ProblemData.objects.get_or_create(problem=self.object)[0])
+ return ProblemDataForm(
+ data=self.request.POST if post else None,
+ prefix="problem-data",
+ files=self.request.FILES if post else None,
+ instance=ProblemData.objects.get_or_create(problem=self.object)[0],
+ )
def get_case_formset(self, files, post=False):
- return ProblemCaseFormSet(data=self.request.POST if post else None, prefix='cases', valid_files=files,
- queryset=ProblemTestCase.objects.filter(dataset_id=self.object.pk).order_by('order'))
+ return ProblemCaseFormSet(
+ data=self.request.POST if post else None,
+ prefix="cases",
+ valid_files=files,
+ queryset=ProblemTestCase.objects.filter(dataset_id=self.object.pk).order_by(
+ "order"
+ ),
+ )
def get_valid_files(self, data, post=False):
try:
- if post and 'problem-data-zipfile-clear' in self.request.POST:
+ if post and "problem-data-zipfile-clear" in self.request.POST:
return []
- elif post and 'problem-data-zipfile' in self.request.FILES:
- return ZipFile(self.request.FILES['problem-data-zipfile']).namelist()
+ elif post and "problem-data-zipfile" in self.request.FILES:
+ return ZipFile(self.request.FILES["problem-data-zipfile"]).namelist()
elif data.zipfile:
return ZipFile(data.zipfile.path).namelist()
except BadZipfile:
@@ -183,14 +240,18 @@ def get_valid_files(self, data, post=False):
def get_context_data(self, **kwargs):
context = super(ProblemDataView, self).get_context_data(**kwargs)
- if 'data_form' not in context:
- context['data_form'] = self.get_data_form()
- valid_files = context['valid_files'] = self.get_valid_files(context['data_form'].instance)
- context['cases_formset'] = self.get_case_formset(valid_files)
- if context['valid_files']:
- context['valid_files_json'] = mark_safe(json.dumps(context['valid_files']))
- context['valid_files'] = set(context['valid_files'])
- context['all_case_forms'] = chain(context['cases_formset'], [context['cases_formset'].empty_form])
+ if "data_form" not in context:
+ context["data_form"] = self.get_data_form()
+ valid_files = context["valid_files"] = self.get_valid_files(
+ context["data_form"].instance
+ )
+ context["cases_formset"] = self.get_case_formset(valid_files)
+ if context["valid_files"]:
+ context["valid_files_json"] = mark_safe(json.dumps(context["valid_files"]))
+ context["valid_files"] = set(context["valid_files"])
+ context["all_case_forms"] = chain(
+ context["cases_formset"], [context["cases_formset"].empty_form]
+ )
return context
def post(self, request, *args, **kwargs):
@@ -206,10 +267,17 @@ def post(self, request, *args, **kwargs):
case.save()
for case in cases_formset.deleted_objects:
case.delete()
- ProblemDataCompiler.generate(problem, data, problem.cases.order_by('order'), valid_files)
+ ProblemDataCompiler.generate(
+ problem, data, problem.cases.order_by("order"), valid_files
+ )
return HttpResponseRedirect(request.get_full_path())
- return self.render_to_response(self.get_context_data(data_form=data_form, cases_formset=cases_formset,
- valid_files=valid_files))
+ return self.render_to_response(
+ self.get_context_data(
+ data_form=data_form,
+ cases_formset=cases_formset,
+ valid_files=valid_files,
+ )
+ )
put = post
@@ -221,22 +289,33 @@ def problem_data_file(request, problem, path):
raise Http404()
problem_dir = problem_data_storage.path(problem)
- if os.path.commonpath((problem_data_storage.path(os.path.join(problem, path)), problem_dir)) != problem_dir:
+ if (
+ os.path.commonpath(
+ (problem_data_storage.path(os.path.join(problem, path)), problem_dir)
+ )
+ != problem_dir
+ ):
raise Http404()
response = HttpResponse()
- if hasattr(settings, 'DMOJ_PROBLEM_DATA_INTERNAL'):
- url_path = '%s/%s/%s' % (settings.DMOJ_PROBLEM_DATA_INTERNAL, problem, path)
+ if hasattr(settings, "DMOJ_PROBLEM_DATA_INTERNAL"):
+ url_path = "%s/%s/%s" % (settings.DMOJ_PROBLEM_DATA_INTERNAL, problem, path)
else:
url_path = None
try:
- add_file_response(request, response, url_path, os.path.join(problem, path), problem_data_storage)
+ add_file_response(
+ request,
+ response,
+ url_path,
+ os.path.join(problem, path),
+ problem_data_storage,
+ )
except IOError:
raise Http404()
- response['Content-Type'] = 'application/octet-stream'
+ response["Content-Type"] = "application/octet-stream"
return response
@@ -247,15 +326,29 @@ def problem_init_view(request, problem):
raise Http404()
try:
- with problem_data_storage.open(os.path.join(problem.code, 'init.yml'), 'rb') as f:
- data = utf8text(f.read()).rstrip('\n')
+ with problem_data_storage.open(
+ os.path.join(problem.code, "init.yml"), "rb"
+ ) as f:
+ data = utf8text(f.read()).rstrip("\n")
except IOError:
raise Http404()
- return render(request, 'problem/yaml.html', {
- 'raw_source': data, 'highlighted_source': highlight_code(data, 'yaml'),
- 'title': _('Generated init.yml for %s') % problem.name,
- 'content_title': mark_safe(escape(_('Generated init.yml for %s')) % (
- format_html('{0} ', problem.name,
- reverse('problem_detail', args=[problem.code])))),
- })
+ return render(
+ request,
+ "problem/yaml.html",
+ {
+ "raw_source": data,
+ "highlighted_source": highlight_code(data, "yaml"),
+ "title": _("Generated init.yml for %s") % problem.name,
+ "content_title": mark_safe(
+ escape(_("Generated init.yml for %s"))
+ % (
+ format_html(
+ '{0} ',
+ problem.name,
+ reverse("problem_detail", args=[problem.code]),
+ )
+ )
+ ),
+ },
+ )
diff --git a/judge/views/problem_manage.py b/judge/views/problem_manage.py
index 5857b0cc55..63a084eec1 100644
--- a/judge/views/problem_manage.py
+++ b/judge/views/problem_manage.py
@@ -3,7 +3,12 @@
from celery.result import AsyncResult
from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin
-from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect
+from django.http import (
+ Http404,
+ HttpResponse,
+ HttpResponseBadRequest,
+ HttpResponseRedirect,
+)
from django.urls import reverse
from django.utils.html import escape, format_html
from django.utils.safestring import mark_safe
@@ -44,33 +49,46 @@ def post(self, request, *args, **kwargs):
class ManageProblemSubmissionView(TitleMixin, ManageProblemSubmissionMixin, DetailView):
- template_name = 'problem/manage_submission.html'
+ template_name = "problem/manage_submission.html"
def get_title(self):
- return _('Managing submissions for %s') % (self.object.name,)
+ return _("Managing submissions for %s") % (self.object.name,)
def get_content_title(self):
- return mark_safe(escape(_('Managing submissions for %s')) % (
- format_html('{0} ', self.object.name,
- reverse('problem_detail', args=[self.object.code]))))
+ return mark_safe(
+ escape(_("Managing submissions for %s"))
+ % (
+ format_html(
+ '{0} ',
+ self.object.name,
+ reverse("problem_detail", args=[self.object.code]),
+ )
+ )
+ )
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
- context['submission_count'] = self.object.submission_set.count()
- context['languages'] = [(lang_id, short_name or key) for lang_id, key, short_name in
- Language.objects.values_list('id', 'key', 'short_name')]
- context['results'] = sorted(map(itemgetter(0), Submission.RESULT))
+ context["submission_count"] = self.object.submission_set.count()
+ context["languages"] = [
+ (lang_id, short_name or key)
+ for lang_id, key, short_name in Language.objects.values_list(
+ "id", "key", "short_name"
+ )
+ ]
+ context["results"] = sorted(map(itemgetter(0), Submission.RESULT))
return context
-class BaseRejudgeSubmissionsView(PermissionRequiredMixin, ManageProblemSubmissionActionMixin, BaseDetailView):
- permission_required = 'judge.rejudge_submission_lot'
+class BaseRejudgeSubmissionsView(
+ PermissionRequiredMixin, ManageProblemSubmissionActionMixin, BaseDetailView
+):
+ permission_required = "judge.rejudge_submission_lot"
def perform_action(self):
- if self.request.POST.get('use_range', 'off') == 'on':
+ if self.request.POST.get("use_range", "off") == "on":
try:
- start = int(self.request.POST.get('start'))
- end = int(self.request.POST.get('end'))
+ start = int(self.request.POST.get("start"))
+ end = int(self.request.POST.get("end"))
except (KeyError, ValueError):
return HttpResponseBadRequest()
id_range = (start, end)
@@ -78,11 +96,13 @@ def perform_action(self):
id_range = None
try:
- languages = list(map(int, self.request.POST.getlist('language')))
+ languages = list(map(int, self.request.POST.getlist("language")))
except ValueError:
return HttpResponseBadRequest()
- return self.generate_response(id_range, languages, self.request.POST.getlist('result'))
+ return self.generate_response(
+ id_range, languages, self.request.POST.getlist("result")
+ )
def generate_response(self, id_range, languages, results):
raise NotImplementedError()
@@ -90,17 +110,24 @@ def generate_response(self, id_range, languages, results):
class RejudgeSubmissionsView(BaseRejudgeSubmissionsView):
def generate_response(self, id_range, languages, results):
- status = rejudge_problem_filter.delay(self.object.id, id_range, languages, results,
- user_id=self.request.user.id)
+ status = rejudge_problem_filter.delay(
+ self.object.id, id_range, languages, results, user_id=self.request.user.id
+ )
return redirect_to_task_status(
- status, message=_('Rejudging selected submissions for %s...') % (self.object.name,),
- redirect=reverse('problem_submissions_rejudge_success', args=[self.object.code, status.id]),
+ status,
+ message=_("Rejudging selected submissions for %s...") % (self.object.name,),
+ redirect=reverse(
+ "problem_submissions_rejudge_success",
+ args=[self.object.code, status.id],
+ ),
)
class PreviewRejudgeSubmissionsView(BaseRejudgeSubmissionsView):
def generate_response(self, id_range, languages, results):
- queryset = apply_submission_filter(self.object.submission_set.all(), id_range, languages, results)
+ queryset = apply_submission_filter(
+ self.object.submission_set.all(), id_range, languages, results
+ )
return HttpResponse(str(queryset.count()))
@@ -108,8 +135,12 @@ class RescoreAllSubmissionsView(ManageProblemSubmissionActionMixin, BaseDetailVi
def perform_action(self):
status = rescore_problem.delay(self.object.id)
return redirect_to_task_status(
- status, message=_('Rescoring all submissions for %s...') % (self.object.name,),
- redirect=reverse('problem_submissions_rescore_success', args=[self.object.code, status.id]),
+ status,
+ message=_("Rescoring all submissions for %s...") % (self.object.name,),
+ redirect=reverse(
+ "problem_submissions_rescore_success",
+ args=[self.object.code, status.id],
+ ),
)
@@ -117,15 +148,29 @@ def rejudge_success(request, problem, task_id):
count = AsyncResult(task_id).result
if not isinstance(count, int):
raise Http404()
- messages.success(request, ngettext('Successfully scheduled %d submission for rejudging.',
- 'Successfully scheduled %d submissions for rejudging.', count) % (count,))
- return HttpResponseRedirect(reverse('problem_manage_submissions', args=[problem]))
+ messages.success(
+ request,
+ ngettext(
+ "Successfully scheduled %d submission for rejudging.",
+ "Successfully scheduled %d submissions for rejudging.",
+ count,
+ )
+ % (count,),
+ )
+ return HttpResponseRedirect(reverse("problem_manage_submissions", args=[problem]))
def rescore_success(request, problem, task_id):
count = AsyncResult(task_id).result
if not isinstance(count, int):
raise Http404()
- messages.success(request, ngettext('%d submission was successfully rescored.',
- '%d submissions were successfully rescored.', count) % (count,))
- return HttpResponseRedirect(reverse('problem_manage_submissions', args=[problem]))
+ messages.success(
+ request,
+ ngettext(
+ "%d submission was successfully rescored.",
+ "%d submissions were successfully rescored.",
+ count,
+ )
+ % (count,),
+ )
+ return HttpResponseRedirect(reverse("problem_manage_submissions", args=[problem]))
diff --git a/judge/views/ranked_submission.py b/judge/views/ranked_submission.py
index d2f9473fb7..4d73bf4d24 100644
--- a/judge/views/ranked_submission.py
+++ b/judge/views/ranked_submission.py
@@ -8,33 +8,41 @@
from judge.utils.raw_sql import join_sql_subquery
from judge.views.submission import ForceContestMixin, ProblemSubmissions
-__all__ = ['RankedSubmissions', 'ContestRankedSubmission']
+__all__ = ["RankedSubmissions", "ContestRankedSubmission"]
class RankedSubmissions(ProblemSubmissions):
- tab = 'best_submissions_list'
+ tab = "best_submissions_list"
dynamic_update = False
def get_queryset(self):
params = [self.problem.id]
if self.in_contest:
- contest_join = 'INNER JOIN judge_contestsubmission AS cs ON (sub.id = cs.submission_id)'
- points = 'cs.points'
- constraint = ' AND sub.contest_object_id = %s'
+ contest_join = "INNER JOIN judge_contestsubmission AS cs ON (sub.id = cs.submission_id)"
+ points = "cs.points"
+ constraint = " AND sub.contest_object_id = %s"
params.append(self.contest.id)
else:
- contest_join = ''
- points = 'sub.points'
- constraint = ''
+ contest_join = ""
+ points = "sub.points"
+ constraint = ""
if self.selected_languages:
- lang_ids = Language.objects.filter(key__in=self.selected_languages).values_list('id', flat=True)
+ lang_ids = Language.objects.filter(
+ key__in=self.selected_languages
+ ).values_list("id", flat=True)
if lang_ids:
- constraint += f' AND sub.language_id IN ({", ".join(["%s"] * len(lang_ids))})'
+ constraint += (
+ f' AND sub.language_id IN ({", ".join(["%s"] * len(lang_ids))})'
+ )
params.extend(lang_ids)
self.selected_languages = set()
- queryset = super(RankedSubmissions, self).get_queryset().filter(user__is_unlisted=False)
+ queryset = (
+ super(RankedSubmissions, self)
+ .get_queryset()
+ .filter(user__is_unlisted=False)
+ )
join_sql_subquery(
queryset,
subquery="""
@@ -54,23 +62,34 @@ def get_queryset(self):
ON (sub.user_id = fastest.uid AND sub.time = fastest.time)
WHERE sub.problem_id = %s {constraint}
GROUP BY sub.user_id
- """.format(points=points, contest_join=contest_join, constraint=constraint),
- params=params * 3, alias='best_subs', join_fields=[('id', 'id')], related_model=Submission,
+ """.format(
+ points=points, contest_join=contest_join, constraint=constraint
+ ),
+ params=params * 3,
+ alias="best_subs",
+ join_fields=[("id", "id")],
+ related_model=Submission,
)
if self.in_contest:
- return queryset.order_by('-contest__points', 'time')
+ return queryset.order_by("-contest__points", "time")
else:
- return queryset.order_by('-points', 'time')
+ return queryset.order_by("-points", "time")
def get_title(self):
- return _('Best solutions for %s') % self.problem_name
+ return _("Best solutions for %s") % self.problem_name
def get_content_title(self):
- return mark_safe(escape(_('Best solutions for %s')) % (
- format_html('{0} ', self.problem_name,
- reverse('problem_detail', args=[self.problem.code])),
- ))
+ return mark_safe(
+ escape(_("Best solutions for %s"))
+ % (
+ format_html(
+ '{0} ',
+ self.problem_name,
+ reverse("problem_detail", args=[self.problem.code]),
+ ),
+ )
+ )
def _get_result_data(self, queryset=None):
if queryset is None:
@@ -81,26 +100,43 @@ def _get_result_data(self, queryset=None):
class ContestRankedSubmission(ForceContestMixin, RankedSubmissions):
def get_title(self):
if self.problem.is_accessible_by(self.request.user):
- return _('Best solutions for %(problem)s in %(contest)s') % {
- 'problem': self.problem_name, 'contest': self.contest.name,
+ return _("Best solutions for %(problem)s in %(contest)s") % {
+ "problem": self.problem_name,
+ "contest": self.contest.name,
}
- return _('Best solutions for problem %(number)s in %(contest)s') % {
- 'number': self.get_problem_number(self.problem), 'contest': self.contest.name,
+ return _("Best solutions for problem %(number)s in %(contest)s") % {
+ "number": self.get_problem_number(self.problem),
+ "contest": self.contest.name,
}
def get_content_title(self):
if self.problem.is_accessible_by(self.request.user):
- return mark_safe(escape(_('Best solutions for %(problem)s in %(contest)s')) % {
- 'problem': format_html('{0} ', self.problem_name,
- reverse('problem_detail', args=[self.problem.code])),
- 'contest': format_html('{0} ', self.contest.name,
- reverse('contest_view', args=[self.contest.key])),
- })
- return mark_safe(escape(_('Best solutions for problem %(number)s in %(contest)s')) % {
- 'number': self.get_problem_number(self.problem),
- 'contest': format_html('{0} ', self.contest.name,
- reverse('contest_view', args=[self.contest.key])),
- })
+ return mark_safe(
+ escape(_("Best solutions for %(problem)s in %(contest)s"))
+ % {
+ "problem": format_html(
+ '{0} ',
+ self.problem_name,
+ reverse("problem_detail", args=[self.problem.code]),
+ ),
+ "contest": format_html(
+ '{0} ',
+ self.contest.name,
+ reverse("contest_view", args=[self.contest.key]),
+ ),
+ }
+ )
+ return mark_safe(
+ escape(_("Best solutions for problem %(number)s in %(contest)s"))
+ % {
+ "number": self.get_problem_number(self.problem),
+ "contest": format_html(
+ '{0} ',
+ self.contest.name,
+ reverse("contest_view", args=[self.contest.key]),
+ ),
+ }
+ )
def _get_queryset(self):
return super()._get_queryset().filter(contest_object=self.contest)
diff --git a/judge/views/register.py b/judge/views/register.py
index f5cdd64029..8711e92775 100644
--- a/judge/views/register.py
+++ b/judge/views/register.py
@@ -6,8 +6,10 @@
from django.forms import ChoiceField, ModelChoiceField
from django.shortcuts import render
from django.utils.translation import gettext, gettext_lazy as _, ngettext
-from registration.backends.default.views import (ActivationView as OldActivationView,
- RegistrationView as OldRegistrationView)
+from registration.backends.default.views import (
+ ActivationView as OldActivationView,
+ RegistrationView as OldRegistrationView,
+)
from registration.forms import RegistrationForm
from sortedm2m.forms import SortedMultipleChoiceField
@@ -19,88 +21,121 @@
class CustomRegistrationForm(RegistrationForm):
- username = forms.RegexField(regex=r'^\w+$', max_length=30, label=_('Username'),
- error_messages={'invalid': _('A username must contain letters, '
- 'numbers, or underscores.')})
- timezone = ChoiceField(label=_('Timezone'), choices=TIMEZONE,
- widget=Select2Widget(attrs={'style': 'width:100%'}))
- language = ModelChoiceField(queryset=Language.objects.all(), label=_('Preferred language'), empty_label=None,
- widget=Select2Widget(attrs={'style': 'width:100%'}))
- organizations = SortedMultipleChoiceField(queryset=Organization.objects.filter(is_open=True),
- label=_('Organizations'), required=False,
- widget=Select2MultipleWidget(attrs={'style': 'width:100%'}))
+ username = forms.RegexField(
+ regex=r"^\w+$",
+ max_length=30,
+ label=_("Username"),
+ error_messages={
+ "invalid": _("A username must contain letters, " "numbers, or underscores.")
+ },
+ )
+ timezone = ChoiceField(
+ label=_("Timezone"),
+ choices=TIMEZONE,
+ widget=Select2Widget(attrs={"style": "width:100%"}),
+ )
+ language = ModelChoiceField(
+ queryset=Language.objects.all(),
+ label=_("Preferred language"),
+ empty_label=None,
+ widget=Select2Widget(attrs={"style": "width:100%"}),
+ )
+ organizations = SortedMultipleChoiceField(
+ queryset=Organization.objects.filter(is_open=True),
+ label=_("Organizations"),
+ required=False,
+ widget=Select2MultipleWidget(attrs={"style": "width:100%"}),
+ )
if newsletter_id is not None:
- newsletter = forms.BooleanField(label=_('Subscribe to newsletter?'), initial=True, required=False)
+ newsletter = forms.BooleanField(
+ label=_("Subscribe to newsletter?"), initial=True, required=False
+ )
if ReCaptchaField is not None:
captcha = ReCaptchaField(widget=ReCaptchaWidget())
def clean_email(self):
- if User.objects.filter(email=self.cleaned_data['email']).exists():
- raise forms.ValidationError(gettext('The email address "%s" is already taken. Only one registration '
- 'is allowed per address.') % self.cleaned_data['email'])
- validate_email_domain(self.cleaned_data['email'])
- return self.cleaned_data['email']
+ if User.objects.filter(email=self.cleaned_data["email"]).exists():
+ raise forms.ValidationError(
+ gettext(
+ 'The email address "%s" is already taken. Only one registration '
+ "is allowed per address."
+ )
+ % self.cleaned_data["email"]
+ )
+ validate_email_domain(self.cleaned_data["email"])
+ return self.cleaned_data["email"]
def clean_organizations(self):
- organizations = self.cleaned_data.get('organizations') or []
+ organizations = self.cleaned_data.get("organizations") or []
max_orgs = settings.DMOJ_USER_MAX_ORGANIZATION_COUNT
if len(organizations) > max_orgs:
- raise forms.ValidationError(ngettext('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['organizations']
+ raise forms.ValidationError(
+ ngettext(
+ "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["organizations"]
class RegistrationView(OldRegistrationView):
- title = _('Register')
+ title = _("Register")
form_class = CustomRegistrationForm
- template_name = 'registration/registration_form.html'
+ template_name = "registration/registration_form.html"
def get_context_data(self, **kwargs):
- if 'title' not in kwargs:
- kwargs['title'] = self.title
- kwargs['TIMEZONE_MAP'] = settings.TIMEZONE_MAP
- kwargs['password_validators'] = get_default_password_validators()
- kwargs['tos_url'] = settings.TERMS_OF_SERVICE_URL
+ if "title" not in kwargs:
+ kwargs["title"] = self.title
+ kwargs["TIMEZONE_MAP"] = settings.TIMEZONE_MAP
+ kwargs["password_validators"] = get_default_password_validators()
+ kwargs["tos_url"] = settings.TERMS_OF_SERVICE_URL
return super(RegistrationView, self).get_context_data(**kwargs)
def register(self, form):
user = super(RegistrationView, self).register(form)
- profile, _ = Profile.objects.get_or_create(user=user, defaults={
- 'language': Language.get_default_language(),
- })
+ profile, _ = Profile.objects.get_or_create(
+ user=user,
+ defaults={
+ "language": Language.get_default_language(),
+ },
+ )
cleaned_data = form.cleaned_data
- profile.timezone = cleaned_data['timezone']
- profile.language = cleaned_data['language']
- profile.organizations.add(*cleaned_data['organizations'])
+ profile.timezone = cleaned_data["timezone"]
+ profile.language = cleaned_data["language"]
+ profile.organizations.add(*cleaned_data["organizations"])
profile.save()
- if newsletter_id is not None and cleaned_data['newsletter']:
+ if newsletter_id is not None and cleaned_data["newsletter"]:
Subscription(user=user, newsletter_id=newsletter_id, subscribed=True).save()
return user
def get_initial(self, *args, **kwargs):
initial = super(RegistrationView, self).get_initial(*args, **kwargs)
- initial['timezone'] = settings.DEFAULT_USER_TIME_ZONE
- initial['language'] = Language.objects.get(key=settings.DEFAULT_USER_LANGUAGE)
+ initial["timezone"] = settings.DEFAULT_USER_TIME_ZONE
+ initial["language"] = Language.objects.get(key=settings.DEFAULT_USER_LANGUAGE)
return initial
class ActivationView(OldActivationView):
- title = _('Activation Key Invalid')
- template_name = 'registration/activate.html'
+ title = _("Activation Key Invalid")
+ template_name = "registration/activate.html"
def get_context_data(self, **kwargs):
- if 'title' not in kwargs:
- kwargs['title'] = self.title
+ if "title" not in kwargs:
+ kwargs["title"] = self.title
return super(ActivationView, self).get_context_data(**kwargs)
def social_auth_error(request):
- return render(request, 'generic-message.html', {
- 'title': gettext('Authentication failure'),
- 'message': request.GET.get('message'),
- })
+ return render(
+ request,
+ "generic-message.html",
+ {
+ "title": gettext("Authentication failure"),
+ "message": request.GET.get("message"),
+ },
+ )
diff --git a/judge/views/select2.py b/judge/views/select2.py
index 2b7fb47698..5926b3947f 100644
--- a/judge/views/select2.py
+++ b/judge/views/select2.py
@@ -10,7 +10,7 @@
def _get_user_queryset(term):
qs = Profile.objects
- if term.endswith(' '):
+ if term.endswith(" "):
qs = qs.filter(user__username=term.strip())
else:
qs = qs.filter(user__username__icontains=term)
@@ -22,18 +22,22 @@ class Select2View(BaseListView):
def get(self, request, *args, **kwargs):
self.request = request
- self.term = kwargs.get('term', request.GET.get('term', ''))
+ self.term = kwargs.get("term", request.GET.get("term", ""))
self.object_list = self.get_queryset()
context = self.get_context_data()
- return JsonResponse({
- 'results': [
- {
- 'text': smart_str(self.get_name(obj)),
- 'id': obj.pk,
- } for obj in context['object_list']],
- 'more': context['page_obj'].has_next(),
- })
+ return JsonResponse(
+ {
+ "results": [
+ {
+ "text": smart_str(self.get_name(obj)),
+ "id": obj.pk,
+ }
+ for obj in context["object_list"]
+ ],
+ "more": context["page_obj"].has_next(),
+ }
+ )
def get_name(self, obj):
return str(obj)
@@ -41,7 +45,11 @@ def get_name(self, obj):
class UserSelect2View(Select2View):
def get_queryset(self):
- return _get_user_queryset(self.term).annotate(username=F('user__username')).only('id')
+ return (
+ _get_user_queryset(self.term)
+ .annotate(username=F("user__username"))
+ .only("id")
+ )
def get_name(self, obj):
return obj.username
@@ -54,19 +62,23 @@ def get_queryset(self):
class ClassSelect2View(Select2View):
def get_queryset(self):
- return Class.get_visible_classes(self.request.user).filter(name__icontains=self.term)
+ return Class.get_visible_classes(self.request.user).filter(
+ name__icontains=self.term
+ )
class ProblemSelect2View(Select2View):
def get_queryset(self):
- return Problem.get_visible_problems(self.request.user) \
- .filter(Q(code__icontains=self.term) | Q(name__icontains=self.term))
+ return Problem.get_visible_problems(self.request.user).filter(
+ Q(code__icontains=self.term) | Q(name__icontains=self.term)
+ )
class ContestSelect2View(Select2View):
def get_queryset(self):
- return Contest.get_visible_contests(self.request.user) \
- .filter(Q(key__icontains=self.term) | Q(name__icontains=self.term))
+ return Contest.get_visible_contests(self.request.user).filter(
+ Q(key__icontains=self.term) | Q(name__icontains=self.term)
+ )
class CommentSelect2View(Select2View):
@@ -83,25 +95,38 @@ def get_queryset(self):
def get(self, request, *args, **kwargs):
self.request = request
self.kwargs = kwargs
- self.term = kwargs.get('term', request.GET.get('term', ''))
- self.gravatar_size = request.GET.get('gravatar_size', 128)
- self.gravatar_default = request.GET.get('gravatar_default', None)
-
- self.object_list = self.get_queryset().values_list('pk', 'user__username', 'user__email', 'display_rank',
- 'username_display_override')
+ self.term = kwargs.get("term", request.GET.get("term", ""))
+ self.gravatar_size = request.GET.get("gravatar_size", 128)
+ self.gravatar_default = request.GET.get("gravatar_default", None)
+
+ self.object_list = self.get_queryset().values_list(
+ "pk",
+ "user__username",
+ "user__email",
+ "display_rank",
+ "username_display_override",
+ )
context = self.get_context_data()
- return JsonResponse({
- 'results': [
- {
- 'text': username_override or username,
- 'id': username,
- 'gravatar_url': gravatar(email, self.gravatar_size, self.gravatar_default),
- 'display_rank': display_rank,
- } for pk, username, email, display_rank, username_override in context['object_list']],
- 'more': context['page_obj'].has_next(),
- })
+ return JsonResponse(
+ {
+ "results": [
+ {
+ "text": username_override or username,
+ "id": username,
+ "gravatar_url": gravatar(
+ email, self.gravatar_size, self.gravatar_default
+ ),
+ "display_rank": display_rank,
+ }
+ for pk, username, email, display_rank, username_override in context[
+ "object_list"
+ ]
+ ],
+ "more": context["page_obj"].has_next(),
+ }
+ )
def get_name(self, obj):
return str(obj)
@@ -109,21 +134,26 @@ def get_name(self, obj):
class ContestUserSearchSelect2View(UserSearchSelect2View):
def get_queryset(self):
- contest = get_object_or_404(Contest, key=self.kwargs['contest'])
- if not contest.is_accessible_by(self.request.user) or not contest.can_see_full_scoreboard(self.request.user):
+ contest = get_object_or_404(Contest, key=self.kwargs["contest"])
+ if not contest.is_accessible_by(
+ self.request.user
+ ) or not contest.can_see_full_scoreboard(self.request.user):
raise Http404()
- return Profile.objects.filter(contest_history__contest=contest,
- user__username__icontains=self.term).distinct()
+ return Profile.objects.filter(
+ contest_history__contest=contest, user__username__icontains=self.term
+ ).distinct()
class TicketUserSelect2View(UserSearchSelect2View):
def get_queryset(self):
- return Profile.objects.filter(tickets__isnull=False,
- user__username__icontains=self.term).distinct()
+ return Profile.objects.filter(
+ tickets__isnull=False, user__username__icontains=self.term
+ ).distinct()
class AssigneeSelect2View(UserSearchSelect2View):
def get_queryset(self):
- return Profile.objects.filter(assigned_tickets__isnull=False,
- user__username__icontains=self.term).distinct()
+ return Profile.objects.filter(
+ assigned_tickets__isnull=False, user__username__icontains=self.term
+ ).distinct()
diff --git a/judge/views/stats.py b/judge/views/stats.py
index 9cfdfd273b..6b6f8146b5 100644
--- a/judge/views/stats.py
+++ b/judge/views/stats.py
@@ -8,27 +8,42 @@
from django.utils.translation import gettext as _
from judge.models import Language, Submission
-from judge.utils.stats import chart_colors, get_bar_chart, get_pie_chart, highlight_colors
+from judge.utils.stats import (
+ chart_colors,
+ get_bar_chart,
+ get_pie_chart,
+ highlight_colors,
+)
-ac_count = Count(Value(1), filter=Q(submission__result='AC'))
+ac_count = Count(Value(1), filter=Q(submission__result="AC"))
-def language_data(request, language_count=Language.objects.annotate(count=Count('submission'))):
- languages = language_count.filter(count__gt=0).values('name', 'count').order_by('-count')
+def language_data(
+ request, language_count=Language.objects.annotate(count=Count("submission"))
+):
+ languages = (
+ language_count.filter(count__gt=0).values("name", "count").order_by("-count")
+ )
num_languages = min(len(languages), settings.DMOJ_STATS_LANGUAGE_THRESHOLD)
- other_count = sum(map(itemgetter('count'), languages[num_languages:]))
-
- return JsonResponse({
- 'labels': list(map(itemgetter('name'), languages[:num_languages])) + [_('Other')],
- 'datasets': [
- {
- 'backgroundColor': chart_colors[:num_languages] + ['#FDB45C'],
- 'highlightBackgroundColor': highlight_colors[:num_languages] + ['#FFC870'],
- 'data': list(map(itemgetter('count'), languages[:num_languages])) + [other_count],
- },
- ],
- }, safe=False)
+ other_count = sum(map(itemgetter("count"), languages[num_languages:]))
+
+ return JsonResponse(
+ {
+ "labels": list(map(itemgetter("name"), languages[:num_languages]))
+ + [_("Other")],
+ "datasets": [
+ {
+ "backgroundColor": chart_colors[:num_languages] + ["#FDB45C"],
+ "highlightBackgroundColor": highlight_colors[:num_languages]
+ + ["#FFC870"],
+ "data": list(map(itemgetter("count"), languages[:num_languages]))
+ + [other_count],
+ },
+ ],
+ },
+ safe=False,
+ )
def ac_language_data(request):
@@ -37,27 +52,42 @@ def ac_language_data(request):
def status_data(request, statuses=None):
if not statuses:
- statuses = (Submission.objects.values('result').annotate(count=Count('result'))
- .values('result', 'count').order_by('-count'))
+ statuses = (
+ Submission.objects.values("result")
+ .annotate(count=Count("result"))
+ .values("result", "count")
+ .order_by("-count")
+ )
data = []
for status in statuses:
- res = status['result']
+ res = status["result"]
if not res:
continue
- count = status['count']
+ count = status["count"]
data.append((str(Submission.USER_DISPLAY_CODES[res]), count))
return JsonResponse(get_pie_chart(data), safe=False)
def ac_rate(request):
- rate = CombinedExpression(ac_count / Count('submission'), '*', Value(100.0), output_field=FloatField())
- data = Language.objects.annotate(total=Count('submission'), ac_rate=rate).filter(total__gt=0) \
- .order_by('total').values_list('name', 'ac_rate')
+ rate = CombinedExpression(
+ ac_count / Count("submission"), "*", Value(100.0), output_field=FloatField()
+ )
+ data = (
+ Language.objects.annotate(total=Count("submission"), ac_rate=rate)
+ .filter(total__gt=0)
+ .order_by("total")
+ .values_list("name", "ac_rate")
+ )
return JsonResponse(get_bar_chart(list(data)))
def language(request):
- return render(request, 'stats/language.html', {
- 'title': _('Language statistics'), 'tab': 'language',
- })
+ return render(
+ request,
+ "stats/language.html",
+ {
+ "title": _("Language statistics"),
+ "tab": "language",
+ },
+ )
diff --git a/judge/views/status.py b/judge/views/status.py
index 9e11603860..306fe9fa50 100644
--- a/judge/views/status.py
+++ b/judge/views/status.py
@@ -7,37 +7,45 @@
from judge.models import Judge, Language, RuntimeVersion
-__all__ = ['status_all', 'status_table']
+__all__ = ["status_all", "status_table"]
def get_judges(request):
if request.user.is_superuser or request.user.is_staff:
- return True, Judge.objects.order_by('-online', 'name')
+ return True, Judge.objects.order_by("-online", "name")
else:
return False, Judge.objects.filter(online=True)
def status_all(request):
see_all, judges = get_judges(request)
- return render(request, 'status/judge-status.html', {
- 'title': _('Status'),
- 'judges': judges,
- 'runtime_version_data': Judge.runtime_versions(),
- 'see_all_judges': see_all,
- })
+ return render(
+ request,
+ "status/judge-status.html",
+ {
+ "title": _("Status"),
+ "judges": judges,
+ "runtime_version_data": Judge.runtime_versions(),
+ "see_all_judges": see_all,
+ },
+ )
def status_table(request):
see_all, judges = get_judges(request)
- return render(request, 'status/judge-status-table.html', {
- 'judges': judges,
- 'runtime_version_data': Judge.runtime_versions(),
- 'see_all_judges': see_all,
- })
+ return render(
+ request,
+ "status/judge-status-table.html",
+ {
+ "judges": judges,
+ "runtime_version_data": Judge.runtime_versions(),
+ "see_all_judges": see_all,
+ },
+ )
class LatestList(list):
- __slots__ = ('versions', 'is_latest')
+ __slots__ = ("versions", "is_latest")
def compare_version_list(x, y):
@@ -62,11 +70,13 @@ def version_matrix(request):
judges = {judge.id: judge.name for judge in Judge.objects.filter(online=True)}
languages = Language.objects.filter(judges__online=True).distinct()
- for runtime in RuntimeVersion.objects.filter(judge__online=True).order_by('priority'):
+ for runtime in RuntimeVersion.objects.filter(judge__online=True).order_by(
+ "priority"
+ ):
matrix[runtime.judge_id][runtime.language_id].append(runtime)
for judge, data in matrix.items():
- name_tuple = judges[judge].rpartition('.')
+ name_tuple = judges[judge].rpartition(".")
groups[name_tuple[0] or name_tuple[-1]].append((judges[judge], data))
matrix = {}
@@ -104,9 +114,13 @@ def version_matrix(request):
versions.is_latest = versions.versions == latest[language]
languages = sorted(languages, key=lambda lang: version.parse(lang.name))
- return render(request, 'status/versions.html', {
- 'title': _('Version'),
- 'judges': sorted(matrix.keys()),
- 'languages': languages,
- 'matrix': matrix,
- })
+ return render(
+ request,
+ "status/versions.html",
+ {
+ "title": _("Version"),
+ "judges": sorted(matrix.keys()),
+ "languages": languages,
+ "matrix": matrix,
+ },
+ )
diff --git a/judge/views/submission.py b/judge/views/submission.py
index d62243788a..8272880d5e 100644
--- a/judge/views/submission.py
+++ b/judge/views/submission.py
@@ -6,9 +6,19 @@
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.cache import cache
-from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist, PermissionDenied
+from django.core.exceptions import (
+ ImproperlyConfigured,
+ ObjectDoesNotExist,
+ PermissionDenied,
+)
from django.db.models import Prefetch, Q
-from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect, JsonResponse
+from django.http import (
+ Http404,
+ HttpResponse,
+ HttpResponseBadRequest,
+ HttpResponseRedirect,
+ JsonResponse,
+)
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.utils import timezone
@@ -21,22 +31,56 @@
from judge import event_poster as event
from judge.highlight_code import highlight_code
-from judge.models import Contest, Language, Problem, ProblemTranslation, Profile, Submission
+from judge.models import (
+ Contest,
+ Language,
+ Problem,
+ ProblemTranslation,
+ Profile,
+ Submission,
+)
from judge.models.problem import SubmissionSourceAccess
from judge.utils.infinite_paginator import InfinitePaginationMixin
from judge.utils.lazy import memo_lazy
-from judge.utils.problems import get_result_data, user_completed_ids, user_editable_ids, user_tester_ids
+from judge.utils.problems import (
+ get_result_data,
+ user_completed_ids,
+ user_editable_ids,
+ user_tester_ids,
+)
from judge.utils.raw_sql import join_sql_subquery, use_straight_join
from judge.utils.views import DiggPaginatorMixin, TitleMixin, generic_message
def submission_related(queryset):
- return queryset.select_related('user__user', 'problem', 'language') \
- .only('id', 'user__user__username', 'user__display_rank', 'user__rating', 'problem__name',
- 'problem__code', 'problem__is_public', 'language__short_name', 'language__key', 'date', 'time', 'memory',
- 'points', 'result', 'status', 'case_points', 'case_total', 'current_testcase', 'contest_object',
- 'locked_after', 'problem__submission_source_visibility_mode', 'user__username_display_override') \
- .prefetch_related('contest_object__authors', 'contest_object__curators')
+ return (
+ queryset.select_related("user__user", "problem", "language")
+ .only(
+ "id",
+ "user__user__username",
+ "user__display_rank",
+ "user__rating",
+ "problem__name",
+ "problem__code",
+ "problem__is_public",
+ "language__short_name",
+ "language__key",
+ "date",
+ "time",
+ "memory",
+ "points",
+ "result",
+ "status",
+ "case_points",
+ "case_total",
+ "current_testcase",
+ "contest_object",
+ "locked_after",
+ "problem__submission_source_visibility_mode",
+ "user__username_display_override",
+ )
+ .prefetch_related("contest_object__authors", "contest_object__curators")
+ )
class SubmissionPermissionDenied(PermissionDenied):
@@ -46,8 +90,8 @@ def __init__(self, submission):
class SubmissionMixin(object):
model = Submission
- context_object_name = 'submission'
- pk_url_kwarg = 'submission'
+ context_object_name = "submission"
+ pk_url_kwarg = "submission"
class SubmissionDetailBase(LoginRequiredMixin, TitleMixin, SubmissionMixin, DetailView):
@@ -65,67 +109,94 @@ def get(self, request, *args, **kwargs):
def no_permission(self, submission):
problem = submission.problem
- if problem.is_accessible_by(self.request.user) and \
- problem.submission_source_visibility == SubmissionSourceAccess.SOLVED:
-
- message = escape(_('Permission denied. Solve %(problem)s in order to view it.')) % {
- 'problem': format_html('{1} ',
- reverse('problem_detail', args=[problem.code]),
- problem.translated_name(self.request.LANGUAGE_CODE)),
+ if (
+ problem.is_accessible_by(self.request.user)
+ and problem.submission_source_visibility == SubmissionSourceAccess.SOLVED
+ ):
+ message = escape(
+ _("Permission denied. Solve %(problem)s in order to view it.")
+ ) % {
+ "problem": format_html(
+ '{1} ',
+ reverse("problem_detail", args=[problem.code]),
+ problem.translated_name(self.request.LANGUAGE_CODE),
+ ),
}
- return generic_message(self.request, _("Can't access submission"), mark_safe(message), status=403)
+ return generic_message(
+ self.request,
+ _("Can't access submission"),
+ mark_safe(message),
+ status=403,
+ )
else:
- return generic_message(self.request, _("Can't access submission"), _('Permission denied.'), status=403)
+ return generic_message(
+ self.request,
+ _("Can't access submission"),
+ _("Permission denied."),
+ status=403,
+ )
def get_title(self):
submission = self.object
- return _('Submission of %(problem)s by %(user)s') % {
- 'problem': submission.problem.translated_name(self.request.LANGUAGE_CODE),
- 'user': submission.user.display_name,
+ return _("Submission of %(problem)s by %(user)s") % {
+ "problem": submission.problem.translated_name(self.request.LANGUAGE_CODE),
+ "user": submission.user.display_name,
}
def get_content_title(self):
submission = self.object
- return mark_safe(escape(_('Submission of %(problem)s by %(user)s')) % {
- 'problem': format_html('{1} ',
- reverse('problem_detail', args=[submission.problem.code]),
- submission.problem.translated_name(self.request.LANGUAGE_CODE)),
- 'user': format_html('{1} ',
- reverse('user_page', args=[submission.user.user.username]),
- submission.user.display_name),
- })
+ return mark_safe(
+ escape(_("Submission of %(problem)s by %(user)s"))
+ % {
+ "problem": format_html(
+ '{1} ',
+ reverse("problem_detail", args=[submission.problem.code]),
+ submission.problem.translated_name(self.request.LANGUAGE_CODE),
+ ),
+ "user": format_html(
+ '{1} ',
+ reverse("user_page", args=[submission.user.user.username]),
+ submission.user.display_name,
+ ),
+ }
+ )
class SubmissionSource(SubmissionDetailBase):
- template_name = 'submission/source.html'
+ template_name = "submission/source.html"
def get_queryset(self):
- return super().get_queryset().select_related('source')
+ return super().get_queryset().select_related("source")
def get_context_data(self, **kwargs):
context = super(SubmissionSource, self).get_context_data(**kwargs)
submission = self.object
- context['raw_source'] = submission.source.source.rstrip('\n')
- context['highlighted_source'] = highlight_code(submission.source.source, submission.language.pygments)
+ context["raw_source"] = submission.source.source.rstrip("\n")
+ context["highlighted_source"] = highlight_code(
+ submission.source.source, submission.language.pygments
+ )
return context
def make_batch(batch, cases):
- result = {'id': batch, 'cases': cases}
+ result = {"id": batch, "cases": cases}
if batch:
- result['points'] = min(map(attrgetter('points'), cases))
- result['total'] = max(map(attrgetter('total'), cases))
+ result["points"] = min(map(attrgetter("points"), cases))
+ result["total"] = max(map(attrgetter("total"), cases))
return result
-TestCase = namedtuple('TestCase', 'id status batch num_combined')
+TestCase = namedtuple("TestCase", "id status batch num_combined")
def get_statuses(batch, cases):
- cases = [TestCase(id=case.id, status=case.status, batch=batch, num_combined=1) for case in cases]
+ cases = [
+ TestCase(id=case.id, status=case.status, batch=batch, num_combined=1)
+ for case in cases
+ ]
if batch:
# Get the first non-AC case if it exists.
- return [next((case for case in cases if case.status != 'AC'), cases[0])]
+ return [next((case for case in cases if case.status != "AC"), cases[0])]
else:
return cases
@@ -134,14 +205,22 @@ def combine_statuses(status_cases, submission):
ret = []
# If the submission is not graded and the final case is a batch,
# we don't actually know if it is completed or not, so just remove it.
- if not submission.is_graded and len(status_cases) > 0 and status_cases[-1].batch is not None:
+ if (
+ not submission.is_graded
+ and len(status_cases) > 0
+ and status_cases[-1].batch is not None
+ ):
status_cases.pop()
- for key, group in groupby(status_cases, key=attrgetter('status')):
+ for key, group in groupby(status_cases, key=attrgetter("status")):
group = list(group)
if len(group) > 10:
# Grab the first case's id so the user can jump to that case, and combine the rest.
- ret.append(TestCase(id=group[0].id, status=key, batch=None, num_combined=len(group)))
+ ret.append(
+ TestCase(
+ id=group[0].id, status=key, batch=None, num_combined=len(group)
+ )
+ )
else:
ret.extend(group)
return ret
@@ -169,59 +248,66 @@ def group_test_cases(cases):
class SubmissionStatus(SubmissionDetailBase):
- template_name = 'submission/status.html'
+ template_name = "submission/status.html"
def get_context_data(self, **kwargs):
context = super(SubmissionStatus, self).get_context_data(**kwargs)
submission = self.object
- context['last_msg'] = event.last()
+ context["last_msg"] = event.last()
- context['batches'], statuses, context['max_execution_time'] = group_test_cases(submission.test_cases.all())
- context['statuses'] = combine_statuses(statuses, submission)
+ context["batches"], statuses, context["max_execution_time"] = group_test_cases(
+ submission.test_cases.all()
+ )
+ context["statuses"] = combine_statuses(statuses, submission)
- context['time_limit'] = submission.problem.time_limit
+ context["time_limit"] = submission.problem.time_limit
try:
- lang_limit = submission.problem.language_limits.get(language=submission.language)
+ lang_limit = submission.problem.language_limits.get(
+ language=submission.language
+ )
except ObjectDoesNotExist:
pass
else:
- context['time_limit'] = lang_limit.time_limit
+ context["time_limit"] = lang_limit.time_limit
return context
class SubmissionTestCaseQuery(SubmissionStatus):
- template_name = 'submission/status-testcases.html'
+ template_name = "submission/status-testcases.html"
def get(self, request, *args, **kwargs):
- if 'id' not in request.GET or not request.GET['id'].isdigit():
+ if "id" not in request.GET or not request.GET["id"].isdigit():
return HttpResponseBadRequest()
- self.kwargs[self.pk_url_kwarg] = kwargs[self.pk_url_kwarg] = int(request.GET['id'])
+ self.kwargs[self.pk_url_kwarg] = kwargs[self.pk_url_kwarg] = int(
+ request.GET["id"]
+ )
return super(SubmissionTestCaseQuery, self).get(request, *args, **kwargs)
class SubmissionSourceRaw(SubmissionSource):
def get(self, request, *args, **kwargs):
submission = self.get_object()
- return HttpResponse(submission.source.source, content_type='text/plain')
+ return HttpResponse(submission.source.source, content_type="text/plain")
@require_POST
def abort_submission(request, submission):
submission = get_object_or_404(Submission, id=int(submission))
- if (not request.user.has_perm('judge.abort_any_submission') and
- (submission.rejudged_date is not None or request.profile != submission.user)):
+ if not request.user.has_perm("judge.abort_any_submission") and (
+ submission.rejudged_date is not None or request.profile != submission.user
+ ):
raise PermissionDenied()
submission.abort()
- return HttpResponseRedirect(reverse('submission_status', args=(submission.id,)))
+ return HttpResponseRedirect(reverse("submission_status", args=(submission.id,)))
def filter_submissions_by_visible_problems(queryset, user):
join_sql_subquery(
queryset,
- subquery=str(Problem.get_visible_problems(user).only('id').query),
+ subquery=str(Problem.get_visible_problems(user).only("id").query),
params=[],
- join_fields=[('problem_id', 'id')],
- alias='visible_problems',
+ join_fields=[("problem_id", "id")],
+ alias="visible_problems",
related_model=Problem,
)
@@ -230,17 +316,17 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
model = Submission
paginate_by = 50
show_problem = True
- title = gettext_lazy('All submissions')
- content_title = gettext_lazy('All submissions')
- tab = 'all_submissions_list'
- template_name = 'submission/list.html'
- context_object_name = 'submissions'
+ title = gettext_lazy("All submissions")
+ content_title = gettext_lazy("All submissions")
+ tab = "all_submissions_list"
+ template_name = "submission/list.html"
+ context_object_name = "submissions"
first_page_href = None
def get_result_data(self):
result = self._get_result_data()
- for category in result['categories']:
- category['name'] = _(category['name'])
+ for category in result["categories"]:
+ category["name"] = _(category["name"])
return result
def _get_result_data(self, queryset=None):
@@ -253,7 +339,10 @@ def access_check(self, request):
@cached_property
def in_contest(self):
- return self.request.user.is_authenticated and self.request.profile.current_contest is not None
+ return (
+ self.request.user.is_authenticated
+ and self.request.profile.current_contest is not None
+ )
@cached_property
def contest(self):
@@ -262,36 +351,47 @@ def contest(self):
def _get_queryset(self):
queryset = Submission.objects.all()
use_straight_join(queryset)
- queryset = submission_related(queryset.order_by('-id'))
+ queryset = submission_related(queryset.order_by("-id"))
if self.show_problem:
- queryset = queryset.prefetch_related(Prefetch('problem__translations',
- queryset=ProblemTranslation.objects.filter(
- language=self.request.LANGUAGE_CODE), to_attr='_trans'))
+ queryset = queryset.prefetch_related(
+ Prefetch(
+ "problem__translations",
+ queryset=ProblemTranslation.objects.filter(
+ language=self.request.LANGUAGE_CODE
+ ),
+ to_attr="_trans",
+ )
+ )
if self.in_contest:
queryset = queryset.filter(contest_object=self.contest)
if not self.contest.can_see_full_scoreboard(self.request.user):
queryset = queryset.filter(user=self.request.profile)
else:
- queryset = queryset.select_related('contest_object').defer('contest_object__description')
+ queryset = queryset.select_related("contest_object").defer(
+ "contest_object__description"
+ )
- if not self.request.user.has_perm('judge.see_private_contest'):
+ if not self.request.user.has_perm("judge.see_private_contest"):
# Show submissions for any contest you can edit or where you can see submissions
contest_queryset = Contest.objects.filter(
- Q(authors=self.request.profile) |
- Q(curators=self.request.profile) |
- Q(tester_see_submissions=True, testers=self.request.profile) |
- Q(view_contest_submissions=self.request.profile) |
- Q(scoreboard_visibility=Contest.SCOREBOARD_VISIBLE) |
- Q(end_time__lt=timezone.now(), scoreboard_visibility__in=(
- Contest.SCOREBOARD_AFTER_PARTICIPATION,
- Contest.SCOREBOARD_AFTER_CONTEST,
- )),
+ Q(authors=self.request.profile)
+ | Q(curators=self.request.profile)
+ | Q(tester_see_submissions=True, testers=self.request.profile)
+ | Q(view_contest_submissions=self.request.profile)
+ | Q(scoreboard_visibility=Contest.SCOREBOARD_VISIBLE)
+ | Q(
+ end_time__lt=timezone.now(),
+ scoreboard_visibility__in=(
+ Contest.SCOREBOARD_AFTER_PARTICIPATION,
+ Contest.SCOREBOARD_AFTER_CONTEST,
+ ),
+ ),
).distinct()
queryset = queryset.filter(
- Q(user=self.request.profile) |
- Q(contest_object__in=contest_queryset) |
- Q(contest_object__isnull=True),
+ Q(user=self.request.profile)
+ | Q(contest_object__in=contest_queryset)
+ | Q(contest_object__isnull=True),
)
if self.selected_languages:
@@ -299,8 +399,13 @@ def _get_queryset(self):
# so we are forcing an eager evaluation to get the IDs right here.
# Otherwise, with multiple language filters, MariaDB refuses to use an index
# (or runs the subquery for every submission, which is even more horrifying to think about).
- queryset = queryset.filter(language__in=list(
- Language.objects.filter(key__in=self.selected_languages).values_list('id', flat=True)))
+ queryset = queryset.filter(
+ language__in=list(
+ Language.objects.filter(
+ key__in=self.selected_languages
+ ).values_list("id", flat=True)
+ )
+ )
if self.selected_statuses:
queryset = queryset.filter(result__in=self.selected_statuses)
@@ -317,40 +422,52 @@ def get_my_submissions_page(self):
return None
def get_all_submissions_page(self):
- return reverse('all_submissions')
+ return reverse("all_submissions")
def get_searchable_status_codes(self):
- hidden_codes = ['SC']
+ hidden_codes = ["SC"]
if not self.request.user.is_superuser and not self.request.user.is_staff:
- hidden_codes += ['IE']
- return [(key, value) for key, value in Submission.RESULT if key not in hidden_codes]
+ hidden_codes += ["IE"]
+ return [
+ (key, value) for key, value in Submission.RESULT if key not in hidden_codes
+ ]
def get_context_data(self, **kwargs):
context = super(SubmissionsListBase, self).get_context_data(**kwargs)
authenticated = self.request.user.is_authenticated
- context['dynamic_update'] = False
- context['dynamic_contest_id'] = self.in_contest and self.contest.id
- context['show_problem'] = self.show_problem
+ context["dynamic_update"] = False
+ context["dynamic_contest_id"] = self.in_contest and self.contest.id
+ context["show_problem"] = self.show_problem
profile = self.request.profile
- context['completed_problem_ids'] = memo_lazy(lambda: user_completed_ids(profile), set) if authenticated else []
- context['editable_problem_ids'] = memo_lazy(lambda: user_editable_ids(profile), set) if authenticated else []
- context['tester_problem_ids'] = memo_lazy(lambda: user_tester_ids(profile), set) if authenticated else []
+ context["completed_problem_ids"] = (
+ memo_lazy(lambda: user_completed_ids(profile), set) if authenticated else []
+ )
+ context["editable_problem_ids"] = (
+ memo_lazy(lambda: user_editable_ids(profile), set) if authenticated else []
+ )
+ context["tester_problem_ids"] = (
+ memo_lazy(lambda: user_tester_ids(profile), set) if authenticated else []
+ )
- context['all_languages'] = Language.objects.all().values_list('key', 'name')
- context['selected_languages'] = self.selected_languages
+ context["all_languages"] = Language.objects.all().values_list("key", "name")
+ context["selected_languages"] = self.selected_languages
- context['all_statuses'] = self.get_searchable_status_codes()
- context['selected_statuses'] = self.selected_statuses
+ context["all_statuses"] = self.get_searchable_status_codes()
+ context["selected_statuses"] = self.selected_statuses
- context['results_json'] = mark_safe(json.dumps(self.get_result_data()))
- context['results_colors_json'] = mark_safe(json.dumps(settings.DMOJ_STATS_SUBMISSION_RESULT_COLORS))
+ context["results_json"] = mark_safe(json.dumps(self.get_result_data()))
+ context["results_colors_json"] = mark_safe(
+ json.dumps(settings.DMOJ_STATS_SUBMISSION_RESULT_COLORS)
+ )
- context['page_suffix'] = suffix = ('?' + self.request.GET.urlencode()) if self.request.GET else ''
- context['first_page_href'] = (self.first_page_href or '.') + suffix
- context['my_submissions_link'] = self.get_my_submissions_page()
- context['all_submissions_link'] = self.get_all_submissions_page()
- context['tab'] = self.tab
+ context["page_suffix"] = suffix = (
+ ("?" + self.request.GET.urlencode()) if self.request.GET else ""
+ )
+ context["first_page_href"] = (self.first_page_href or ".") + suffix
+ context["my_submissions_link"] = self.get_my_submissions_page()
+ context["all_submissions_link"] = self.get_all_submissions_page()
+ context["tab"] = self.tab
return context
def get(self, request, *args, **kwargs):
@@ -358,10 +475,10 @@ def get(self, request, *args, **kwargs):
if check is not None:
return check
- self.selected_languages = set(request.GET.getlist('language'))
- self.selected_statuses = set(request.GET.getlist('status'))
+ self.selected_languages = set(request.GET.getlist("language"))
+ self.selected_statuses = set(request.GET.getlist("status"))
- if 'results' in request.GET:
+ if "results" in request.GET:
return JsonResponse(self.get_result_data())
return super(SubmissionsListBase, self).get(request, *args, **kwargs)
@@ -369,54 +486,68 @@ def get(self, request, *args, **kwargs):
class UserMixin(object):
def get(self, request, *args, **kwargs):
- if 'user' not in kwargs:
- raise ImproperlyConfigured('Must pass a user')
- self.profile = get_object_or_404(Profile, user__username=kwargs['user'])
- self.username = kwargs['user']
+ if "user" not in kwargs:
+ raise ImproperlyConfigured("Must pass a user")
+ self.profile = get_object_or_404(Profile, user__username=kwargs["user"])
+ self.username = kwargs["user"]
return super(UserMixin, self).get(request, *args, **kwargs)
class ConditionalUserTabMixin(object):
@cached_property
def is_own(self):
- return self.request.user.is_authenticated and self.request.profile == self.profile
+ return (
+ self.request.user.is_authenticated and self.request.profile == self.profile
+ )
def get_context_data(self, **kwargs):
context = super(ConditionalUserTabMixin, self).get_context_data(**kwargs)
if self.is_own:
- context['tab'] = 'my_submissions_tab'
+ context["tab"] = "my_submissions_tab"
else:
- context['tab'] = 'user_submissions_tab'
- context['tab_username'] = self.profile.display_name
+ context["tab"] = "user_submissions_tab"
+ context["tab_username"] = self.profile.display_name
return context
class AllUserSubmissions(ConditionalUserTabMixin, UserMixin, SubmissionsListBase):
def get_queryset(self):
- return super(AllUserSubmissions, self).get_queryset().filter(user_id=self.profile.id)
+ return (
+ super(AllUserSubmissions, self)
+ .get_queryset()
+ .filter(user_id=self.profile.id)
+ )
def get_title(self):
if self.is_own:
- return _('All my submissions')
- return _('All submissions by %s') % self.profile.display_name
+ return _("All my submissions")
+ return _("All submissions by %s") % self.profile.display_name
def get_content_title(self):
if self.is_own:
- return _('All my submissions')
- return mark_safe(escape(_('All submissions by %s')) % (
- format_html('{0} ', self.profile.display_name,
- reverse('user_page', args=[self.username])),
- ))
+ return _("All my submissions")
+ return mark_safe(
+ escape(_("All submissions by %s"))
+ % (
+ format_html(
+ '{0} ',
+ self.profile.display_name,
+ reverse("user_page", args=[self.username]),
+ ),
+ )
+ )
def get_my_submissions_page(self):
if self.request.user.is_authenticated:
- return reverse('all_user_submissions', kwargs={'user': self.request.user.username})
+ return reverse(
+ "all_user_submissions", kwargs={"user": self.request.user.username}
+ )
def get_context_data(self, **kwargs):
context = super(AllUserSubmissions, self).get_context_data(**kwargs)
- context['dynamic_update'] = context['page_obj'].number == 1
- context['dynamic_user_id'] = self.profile.id
- context['last_msg'] = event.last()
+ context["dynamic_update"] = context["page_obj"].number == 1
+ context["dynamic_user_id"] = self.profile.id
+ context["last_msg"] = event.last()
return context
@@ -426,18 +557,33 @@ class ProblemSubmissionsBase(SubmissionsListBase):
check_contest_in_access_check = True
def get_queryset(self):
- if self.in_contest and not self.contest.contest_problems.filter(problem_id=self.problem.id).exists():
+ if (
+ self.in_contest
+ and not self.contest.contest_problems.filter(
+ problem_id=self.problem.id
+ ).exists()
+ ):
raise Http404()
- return super(ProblemSubmissionsBase, self)._get_queryset().filter(problem_id=self.problem.id)
+ return (
+ super(ProblemSubmissionsBase, self)
+ ._get_queryset()
+ .filter(problem_id=self.problem.id)
+ )
def get_title(self):
- return _('All submissions for %s') % self.problem_name
+ return _("All submissions for %s") % self.problem_name
def get_content_title(self):
- return mark_safe(escape(_('All submissions for %s')) % (
- format_html('{0} ', self.problem_name,
- reverse('problem_detail', args=[self.problem.code])),
- ))
+ return mark_safe(
+ escape(_("All submissions for %s"))
+ % (
+ format_html(
+ '{0} ',
+ self.problem_name,
+ reverse("problem_detail", args=[self.problem.code]),
+ ),
+ )
+ )
def access_check_contest(self, request):
if self.in_contest and not self.contest.can_see_own_scoreboard(request.user):
@@ -445,7 +591,11 @@ def access_check_contest(self, request):
def access_check(self, request):
# FIXME: This should be rolled into the `is_accessible_by` check when implementing #1509
- if self.in_contest and request.user.is_authenticated and request.profile.id in self.contest.editor_ids:
+ if (
+ self.in_contest
+ and request.user.is_authenticated
+ and request.profile.id in self.contest.editor_ids
+ ):
return
if not self.problem.is_accessible_by(request.user):
@@ -455,30 +605,39 @@ def access_check(self, request):
self.access_check_contest(request)
def get(self, request, *args, **kwargs):
- if 'problem' not in kwargs:
- raise ImproperlyConfigured('Must pass a problem')
- self.problem = get_object_or_404(Problem, code=kwargs['problem'])
+ if "problem" not in kwargs:
+ raise ImproperlyConfigured("Must pass a problem")
+ self.problem = get_object_or_404(Problem, code=kwargs["problem"])
self.problem_name = self.problem.translated_name(self.request.LANGUAGE_CODE)
return super(ProblemSubmissionsBase, self).get(request, *args, **kwargs)
def get_all_submissions_page(self):
- return reverse('chronological_submissions', kwargs={'problem': self.problem.code})
+ return reverse(
+ "chronological_submissions", kwargs={"problem": self.problem.code}
+ )
def get_context_data(self, **kwargs):
context = super(ProblemSubmissionsBase, self).get_context_data(**kwargs)
if self.dynamic_update:
- context['dynamic_update'] = context['page_obj'].number == 1
- context['dynamic_problem_id'] = self.problem.id
- context['last_msg'] = event.last()
- context['best_submissions_link'] = reverse('ranked_submissions', kwargs={'problem': self.problem.code})
+ context["dynamic_update"] = context["page_obj"].number == 1
+ context["dynamic_problem_id"] = self.problem.id
+ context["last_msg"] = event.last()
+ context["best_submissions_link"] = reverse(
+ "ranked_submissions", kwargs={"problem": self.problem.code}
+ )
return context
class ProblemSubmissions(ProblemSubmissionsBase):
def get_my_submissions_page(self):
if self.request.user.is_authenticated:
- return reverse('user_submissions', kwargs={'problem': self.problem.code,
- 'user': self.request.user.username})
+ return reverse(
+ "user_submissions",
+ kwargs={
+ "problem": self.problem.code,
+ "user": self.request.user.username,
+ },
+ )
class UserProblemSubmissions(ConditionalUserTabMixin, UserMixin, ProblemSubmissions):
@@ -491,57 +650,90 @@ def access_check(self, request):
self.access_check_contest(request)
def get_queryset(self):
- return super(UserProblemSubmissions, self).get_queryset().filter(user_id=self.profile.id)
+ return (
+ super(UserProblemSubmissions, self)
+ .get_queryset()
+ .filter(user_id=self.profile.id)
+ )
def get_title(self):
if self.is_own:
- return _('My submissions for %(problem)s') % {'problem': self.problem_name}
+ return _("My submissions for %(problem)s") % {"problem": self.problem_name}
return _("%(user)s's submissions for %(problem)s") % {
- 'user': self.profile.display_name, 'problem': self.problem_name,
+ "user": self.profile.display_name,
+ "problem": self.problem_name,
}
def get_content_title(self):
if self.request.user.is_authenticated and self.request.profile == self.profile:
- return mark_safe(escape(_('My submissions for %(problem)s')) % {
- 'problem': format_html('{0} ', self.problem_name,
- reverse('problem_detail', args=[self.problem.code])),
- })
- return mark_safe(escape(_("%(user)s's submissions for %(problem)s")) % {
- 'user': format_html('{0} ', self.profile.display_name,
- reverse('user_page', args=[self.username])),
- 'problem': format_html('{0} ', self.problem_name,
- reverse('problem_detail', args=[self.problem.code])),
- })
+ return mark_safe(
+ escape(_("My submissions for %(problem)s"))
+ % {
+ "problem": format_html(
+ '{0} ',
+ self.problem_name,
+ reverse("problem_detail", args=[self.problem.code]),
+ ),
+ }
+ )
+ return mark_safe(
+ escape(_("%(user)s's submissions for %(problem)s"))
+ % {
+ "user": format_html(
+ '{0} ',
+ self.profile.display_name,
+ reverse("user_page", args=[self.username]),
+ ),
+ "problem": format_html(
+ '{0} ',
+ self.problem_name,
+ reverse("problem_detail", args=[self.problem.code]),
+ ),
+ }
+ )
def get_context_data(self, **kwargs):
context = super(UserProblemSubmissions, self).get_context_data(**kwargs)
- context['dynamic_user_id'] = self.profile.id
+ context["dynamic_user_id"] = self.profile.id
return context
def single_submission(request):
request.no_profile_update = True
- if 'id' not in request.GET or not request.GET['id'].isdigit():
+ if "id" not in request.GET or not request.GET["id"].isdigit():
return HttpResponseBadRequest()
try:
- show_problem = int(request.GET.get('show_problem', '1'))
+ show_problem = int(request.GET.get("show_problem", "1"))
except ValueError:
return HttpResponseBadRequest()
authenticated = request.user.is_authenticated
- submission = get_object_or_404(submission_related(Submission.objects.all()), id=int(request.GET['id']))
+ submission = get_object_or_404(
+ submission_related(Submission.objects.all()), id=int(request.GET["id"])
+ )
if not submission.problem.is_accessible_by(request.user):
raise Http404()
- return render(request, 'submission/row.html', {
- 'submission': submission,
- 'completed_problem_ids': user_completed_ids(request.profile) if authenticated else [],
- 'editable_problem_ids': user_editable_ids(request.profile) if authenticated else [],
- 'tester_problem_ids': user_tester_ids(request.profile) if authenticated else [],
- 'show_problem': show_problem,
- 'problem_name': show_problem and submission.problem.translated_name(request.LANGUAGE_CODE),
- 'profile_id': request.profile.id if authenticated else 0,
- })
+ return render(
+ request,
+ "submission/row.html",
+ {
+ "submission": submission,
+ "completed_problem_ids": user_completed_ids(request.profile)
+ if authenticated
+ else [],
+ "editable_problem_ids": user_editable_ids(request.profile)
+ if authenticated
+ else [],
+ "tester_problem_ids": user_tester_ids(request.profile)
+ if authenticated
+ else [],
+ "show_problem": show_problem,
+ "problem_name": show_problem
+ and submission.problem.translated_name(request.LANGUAGE_CODE),
+ "profile_id": request.profile.id if authenticated else 0,
+ },
+ )
class AllSubmissions(InfinitePaginationMixin, SubmissionsListBase):
@@ -553,20 +745,27 @@ def use_infinite_pagination(self):
def get_my_submissions_page(self):
if self.request.user.is_authenticated:
- return reverse('all_user_submissions', kwargs={'user': self.request.user.username})
+ return reverse(
+ "all_user_submissions", kwargs={"user": self.request.user.username}
+ )
def get_context_data(self, **kwargs):
context = super(AllSubmissions, self).get_context_data(**kwargs)
- context['dynamic_update'] = context['page_obj'].number == 1
- context['last_msg'] = event.last()
- context['stats_update_interval'] = self.stats_update_interval
+ context["dynamic_update"] = context["page_obj"].number == 1
+ context["last_msg"] = event.last()
+ context["stats_update_interval"] = self.stats_update_interval
return context
def _get_result_data(self, queryset=None):
- if queryset is not None or self.in_contest or self.selected_languages or self.selected_statuses:
+ if (
+ queryset is not None
+ or self.in_contest
+ or self.selected_languages
+ or self.selected_statuses
+ ):
return super(AllSubmissions, self)._get_result_data(queryset)
- key = 'global_submission_result_data'
+ key = "global_submission_result_data"
result = cache.get(key)
if result:
return result
@@ -587,55 +786,82 @@ def contest(self):
def access_check(self, request):
super(ForceContestMixin, self).access_check(request)
- if not request.user.has_perm('judge.see_private_contest'):
+ if not request.user.has_perm("judge.see_private_contest"):
if not self.contest.is_visible:
raise Http404()
- if self.contest.start_time is not None and self.contest.start_time > timezone.now():
+ if (
+ self.contest.start_time is not None
+ and self.contest.start_time > timezone.now()
+ ):
raise Http404()
def get_problem_number(self, problem):
- return self.contest.contest_problems.select_related('problem').get(problem=problem).order
+ return (
+ self.contest.contest_problems.select_related("problem")
+ .get(problem=problem)
+ .order
+ )
def get(self, request, *args, **kwargs):
- if 'contest' not in kwargs:
- raise ImproperlyConfigured('Must pass a contest')
- self._contest = get_object_or_404(Contest, key=kwargs['contest'])
+ if "contest" not in kwargs:
+ raise ImproperlyConfigured("Must pass a contest")
+ self._contest = get_object_or_404(Contest, key=kwargs["contest"])
return super(ForceContestMixin, self).get(request, *args, **kwargs)
class UserAllContestSubmissions(ForceContestMixin, AllUserSubmissions):
def get_title(self):
if self.is_own:
- return _('My submissions in %(contest)s') % {'contest': self.contest.name}
+ return _("My submissions in %(contest)s") % {"contest": self.contest.name}
return _("%(user)s's submissions in %(contest)s") % {
- 'user': self.profile.display_name,
- 'contest': self.contest.name,
+ "user": self.profile.display_name,
+ "contest": self.contest.name,
}
def access_check(self, request):
super().access_check(request)
if not self.contest.users.filter(user_id=self.profile.id).exists():
raise Http404()
- if not self.is_own and not self.contest.can_see_full_scoreboard(self.request.user):
+ if not self.is_own and not self.contest.can_see_full_scoreboard(
+ self.request.user
+ ):
raise Http404()
def get_content_title(self):
if self.is_own:
- return mark_safe(escape(_('My submissions in %(contest)s')) % {
- 'contest': format_html('{0} ', self.contest.name,
- reverse('contest_view', args=[self.contest.key])),
- })
- return mark_safe(escape(_("%(user)s's submissions in %(contest)s")) % {
- 'user': format_html('{0} ', self.profile.display_name,
- reverse('user_page', args=[self.username])),
- 'contest': format_html('{0} ', self.contest.name,
- reverse('contest_view', args=[self.contest.key])),
- })
+ return mark_safe(
+ escape(_("My submissions in %(contest)s"))
+ % {
+ "contest": format_html(
+ '{0} ',
+ self.contest.name,
+ reverse("contest_view", args=[self.contest.key]),
+ ),
+ }
+ )
+ return mark_safe(
+ escape(_("%(user)s's submissions in %(contest)s"))
+ % {
+ "user": format_html(
+ '{0} ',
+ self.profile.display_name,
+ reverse("user_page", args=[self.username]),
+ ),
+ "contest": format_html(
+ '{0} ',
+ self.contest.name,
+ reverse("contest_view", args=[self.contest.key]),
+ ),
+ }
+ )
def get_queryset(self):
queryset = super().get_queryset()
# FIXME: fix this line of code when #1509 is implemented
- if not self.request.user.is_authenticated or self.request.profile.id not in self.contest.editor_ids:
+ if (
+ not self.request.user.is_authenticated
+ or self.request.profile.id not in self.contest.editor_ids
+ ):
filter_submissions_by_visible_problems(queryset, self.request.user)
return queryset
@@ -661,18 +887,37 @@ def access_check(self, request):
def get_content_title(self):
if self.problem.is_accessible_by(self.request.user):
- return mark_safe(escape(_("{user}'s submissions for {problem} in {contest}")).format(
- user=format_html('{0} ', self.profile.display_name,
- reverse('user_page', args=[self.username])),
- problem=format_html('{0} ', self.problem_name,
- reverse('problem_detail', args=[self.problem.code])),
- contest=format_html('{0} ', self.contest.name,
- reverse('contest_view', args=[self.contest.key])),
- ))
- return mark_safe(escape(_("{user}'s submissions for problem {number} in {contest}")).format(
- user=format_html('{0} ', self.profile.display_name,
- reverse('user_page', args=[self.username])),
- number=self.get_problem_number(self.problem),
- contest=format_html('{0} ', self.contest.name,
- reverse('contest_view', args=[self.contest.key])),
- ))
+ return mark_safe(
+ escape(_("{user}'s submissions for {problem} in {contest}")).format(
+ user=format_html(
+ '{0} ',
+ self.profile.display_name,
+ reverse("user_page", args=[self.username]),
+ ),
+ problem=format_html(
+ '{0} ',
+ self.problem_name,
+ reverse("problem_detail", args=[self.problem.code]),
+ ),
+ contest=format_html(
+ '{0} ',
+ self.contest.name,
+ reverse("contest_view", args=[self.contest.key]),
+ ),
+ )
+ )
+ return mark_safe(
+ escape(_("{user}'s submissions for problem {number} in {contest}")).format(
+ user=format_html(
+ '{0} ',
+ self.profile.display_name,
+ reverse("user_page", args=[self.username]),
+ ),
+ number=self.get_problem_number(self.problem),
+ contest=format_html(
+ '{0} ',
+ self.contest.name,
+ reverse("contest_view", args=[self.contest.key]),
+ ),
+ )
+ )
diff --git a/judge/views/tasks.py b/judge/views/tasks.py
index 42e123fbc3..a1c06440bf 100644
--- a/judge/views/tasks.py
+++ b/judge/views/tasks.py
@@ -4,7 +4,12 @@
from celery.result import AsyncResult
from django.core.exceptions import PermissionDenied
-from django.http import Http404, HttpResponseBadRequest, HttpResponseRedirect, JsonResponse
+from django.http import (
+ Http404,
+ HttpResponseBadRequest,
+ HttpResponseRedirect,
+ JsonResponse,
+)
from django.shortcuts import render
from django.urls import reverse
from django.utils.http import is_safe_url
@@ -17,14 +22,19 @@
def get_task_status(task_id):
result = AsyncResult(task_id)
info = result.result
- if result.state == 'PROGRESS':
- return {'code': 'PROGRESS', 'done': info['done'], 'total': info['total'], 'stage': info['stage']}
- elif result.state == 'SUCCESS':
- return {'code': 'SUCCESS'}
- elif result.state == 'FAILURE':
- return {'code': 'FAILURE', 'error': str(info)}
+ if result.state == "PROGRESS":
+ return {
+ "code": "PROGRESS",
+ "done": info["done"],
+ "total": info["total"],
+ "stage": info["stage"],
+ }
+ elif result.state == "SUCCESS":
+ return {"code": "SUCCESS"}
+ elif result.state == "FAILURE":
+ return {"code": "FAILURE", "error": str(info)}
else:
- return {'code': 'WORKING'}
+ return {"code": "WORKING"}
def task_status(request, task_id):
@@ -33,34 +43,48 @@ def task_status(request, task_id):
except ValueError:
raise Http404()
- redirect = request.GET.get('redirect')
+ redirect = request.GET.get("redirect")
if not is_safe_url(redirect, allowed_hosts={request.get_host()}):
redirect = None
status = get_task_status(task_id)
- if status['code'] == 'SUCCESS' and redirect:
+ if status["code"] == "SUCCESS" and redirect:
return HttpResponseRedirect(redirect)
- return render(request, 'task_status.html', {
- 'task_id': task_id, 'task_status': json.dumps(status),
- 'message': request.GET.get('message', ''), 'redirect': redirect or '',
- })
+ return render(
+ request,
+ "task_status.html",
+ {
+ "task_id": task_id,
+ "task_status": json.dumps(status),
+ "message": request.GET.get("message", ""),
+ "redirect": redirect or "",
+ },
+ )
@short_circuit_middleware
def task_status_ajax(request):
- if 'id' not in request.GET:
- return HttpResponseBadRequest('Need to pass GET parameter "id"', content_type='text/plain')
- return JsonResponse(get_task_status(request.GET['id']))
+ if "id" not in request.GET:
+ return HttpResponseBadRequest(
+ 'Need to pass GET parameter "id"', content_type="text/plain"
+ )
+ return JsonResponse(get_task_status(request.GET["id"]))
def demo_task(request, task, message):
if not request.user.is_superuser:
raise PermissionDenied()
result = task.delay()
- return redirect_to_task_status(result, message=message, redirect=reverse('home'))
+ return redirect_to_task_status(result, message=message, redirect=reverse("home"))
-demo_success = partial(demo_task, task=success, message='Running example task that succeeds...')
-demo_failure = partial(demo_task, task=failure, message='Running example task that fails...')
-demo_progress = partial(demo_task, task=progress, message='Running example task that waits 10 seconds...')
+demo_success = partial(
+ demo_task, task=success, message="Running example task that succeeds..."
+)
+demo_failure = partial(
+ demo_task, task=failure, message="Running example task that fails..."
+)
+demo_progress = partial(
+ demo_task, task=progress, message="Running example task that waits 10 seconds..."
+)
diff --git a/judge/views/ticket.py b/judge/views/ticket.py
index eec3d45b43..efa7154d2f 100644
--- a/judge/views/ticket.py
+++ b/judge/views/ticket.py
@@ -2,8 +2,18 @@
from django import forms
from django.contrib.auth.mixins import LoginRequiredMixin
-from django.core.exceptions import ImproperlyConfigured, PermissionDenied, ValidationError
-from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect, JsonResponse
+from django.core.exceptions import (
+ ImproperlyConfigured,
+ PermissionDenied,
+ ValidationError,
+)
+from django.http import (
+ Http404,
+ HttpResponse,
+ HttpResponseBadRequest,
+ HttpResponseRedirect,
+ JsonResponse,
+)
from django.shortcuts import get_object_or_404
from django.template.defaultfilters import truncatechars
from django.template.loader import get_template
@@ -24,61 +34,78 @@
from judge.views.problem import ProblemMixin
from judge.widgets import HeavyPreviewPageDownWidget
-ticket_widget = (forms.Textarea() if HeavyPreviewPageDownWidget is None else
- HeavyPreviewPageDownWidget(preview=reverse_lazy('ticket_preview'),
- preview_timeout=1000, hide_preview_button=True))
+ticket_widget = (
+ forms.Textarea()
+ if HeavyPreviewPageDownWidget is None
+ else HeavyPreviewPageDownWidget(
+ preview=reverse_lazy("ticket_preview"),
+ preview_timeout=1000,
+ hide_preview_button=True,
+ )
+)
class TicketForm(forms.Form):
- title = forms.CharField(max_length=100, label=gettext_lazy('Ticket title'))
+ title = forms.CharField(max_length=100, label=gettext_lazy("Ticket title"))
body = forms.CharField(widget=ticket_widget)
def __init__(self, request, *args, **kwargs):
self.request = request
super(TicketForm, self).__init__(*args, **kwargs)
- self.fields['title'].widget.attrs.update({'placeholder': _('Ticket title')})
- self.fields['body'].widget.attrs.update({'placeholder': _('Issue description')})
+ self.fields["title"].widget.attrs.update({"placeholder": _("Ticket title")})
+ self.fields["body"].widget.attrs.update({"placeholder": _("Issue description")})
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."))
if not self.request.in_contest and not profile.has_any_solves:
- raise ValidationError(_('You must solve at least one problem before you can create a ticket.'))
+ raise ValidationError(
+ _(
+ "You must solve at least one problem before you can create a ticket."
+ )
+ )
return super(TicketForm, self).clean()
class NewTicketView(LoginRequiredMixin, SingleObjectFormView):
form_class = TicketForm
- template_name = 'ticket/new.html'
+ template_name = "ticket/new.html"
def get_assignees(self):
return []
def get_form_kwargs(self):
kwargs = super(NewTicketView, self).get_form_kwargs()
- kwargs['request'] = self.request
+ kwargs["request"] = self.request
return kwargs
def form_valid(self, form):
- ticket = Ticket(user=self.request.profile, title=form.cleaned_data['title'])
+ ticket = Ticket(user=self.request.profile, title=form.cleaned_data["title"])
ticket.linked_item = self.object
ticket.save()
- message = TicketMessage(ticket=ticket, user=ticket.user, body=form.cleaned_data['body'])
+ message = TicketMessage(
+ ticket=ticket, user=ticket.user, body=form.cleaned_data["body"]
+ )
message.save()
ticket.assignees.set(self.get_assignees())
if event.real:
- event.post('tickets', {
- 'type': 'new-ticket', 'id': ticket.id,
- 'message': message.id, 'user': ticket.user_id,
- 'assignees': list(ticket.assignees.values_list('id', flat=True)),
- })
- return HttpResponseRedirect(reverse('ticket', args=[ticket.id]))
+ event.post(
+ "tickets",
+ {
+ "type": "new-ticket",
+ "id": ticket.id,
+ "message": message.id,
+ "user": ticket.user_id,
+ "assignees": list(ticket.assignees.values_list("id", flat=True)),
+ },
+ )
+ return HttpResponseRedirect(reverse("ticket", args=[ticket.id]))
class NewProblemTicketView(ProblemMixin, TitleMixin, NewTicketView):
- template_name = 'ticket/new_problem.html'
+ template_name = "ticket/new_problem.html"
def get_assignees(self):
if self.request.in_contest:
@@ -88,12 +115,17 @@ def get_assignees(self):
return self.object.authors.all()
def get_title(self):
- return _('New ticket for %s') % self.object.name
+ return _("New ticket for %s") % self.object.name
def get_content_title(self):
- return mark_safe(escape(_('New ticket for %s')) %
- format_html('{1} ', reverse('problem_detail', args=[self.object.code]),
- self.object.translated_name(self.request.LANGUAGE_CODE)))
+ return mark_safe(
+ escape(_("New ticket for %s"))
+ % format_html(
+ '{1} ',
+ reverse("problem_detail", args=[self.object.code]),
+ self.object.translated_name(self.request.LANGUAGE_CODE),
+ )
+ )
def form_valid(self, form):
if not self.object.is_accessible_by(self.request.user):
@@ -111,7 +143,7 @@ class TicketMixin(LoginRequiredMixin):
def get_object(self, queryset=None):
ticket = super(TicketMixin, self).get_object(queryset)
profile_id = self.request.profile.id
- if self.request.user.has_perm('judge.change_ticket'):
+ if self.request.user.has_perm("judge.change_ticket"):
return ticket
if ticket.user_id == profile_id:
return ticket
@@ -125,33 +157,51 @@ def get_object(self, queryset=None):
class TicketView(TitleMixin, TicketMixin, SingleObjectFormView):
form_class = TicketCommentForm
- template_name = 'ticket/ticket.html'
- context_object_name = 'ticket'
+ template_name = "ticket/ticket.html"
+ context_object_name = "ticket"
def form_valid(self, form):
- message = TicketMessage(user=self.request.profile,
- body=form.cleaned_data['body'],
- ticket=self.object)
+ message = TicketMessage(
+ user=self.request.profile,
+ body=form.cleaned_data["body"],
+ ticket=self.object,
+ )
message.save()
if event.real:
- event.post('tickets', {
- 'type': 'ticket-message', 'id': self.object.id,
- 'message': message.id, 'user': self.object.user_id,
- 'assignees': list(self.object.assignees.values_list('id', flat=True)),
- })
- event.post('ticket-%d' % self.object.id, {
- 'type': 'ticket-message', 'message': message.id,
- })
- return HttpResponseRedirect('%s#message-%d' % (reverse('ticket', args=[self.object.id]), message.id))
+ event.post(
+ "tickets",
+ {
+ "type": "ticket-message",
+ "id": self.object.id,
+ "message": message.id,
+ "user": self.object.user_id,
+ "assignees": list(
+ self.object.assignees.values_list("id", flat=True)
+ ),
+ },
+ )
+ event.post(
+ "ticket-%d" % self.object.id,
+ {
+ "type": "ticket-message",
+ "message": message.id,
+ },
+ )
+ return HttpResponseRedirect(
+ "%s#message-%d" % (reverse("ticket", args=[self.object.id]), message.id)
+ )
def get_title(self):
- return _('%(title)s - Ticket %(id)d') % {'title': self.object.title, 'id': self.object.id}
+ return _("%(title)s - Ticket %(id)d") % {
+ "title": self.object.title,
+ "id": self.object.id,
+ }
def get_context_data(self, **kwargs):
context = super(TicketView, self).get_context_data(**kwargs)
- context['ticket_messages'] = self.object.messages.select_related('user__user')
- context['assignees'] = self.object.assignees.select_related('user')
- context['last_msg'] = event.last()
+ context["ticket_messages"] = self.object.messages.select_related("user__user")
+ context["assignees"] = self.object.assignees.select_related("user")
+ context["last_msg"] = event.last()
return context
@@ -160,21 +210,32 @@ class TicketStatusChangeView(TicketMixin, SingleObjectMixin, View):
def post(self, request, *args, **kwargs):
if self.open is None:
- raise ImproperlyConfigured('Need to define open')
+ raise ImproperlyConfigured("Need to define open")
ticket = self.get_object()
if ticket.is_open != self.open:
ticket.is_open = self.open
ticket.save()
if event.real:
- event.post('tickets', {
- 'type': 'ticket-status', 'id': ticket.id,
- 'open': self.open, 'user': ticket.user_id,
- 'assignees': list(ticket.assignees.values_list('id', flat=True)),
- 'title': ticket.title,
- })
- event.post('ticket-%d' % ticket.id, {
- 'type': 'ticket-status', 'open': self.open,
- })
+ event.post(
+ "tickets",
+ {
+ "type": "ticket-status",
+ "id": ticket.id,
+ "open": self.open,
+ "user": ticket.user_id,
+ "assignees": list(
+ ticket.assignees.values_list("id", flat=True)
+ ),
+ "title": ticket.title,
+ },
+ )
+ event.post(
+ "ticket-%d" % ticket.id,
+ {
+ "type": "ticket-status",
+ "open": self.open,
+ },
+ )
return HttpResponse(status=204)
@@ -183,16 +244,16 @@ class TicketNotesForm(forms.Form):
class TicketNotesEditView(TicketMixin, SingleObjectFormView):
- template_name = 'ticket/edit-notes.html'
+ template_name = "ticket/edit-notes.html"
form_class = TicketNotesForm
- context_object_name = 'ticket'
+ context_object_name = "ticket"
def get_initial(self):
- return {'notes': self.get_object().notes}
+ return {"notes": self.get_object().notes}
def form_valid(self, form):
ticket = self.get_object()
- ticket.notes = notes = form.cleaned_data['notes']
+ ticket.notes = notes = form.cleaned_data["notes"]
ticket.save()
if notes:
return HttpResponse(linebreaks(notes, autoescape=True))
@@ -205,8 +266,8 @@ def form_invalid(self, form):
class TicketList(LoginRequiredMixin, ListView):
model = Ticket
- template_name = 'ticket/list.html'
- context_object_name = 'tickets'
+ template_name = "ticket/list.html"
+ context_object_name = "tickets"
paginate_by = 50
paginator_class = DiggPaginator
@@ -216,34 +277,40 @@ def profile(self):
@cached_property
def can_edit_all(self):
- return self.request.user.has_perm('judge.change_ticket')
+ return self.request.user.has_perm("judge.change_ticket")
@cached_property
def filter_users(self):
- return self.request.GET.getlist('user')
+ return self.request.GET.getlist("user")
@cached_property
def filter_assignees(self):
- return self.request.GET.getlist('assignee')
+ return self.request.GET.getlist("assignee")
def GET_with_session(self, key):
if not self.request.GET:
return self.request.session.get(key, False)
- return self.request.GET.get(key, None) == '1'
+ return self.request.GET.get(key, None) == "1"
def _get_queryset(self):
- return Ticket.objects.select_related('user__user').prefetch_related('assignees__user').order_by('-id')
+ return (
+ Ticket.objects.select_related("user__user")
+ .prefetch_related("assignees__user")
+ .order_by("-id")
+ )
def get_queryset(self):
queryset = self._get_queryset()
- if self.GET_with_session('open'):
+ if self.GET_with_session("open"):
queryset = queryset.filter(is_open=True)
- if self.GET_with_session('own'):
+ if self.GET_with_session("own"):
queryset = queryset.filter(own_ticket_filter(self.profile.id))
elif not self.can_edit_all:
queryset = filter_visible_tickets(queryset, self.request.user)
if self.filter_assignees:
- queryset = queryset.filter(assignees__user__username__in=self.filter_assignees)
+ queryset = queryset.filter(
+ assignees__user__username__in=self.filter_assignees
+ )
if self.filter_users:
queryset = queryset.filter(user__user__username__in=self.filter_users)
return queryset.distinct()
@@ -251,32 +318,42 @@ def get_queryset(self):
def get_context_data(self, **kwargs):
context = super(TicketList, self).get_context_data(**kwargs)
- page = context['page_obj']
- context['title'] = _('Tickets - Page %(number)d of %(total)d') % {
- 'number': page.number,
- 'total': page.paginator.num_pages,
+ page = context["page_obj"]
+ context["title"] = _("Tickets - Page %(number)d of %(total)d") % {
+ "number": page.number,
+ "total": page.paginator.num_pages,
}
- context['can_edit_all'] = self.can_edit_all
- context['filter_status'] = {
- 'open': self.GET_with_session('open'),
- 'own': self.GET_with_session('own'),
- 'user': self.filter_users,
- 'assignee': self.filter_assignees,
- 'user_id': json.dumps(list(Profile.objects.filter(user__username__in=self.filter_users)
- .values_list('id', flat=True))),
- 'assignee_id': json.dumps(list(Profile.objects.filter(user__username__in=self.filter_assignees)
- .values_list('id', flat=True))),
- 'own_id': self.profile.id if self.GET_with_session('own') else 'null',
+ context["can_edit_all"] = self.can_edit_all
+ context["filter_status"] = {
+ "open": self.GET_with_session("open"),
+ "own": self.GET_with_session("own"),
+ "user": self.filter_users,
+ "assignee": self.filter_assignees,
+ "user_id": json.dumps(
+ list(
+ Profile.objects.filter(
+ user__username__in=self.filter_users
+ ).values_list("id", flat=True)
+ )
+ ),
+ "assignee_id": json.dumps(
+ list(
+ Profile.objects.filter(
+ user__username__in=self.filter_assignees
+ ).values_list("id", flat=True)
+ )
+ ),
+ "own_id": self.profile.id if self.GET_with_session("own") else "null",
}
- context['last_msg'] = event.last()
+ context["last_msg"] = event.last()
context.update(paginate_query_context(self.request))
return context
def post(self, request, *args, **kwargs):
- to_update = ('open', 'own')
+ to_update = ("open", "own")
for key in to_update:
if key in request.GET:
- val = request.GET.get(key) == '1'
+ val = request.GET.get(key) == "1"
request.session[key] = val
else:
request.session.pop(key, None)
@@ -285,38 +362,56 @@ def post(self, request, *args, **kwargs):
class ProblemTicketListView(TicketList):
def _get_queryset(self):
- problem = get_object_or_404(Problem, code=self.kwargs.get('problem'))
+ problem = get_object_or_404(Problem, code=self.kwargs.get("problem"))
if problem.is_editable_by(self.request.user):
- return problem.tickets.order_by('-id')
+ return problem.tickets.order_by("-id")
elif problem.is_accessible_by(self.request.user):
- return problem.tickets.filter(own_ticket_filter(self.profile.id)).order_by('-id')
+ return problem.tickets.filter(own_ticket_filter(self.profile.id)).order_by(
+ "-id"
+ )
raise Http404()
class TicketListDataAjax(TicketMixin, SingleObjectMixin, View):
def get(self, request, *args, **kwargs):
try:
- self.kwargs['pk'] = int(request.GET['id'])
+ self.kwargs["pk"] = int(request.GET["id"])
except (KeyError, ValueError):
return HttpResponseBadRequest()
ticket = self.get_object()
message = ticket.messages.first()
- return JsonResponse({
- 'row': get_template('ticket/row.html').render({'ticket': ticket}, request),
- 'notification': {
- 'title': _('New Ticket: %s') % ticket.title,
- 'body': '%s\n%s' % (_('#%(id)d, assigned to: %(users)s') % {
- 'id': ticket.id,
- 'users': (_(', ').join(ticket.assignees.values_list('user__username', flat=True)) or _('no one')),
- }, truncatechars(message.body, 200)),
- },
- })
+ return JsonResponse(
+ {
+ "row": get_template("ticket/row.html").render(
+ {"ticket": ticket}, request
+ ),
+ "notification": {
+ "title": _("New Ticket: %s") % ticket.title,
+ "body": "%s\n%s"
+ % (
+ _("#%(id)d, assigned to: %(users)s")
+ % {
+ "id": ticket.id,
+ "users": (
+ _(", ").join(
+ ticket.assignees.values_list(
+ "user__username", flat=True
+ )
+ )
+ or _("no one")
+ ),
+ },
+ truncatechars(message.body, 200),
+ ),
+ },
+ }
+ )
class TicketMessageDataAjax(TicketMixin, SingleObjectMixin, View):
def get(self, request, *args, **kwargs):
try:
- message_id = int(request.GET['message'])
+ message_id = int(request.GET["message"])
except (KeyError, ValueError):
return HttpResponseBadRequest()
ticket = self.get_object()
@@ -324,10 +419,14 @@ def get(self, request, *args, **kwargs):
message = ticket.messages.get(id=message_id)
except TicketMessage.DoesNotExist:
return HttpResponseBadRequest()
- return JsonResponse({
- 'message': get_template('ticket/message.html').render({'message': message, 'ticket': ticket}, request),
- 'notification': {
- 'title': _('New Ticket Message For: %s') % ticket.title,
- 'body': truncatechars(message.body, 200),
- },
- })
+ return JsonResponse(
+ {
+ "message": get_template("ticket/message.html").render(
+ {"message": message, "ticket": ticket}, request
+ ),
+ "notification": {
+ "title": _("New Ticket Message For: %s") % ticket.title,
+ "body": truncatechars(message.body, 200),
+ },
+ }
+ )
diff --git a/judge/views/two_factor.py b/judge/views/two_factor.py
index 2e7dfdea73..e6997626e3 100644
--- a/judge/views/two_factor.py
+++ b/judge/views/two_factor.py
@@ -9,7 +9,13 @@
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.views import SuccessURLAllowedHostsMixin
-from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect, JsonResponse
+from django.http import (
+ Http404,
+ HttpResponse,
+ HttpResponseBadRequest,
+ HttpResponseRedirect,
+ JsonResponse,
+)
from django.urls import reverse
from django.utils.http import is_safe_url
from django.utils.translation import gettext as _, gettext_lazy
@@ -29,7 +35,7 @@ class TOTPView(TitleMixin, LoginRequiredMixin, FormView):
def get_form_kwargs(self):
result = super(TOTPView, self).get_form_kwargs()
- result['profile'] = self.profile
+ result["profile"] = self.profile
return result
def dispatch(self, request, *args, **kwargs):
@@ -43,52 +49,56 @@ def check_skip(self):
raise NotImplementedError()
def next_page(self):
- return HttpResponseRedirect(reverse('user_edit_profile'))
+ return HttpResponseRedirect(reverse("user_edit_profile"))
class TOTPEnableView(TOTPView):
- title = gettext_lazy('Enable Two-factor Authentication')
+ title = gettext_lazy("Enable Two-factor Authentication")
form_class = TOTPEnableForm
- template_name = 'registration/totp_enable.html'
+ template_name = "registration/totp_enable.html"
is_edit = False
def get(self, request, *args, **kwargs):
profile = self.profile
- if 'totp_enable_key' not in request.session:
- request.session['totp_enable_key'] = pyotp.random_base32(length=32)
+ if "totp_enable_key" not in request.session:
+ request.session["totp_enable_key"] = pyotp.random_base32(length=32)
if not profile.scratch_codes:
profile.generate_scratch_codes()
return self.render_to_response(self.get_context_data())
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
- kwargs['totp_key'] = self.request.session['totp_enable_key']
+ kwargs["totp_key"] = self.request.session["totp_enable_key"]
return kwargs
def check_skip(self):
return self.profile.is_totp_enabled
def post(self, request, *args, **kwargs):
- if not request.session['totp_enable_key']:
- return HttpResponseBadRequest('No TOTP key generated on server side?')
+ if not request.session["totp_enable_key"]:
+ return HttpResponseBadRequest("No TOTP key generated on server side?")
return super(TOTPEnableView, self).post(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super(TOTPEnableView, self).get_context_data(**kwargs)
- context['totp_key'] = self.request.session['totp_enable_key']
- context['scratch_codes'] = [] if self.is_edit else json.loads(self.profile.scratch_codes)
- context['qr_code'] = self.render_qr_code(self.request.user.username, context['totp_key'])
- context['is_edit'] = self.is_edit
- context['is_hardcore'] = settings.DMOJ_2FA_HARDCORE
+ context["totp_key"] = self.request.session["totp_enable_key"]
+ context["scratch_codes"] = (
+ [] if self.is_edit else json.loads(self.profile.scratch_codes)
+ )
+ context["qr_code"] = self.render_qr_code(
+ self.request.user.username, context["totp_key"]
+ )
+ context["is_edit"] = self.is_edit
+ context["is_hardcore"] = settings.DMOJ_2FA_HARDCORE
return context
def form_valid(self, form):
self.profile.is_totp_enabled = True
- self.profile.totp_key = self.request.session['totp_enable_key']
- self.profile.save(update_fields=['is_totp_enabled', 'totp_key'])
+ self.profile.totp_key = self.request.session["totp_enable_key"]
+ self.profile.save(update_fields=["is_totp_enabled", "totp_key"])
# Make sure users don't get prompted to enter code right after enabling
- self.request.session['2fa_passed'] = True
- del self.request.session['totp_enable_key']
+ self.request.session["2fa_passed"] = True
+ del self.request.session["totp_enable_key"]
return self.next_page()
@classmethod
@@ -100,14 +110,16 @@ def render_qr_code(cls, username, key):
qr.add_data(uri)
qr.make(fit=True)
- image = qr.make_image(fill_color='black', back_color='white')
+ image = qr.make_image(fill_color="black", back_color="white")
buf = BytesIO()
- image.save(buf, format='PNG')
- return 'data:image/png;base64,' + base64.b64encode(buf.getvalue()).decode('ascii')
+ image.save(buf, format="PNG")
+ return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode(
+ "ascii"
+ )
class TOTPEditView(TOTPEnableView):
- title = gettext_lazy('Edit Two-factor Authentication')
+ title = gettext_lazy("Edit Two-factor Authentication")
is_edit = True
def check_skip(self):
@@ -115,13 +127,17 @@ def check_skip(self):
class TOTPDisableView(TOTPView):
- title = gettext_lazy('Disable Two-factor Authentication')
- template_name = 'registration/totp_disable.html'
+ title = gettext_lazy("Disable Two-factor Authentication")
+ template_name = "registration/totp_disable.html"
def check_skip(self):
if not self.profile.is_totp_enabled:
return True
- return settings.DMOJ_REQUIRE_STAFF_2FA and self.request.user.is_staff and not self.profile.is_webauthn_enabled
+ return (
+ settings.DMOJ_REQUIRE_STAFF_2FA
+ and self.request.user.is_staff
+ and not self.profile.is_webauthn_enabled
+ )
def form_valid(self, form):
self.profile.is_totp_enabled = False
@@ -141,7 +157,7 @@ def dispatch(self, request, *args, **kwargs):
class WebAuthnAttestationView(WebAuthnView):
def get(self, request, *args, **kwargs):
challenge = os.urandom(32)
- request.session['webauthn_attest'] = webauthn_encode(challenge)
+ request.session["webauthn_attest"] = webauthn_encode(challenge)
data = webauthn.WebAuthnMakeCredentialOptions(
challenge=challenge,
rp_id=settings.WEBAUTHN_RP_ID,
@@ -149,33 +165,36 @@ def get(self, request, *args, **kwargs):
user_id=request.profile.webauthn_id,
username=request.user.username,
display_name=request.user.username,
- user_verification='discouraged',
+ user_verification="discouraged",
icon_url=gravatar(request.user.email),
- attestation='none',
+ attestation="none",
).registration_dict
- data['excludeCredentials'] = [{
- 'type': 'public-key',
- 'id': {'_bytes': credential.cred_id},
- } for credential in request.profile.webauthn_credentials.all()]
+ data["excludeCredentials"] = [
+ {
+ "type": "public-key",
+ "id": {"_bytes": credential.cred_id},
+ }
+ for credential in request.profile.webauthn_credentials.all()
+ ]
return JsonResponse(data, encoder=WebAuthnJSONEncoder)
def post(self, request, *args, **kwargs):
- if not request.session.get('webauthn_attest'):
+ if not request.session.get("webauthn_attest"):
return HttpResponseBadRequest()
- if 'credential' not in request.POST or len(request.POST['credential']) > 65536:
- return HttpResponseBadRequest(_('Invalid WebAuthn response'))
+ if "credential" not in request.POST or len(request.POST["credential"]) > 65536:
+ return HttpResponseBadRequest(_("Invalid WebAuthn response"))
- if 'name' not in request.POST or len(request.POST['name']) > 100:
- return HttpResponseBadRequest(_('Invalid name'))
+ if "name" not in request.POST or len(request.POST["name"]) > 100:
+ return HttpResponseBadRequest(_("Invalid name"))
- credential = json.loads(request.POST['credential'])
+ credential = json.loads(request.POST["credential"])
response = webauthn.WebAuthnRegistrationResponse(
rp_id=settings.WEBAUTHN_RP_ID,
- origin='https://' + request.get_host(),
- registration_response=credential['response'],
- challenge=request.session['webauthn_attest'],
+ origin="https://" + request.get_host(),
+ registration_response=credential["response"],
+ challenge=request.session["webauthn_attest"],
none_attestation_permitted=True,
)
@@ -185,26 +204,31 @@ def post(self, request, *args, **kwargs):
return HttpResponseBadRequest(str(e))
model = WebAuthnCredential(
- user=request.profile, name=request.POST['name'],
- cred_id=credential.credential_id.decode('ascii'),
- public_key=credential.public_key.decode('ascii'),
+ user=request.profile,
+ name=request.POST["name"],
+ cred_id=credential.credential_id.decode("ascii"),
+ public_key=credential.public_key.decode("ascii"),
counter=credential.sign_count,
)
model.save()
if not request.profile.is_webauthn_enabled:
request.profile.is_webauthn_enabled = True
- request.profile.save(update_fields=['is_webauthn_enabled'])
- return HttpResponse('OK')
+ request.profile.save(update_fields=["is_webauthn_enabled"])
+ return HttpResponse("OK")
class WebAuthnAttestView(WebAuthnView):
def get(self, request, *args, **kwargs):
challenge = os.urandom(32)
- request.session['webauthn_assert'] = webauthn_encode(challenge)
+ request.session["webauthn_assert"] = webauthn_encode(challenge)
data = webauthn.WebAuthnAssertionOptions(
- [credential.webauthn_user for credential in
- request.profile.webauthn_credentials.select_related('user__user')],
+ [
+ credential.webauthn_user
+ for credential in request.profile.webauthn_credentials.select_related(
+ "user__user"
+ )
+ ],
challenge,
).assertion_dict
return JsonResponse(data, encoder=WebAuthnJSONEncoder)
@@ -218,9 +242,13 @@ def post(self, request, *args, **kwargs):
credential = self.get_object()
count = self.get_queryset().count()
- if settings.DMOJ_REQUIRE_STAFF_2FA and self.request.user.is_staff and \
- count <= 1 and not request.profile.is_totp_enabled:
- return HttpResponseBadRequest(_('Staff may not disable 2FA'))
+ if (
+ settings.DMOJ_REQUIRE_STAFF_2FA
+ and self.request.user.is_staff
+ and count <= 1
+ and not request.profile.is_totp_enabled
+ ):
+ return HttpResponseBadRequest(_("Staff may not disable 2FA"))
credential.delete()
return HttpResponse()
@@ -228,34 +256,37 @@ def post(self, request, *args, **kwargs):
class TwoFactorLoginView(SuccessURLAllowedHostsMixin, TOTPView, ContextMixin):
form_class = TwoFactorLoginForm
- title = gettext_lazy('Perform Two-factor Authentication')
- template_name = 'registration/two_factor_auth.html'
- extra_context = {'tfa_in_progress': True}
+ title = gettext_lazy("Perform Two-factor Authentication")
+ template_name = "registration/two_factor_auth.html"
+ extra_context = {"tfa_in_progress": True}
def get_form_kwargs(self):
result = super().get_form_kwargs()
- result['webauthn_challenge'] = self.request.session.get('webauthn_assert')
- result['webauthn_origin'] = 'https://' + self.request.get_host()
+ result["webauthn_challenge"] = self.request.session.get("webauthn_assert")
+ result["webauthn_origin"] = "https://" + self.request.get_host()
return result
def check_skip(self):
- return ((not self.profile.is_totp_enabled and not self.profile.is_webauthn_enabled) or
- self.request.session.get('2fa_passed', False))
+ return (
+ not self.profile.is_totp_enabled and not self.profile.is_webauthn_enabled
+ ) or self.request.session.get("2fa_passed", False)
def next_page(self):
- redirect_to = self.request.GET.get('next', '')
+ redirect_to = self.request.GET.get("next", "")
url_is_safe = is_safe_url(
url=redirect_to,
allowed_hosts=self.get_success_url_allowed_hosts(),
require_https=self.request.is_secure(),
)
- return HttpResponseRedirect((redirect_to if url_is_safe else '') or reverse('user_page'))
+ return HttpResponseRedirect(
+ (redirect_to if url_is_safe else "") or reverse("user_page")
+ )
def form_valid(self, form):
- self.request.session['2fa_passed'] = True
+ self.request.session["2fa_passed"] = True
return self.next_page()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
- context['is_hardcore'] = settings.DMOJ_2FA_HARDCORE
+ context["is_hardcore"] = settings.DMOJ_2FA_HARDCORE
return context
diff --git a/judge/views/user.py b/judge/views/user.py
index 19baad6d66..1170bfd2f4 100644
--- a/judge/views/user.py
+++ b/judge/views/user.py
@@ -11,7 +11,12 @@
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import Permission, User
-from django.contrib.auth.views import LoginView, PasswordChangeView, PasswordResetView, redirect_to_login
+from django.contrib.auth.views import (
+ LoginView,
+ PasswordChangeView,
+ PasswordResetView,
+ redirect_to_login,
+)
from django.contrib.contenttypes.models import ContentType
from django.contrib.sites.shortcuts import get_current_site
from django.core import signing
@@ -31,7 +36,13 @@
from django.views.generic import DetailView, FormView, ListView, TemplateView, View
from reversion import revisions
-from judge.forms import CustomAuthenticationForm, DownloadDataForm, EmailChangeForm, ProfileForm, newsletter_id
+from judge.forms import (
+ CustomAuthenticationForm,
+ DownloadDataForm,
+ EmailChangeForm,
+ ProfileForm,
+ newsletter_id,
+)
from judge.models import Profile, Submission
from judge.performance_points import get_pp_breakdown
from judge.ratings import rating_class, rating_progress
@@ -44,11 +55,24 @@
from judge.utils.ranker import ranker
from judge.utils.subscription import Subscription
from judge.utils.unicode import utf8text
-from judge.utils.views import DiggPaginatorMixin, QueryStringSortMixin, TitleMixin, add_file_response, generic_message
+from judge.utils.views import (
+ DiggPaginatorMixin,
+ QueryStringSortMixin,
+ TitleMixin,
+ add_file_response,
+ generic_message,
+)
from .contests import ContestRanking
-__all__ = ['UserPage', 'UserAboutPage', 'UserProblemsPage', 'UserDownloadData', 'UserPrepareData',
- 'users', 'edit_profile']
+__all__ = [
+ "UserPage",
+ "UserAboutPage",
+ "UserProblemsPage",
+ "UserDownloadData",
+ "UserPrepareData",
+ "users",
+ "edit_profile",
+]
def remap_keys(iterable, mapping):
@@ -57,16 +81,16 @@ def remap_keys(iterable, mapping):
class UserMixin(object):
model = Profile
- slug_field = 'user__username'
- slug_url_kwarg = 'user'
- context_object_name = 'user'
+ slug_field = "user__username"
+ slug_url_kwarg = "user"
+ context_object_name = "user"
def render_to_response(self, context, **response_kwargs):
return super(UserMixin, self).render_to_response(context, **response_kwargs)
class UserPage(TitleMixin, UserMixin, DetailView):
- template_name = 'user/user-base.html'
+ template_name = "user/user-base.html"
def get_object(self, queryset=None):
if self.kwargs.get(self.slug_url_kwarg, None) is None:
@@ -80,12 +104,18 @@ def dispatch(self, request, *args, **kwargs):
try:
return super(UserPage, self).dispatch(request, *args, **kwargs)
except Http404:
- return generic_message(request, _('No such user'), _('No user handle "%s".') %
- self.kwargs.get(self.slug_url_kwarg, None))
+ return generic_message(
+ request,
+ _("No such user"),
+ _('No user handle "%s".') % self.kwargs.get(self.slug_url_kwarg, None),
+ )
def get_title(self):
- return (_('My account') if self.request.user == self.object.user else
- _('User %s') % self.object.display_name)
+ return (
+ _("My account")
+ if self.request.user == self.object.user
+ else _("User %s") % self.object.display_name
+ )
# TODO: the same code exists in problem.py, maybe move to problems.py?
@cached_property
@@ -107,52 +137,76 @@ def get_completed_problems(self):
def get_context_data(self, **kwargs):
context = super(UserPage, self).get_context_data(**kwargs)
- context['hide_solved'] = int(self.hide_solved)
- context['authored'] = self.object.authored_problems.filter(is_public=True, is_organization_private=False) \
- .select_related('group').order_by('code')
- rating = self.object.ratings.order_by('-contest__end_time')[:1]
- context['rating'] = rating[0] if rating else None
+ context["hide_solved"] = int(self.hide_solved)
+ context["authored"] = (
+ self.object.authored_problems.filter(
+ is_public=True, is_organization_private=False
+ )
+ .select_related("group")
+ .order_by("code")
+ )
+ rating = self.object.ratings.order_by("-contest__end_time")[:1]
+ context["rating"] = rating[0] if rating else None
- context['rank'] = Profile.objects.filter(
- is_unlisted=False, performance_points__gt=self.object.performance_points,
- ).exclude(id=self.object.id).count() + 1
+ context["rank"] = (
+ Profile.objects.filter(
+ is_unlisted=False,
+ performance_points__gt=self.object.performance_points,
+ )
+ .exclude(id=self.object.id)
+ .count()
+ + 1
+ )
if rating:
- context['rating_rank'] = Profile.objects.filter(
- is_unlisted=False, rating__gt=self.object.rating,
- ).count() + 1
- context.update(self.object.ratings.aggregate(min_rating=Min('rating'), max_rating=Max('rating'),
- contests=Count('contest')))
+ context["rating_rank"] = (
+ Profile.objects.filter(
+ is_unlisted=False,
+ rating__gt=self.object.rating,
+ ).count()
+ + 1
+ )
+ context.update(
+ self.object.ratings.aggregate(
+ min_rating=Min("rating"),
+ max_rating=Max("rating"),
+ contests=Count("contest"),
+ )
+ )
return context
def get(self, request, *args, **kwargs):
- self.hide_solved = request.GET.get('hide_solved') == '1' if 'hide_solved' in request.GET else False
+ self.hide_solved = (
+ request.GET.get("hide_solved") == "1"
+ if "hide_solved" in request.GET
+ else False
+ )
return super(UserPage, self).get(request, *args, **kwargs)
class CustomLoginView(LoginView):
- template_name = 'registration/login.html'
- extra_context = {'title': gettext_lazy('Login')}
+ template_name = "registration/login.html"
+ extra_context = {"title": gettext_lazy("Login")}
authentication_form = CustomAuthenticationForm
redirect_authenticated_user = True
def form_valid(self, form):
- password = form.cleaned_data['password']
+ password = form.cleaned_data["password"]
validator = PwnedPasswordsValidator()
try:
validator.validate(password)
except ValidationError:
- self.request.session['password_pwned'] = True
+ self.request.session["password_pwned"] = True
else:
- self.request.session['password_pwned'] = False
+ self.request.session["password_pwned"] = False
return super().form_valid(form)
class CustomPasswordChangeView(PasswordChangeView):
- template_name = 'registration/password_change_form.html'
+ template_name = "registration/password_change_form.html"
def form_valid(self, form):
- self.request.session['password_pwned'] = False
+ self.request.session["password_pwned"] = False
return super().form_valid(form)
@@ -160,104 +214,159 @@ def form_valid(self, form):
class UserAboutPage(UserPage):
- template_name = 'user/user-about.html'
+ template_name = "user/user-about.html"
def get_context_data(self, **kwargs):
context = super(UserAboutPage, self).get_context_data(**kwargs)
- ratings = context['ratings'] = self.object.ratings.order_by('-contest__end_time').select_related('contest') \
- .defer('contest__description')
-
- context['rating_data'] = mark_safe(json.dumps([{
- 'label': rating.contest.name,
- 'rating': rating.rating,
- 'ranking': rating.rank,
- 'link': '%s#!%s' % (reverse('contest_ranking', args=(rating.contest.key,)), self.object.user.username),
- 'timestamp': (rating.contest.end_time - EPOCH).total_seconds() * 1000,
- 'date': date_format(timezone.localtime(rating.contest.end_time), _('M j, Y, G:i')),
- 'class': rating_class(rating.rating),
- 'height': '%.3fem' % rating_progress(rating.rating),
- } for rating in ratings]))
+ ratings = context["ratings"] = (
+ self.object.ratings.order_by("-contest__end_time")
+ .select_related("contest")
+ .defer("contest__description")
+ )
+
+ context["rating_data"] = mark_safe(
+ json.dumps(
+ [
+ {
+ "label": rating.contest.name,
+ "rating": rating.rating,
+ "ranking": rating.rank,
+ "link": "%s#!%s"
+ % (
+ reverse("contest_ranking", args=(rating.contest.key,)),
+ self.object.user.username,
+ ),
+ "timestamp": (rating.contest.end_time - EPOCH).total_seconds()
+ * 1000,
+ "date": date_format(
+ timezone.localtime(rating.contest.end_time),
+ _("M j, Y, G:i"),
+ ),
+ "class": rating_class(rating.rating),
+ "height": "%.3fem" % rating_progress(rating.rating),
+ }
+ for rating in ratings
+ ]
+ )
+ )
submissions = (
- self.object.submission_set
- .annotate(date_only=TruncDate('date'))
- .values('date_only').annotate(cnt=Count('id'))
+ self.object.submission_set.annotate(date_only=TruncDate("date"))
+ .values("date_only")
+ .annotate(cnt=Count("id"))
)
- context['submission_data'] = mark_safe(json.dumps({
- date_counts['date_only'].isoformat(): date_counts['cnt'] for date_counts in submissions
- }))
- context['submission_metadata'] = mark_safe(json.dumps({
- 'min_year': (
- self.object.submission_set
- .annotate(year_only=ExtractYear('date'))
- .aggregate(min_year=Min('year_only'))['min_year']
- ),
- }))
+ context["submission_data"] = mark_safe(
+ json.dumps(
+ {
+ date_counts["date_only"].isoformat(): date_counts["cnt"]
+ for date_counts in submissions
+ }
+ )
+ )
+ context["submission_metadata"] = mark_safe(
+ json.dumps(
+ {
+ "min_year": (
+ self.object.submission_set.annotate(
+ year_only=ExtractYear("date")
+ ).aggregate(min_year=Min("year_only"))["min_year"]
+ ),
+ }
+ )
+ )
return context
class UserProblemsPage(UserPage):
- template_name = 'user/user-problems.html'
+ template_name = "user/user-problems.html"
def get_context_data(self, **kwargs):
context = super(UserProblemsPage, self).get_context_data(**kwargs)
- result = Submission.objects.filter(user=self.object, points__gt=0, problem__is_public=True,
- problem__is_organization_private=False) \
- .exclude(problem__in=self.get_completed_problems() if self.hide_solved else []) \
- .values('problem__id', 'problem__code', 'problem__name', 'problem__points', 'problem__group__full_name') \
- .distinct().annotate(points=Max('points')).order_by('problem__group__full_name', 'problem__code')
+ result = (
+ Submission.objects.filter(
+ user=self.object,
+ points__gt=0,
+ problem__is_public=True,
+ problem__is_organization_private=False,
+ )
+ .exclude(
+ problem__in=self.get_completed_problems() if self.hide_solved else []
+ )
+ .values(
+ "problem__id",
+ "problem__code",
+ "problem__name",
+ "problem__points",
+ "problem__group__full_name",
+ )
+ .distinct()
+ .annotate(points=Max("points"))
+ .order_by("problem__group__full_name", "problem__code")
+ )
def process_group(group, problems_iter):
problems = list(problems_iter)
- points = sum(map(itemgetter('points'), problems))
- return {'name': group, 'problems': problems, 'points': points}
-
- context['best_submissions'] = [
- process_group(group, problems) for group, problems in itertools.groupby(
- remap_keys(result, {
- 'problem__code': 'code', 'problem__name': 'name', 'problem__points': 'total',
- 'problem__group__full_name': 'group',
- }), itemgetter('group'))
+ points = sum(map(itemgetter("points"), problems))
+ return {"name": group, "problems": problems, "points": points}
+
+ context["best_submissions"] = [
+ process_group(group, problems)
+ for group, problems in itertools.groupby(
+ remap_keys(
+ result,
+ {
+ "problem__code": "code",
+ "problem__name": "name",
+ "problem__points": "total",
+ "problem__group__full_name": "group",
+ },
+ ),
+ itemgetter("group"),
+ )
]
breakdown, has_more = get_pp_breakdown(self.object, start=0, end=10)
- context['pp_breakdown'] = breakdown
- context['pp_has_more'] = has_more
+ context["pp_breakdown"] = breakdown
+ context["pp_has_more"] = has_more
return context
class UserPerformancePointsAjax(UserProblemsPage):
- template_name = 'user/pp-table-body.html'
+ template_name = "user/pp-table-body.html"
def get_context_data(self, **kwargs):
context = super(UserPerformancePointsAjax, self).get_context_data(**kwargs)
try:
- start = int(self.request.GET.get('start', 0))
- end = int(self.request.GET.get('end', settings.DMOJ_PP_ENTRIES))
+ start = int(self.request.GET.get("start", 0))
+ end = int(self.request.GET.get("end", settings.DMOJ_PP_ENTRIES))
if start < 0 or end < 0 or start > end:
raise ValueError
except ValueError:
start, end = 0, 100
breakdown, self.has_more = get_pp_breakdown(self.object, start=start, end=end)
- context['pp_breakdown'] = breakdown
+ context["pp_breakdown"] = breakdown
return context
def get(self, request, *args, **kwargs):
httpresp = super(UserPerformancePointsAjax, self).get(request, *args, **kwargs)
httpresp.render()
- return JsonResponse({
- 'results': utf8text(httpresp.content),
- 'has_more': self.has_more,
- })
+ return JsonResponse(
+ {
+ "results": utf8text(httpresp.content),
+ "has_more": self.has_more,
+ }
+ )
class UserDataMixin:
@cached_property
def data_path(self):
- return os.path.join(settings.DMOJ_USER_DATA_CACHE, '%s.zip' % self.request.profile.id)
+ return os.path.join(
+ settings.DMOJ_USER_DATA_CACHE, "%s.zip" % self.request.profile.id
+ )
def dispatch(self, request, *args, **kwargs):
if not settings.DMOJ_USER_DATA_DOWNLOAD or self.request.profile.mute:
@@ -266,7 +375,7 @@ def dispatch(self, request, *args, **kwargs):
class UserPrepareData(LoginRequiredMixin, UserDataMixin, TitleMixin, FormView):
- template_name = 'user/prepare-data.html'
+ template_name = "user/prepare-data.html"
form_class = DownloadDataForm
@cached_property
@@ -276,14 +385,16 @@ def _now(self):
@cached_property
def can_prepare_data(self):
return (
- self.request.profile.data_last_downloaded is None or
- self.request.profile.data_last_downloaded + settings.DMOJ_USER_DATA_DOWNLOAD_RATELIMIT < self._now or
- not os.path.exists(self.data_path)
+ self.request.profile.data_last_downloaded is None
+ or self.request.profile.data_last_downloaded
+ + settings.DMOJ_USER_DATA_DOWNLOAD_RATELIMIT
+ < self._now
+ or not os.path.exists(self.data_path)
)
@cached_property
def data_cache_key(self):
- return 'celery_status_id:user_data_download_%s' % self.request.profile.id
+ return "celery_status_id:user_data_download_%s" % self.request.profile.id
@cached_property
def in_progress_url(self):
@@ -291,35 +402,41 @@ def in_progress_url(self):
status = task_status_by_id(status_id).status if status_id else None
return (
self.build_task_url(status_id)
- if status in ('PENDING', 'PROGRESS', 'STARTED')
+ if status in ("PENDING", "PROGRESS", "STARTED")
else None
)
def build_task_url(self, status_id):
return task_status_url_by_id(
- status_id, message=_('Preparing your data...'), redirect=reverse('user_prepare_data'),
+ status_id,
+ message=_("Preparing your data..."),
+ redirect=reverse("user_prepare_data"),
)
def get_title(self):
- return _('Download your data')
+ return _("Download your data")
def form_valid(self, form):
self.request.profile.data_last_downloaded = self._now
self.request.profile.save()
- status = prepare_user_data.delay(self.request.profile.id, json.dumps(form.cleaned_data))
+ status = prepare_user_data.delay(
+ self.request.profile.id, json.dumps(form.cleaned_data)
+ )
cache.set(self.data_cache_key, status.id)
return HttpResponseRedirect(self.build_task_url(status.id))
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
- context['can_prepare_data'] = self.can_prepare_data
- context['can_download_data'] = os.path.exists(self.data_path)
- context['in_progress_url'] = self.in_progress_url
- context['ratelimit'] = settings.DMOJ_USER_DATA_DOWNLOAD_RATELIMIT
+ context["can_prepare_data"] = self.can_prepare_data
+ context["can_download_data"] = os.path.exists(self.data_path)
+ context["in_progress_url"] = self.in_progress_url
+ context["ratelimit"] = settings.DMOJ_USER_DATA_DOWNLOAD_RATELIMIT
if not self.can_prepare_data:
- context['time_until_can_prepare'] = (
- settings.DMOJ_USER_DATA_DOWNLOAD_RATELIMIT - (self._now - self.request.profile.data_last_downloaded)
+ context[
+ "time_until_can_prepare"
+ ] = settings.DMOJ_USER_DATA_DOWNLOAD_RATELIMIT - (
+ self._now - self.request.profile.data_last_downloaded
)
return context
@@ -336,41 +453,64 @@ def get(self, request, *args, **kwargs):
response = HttpResponse()
- if hasattr(settings, 'DMOJ_USER_DATA_INTERNAL'):
- url_path = '%s/%s.zip' % (settings.DMOJ_USER_DATA_INTERNAL, self.request.profile.id)
+ if hasattr(settings, "DMOJ_USER_DATA_INTERNAL"):
+ url_path = "%s/%s.zip" % (
+ settings.DMOJ_USER_DATA_INTERNAL,
+ self.request.profile.id,
+ )
else:
url_path = None
add_file_response(request, response, url_path, self.data_path)
- response['Content-Type'] = 'application/zip'
- response['Content-Disposition'] = 'attachment; filename=%s-data.zip' % self.request.user.username
+ response["Content-Type"] = "application/zip"
+ response["Content-Disposition"] = (
+ "attachment; filename=%s-data.zip" % self.request.user.username
+ )
return response
@login_required
def edit_profile(request):
if request.profile.mute:
- return generic_message(request, _("Can't edit profile"), _('Your part is silent, little toad.'), status=403)
- if request.method == 'POST':
+ return generic_message(
+ request,
+ _("Can't edit profile"),
+ _("Your part is silent, little toad."),
+ status=403,
+ )
+ if request.method == "POST":
form = ProfileForm(request.POST, instance=request.profile, user=request.user)
if form.is_valid():
with revisions.create_revision(atomic=True):
form.save()
revisions.set_user(request.user)
- revisions.set_comment(_('Updated on site'))
+ revisions.set_comment(_("Updated on site"))
if newsletter_id is not None:
try:
- subscription = Subscription.objects.get(user=request.user, newsletter_id=newsletter_id)
+ subscription = Subscription.objects.get(
+ user=request.user, newsletter_id=newsletter_id
+ )
except Subscription.DoesNotExist:
- if form.cleaned_data['newsletter']:
- Subscription(user=request.user, newsletter_id=newsletter_id, subscribed=True).save()
+ if form.cleaned_data["newsletter"]:
+ Subscription(
+ user=request.user,
+ newsletter_id=newsletter_id,
+ subscribed=True,
+ ).save()
else:
- if subscription.subscribed != form.cleaned_data['newsletter']:
- subscription.update(('unsubscribe', 'subscribe')[form.cleaned_data['newsletter']])
-
- perm = Permission.objects.get(codename='test_site', content_type=ContentType.objects.get_for_model(Profile))
- if form.cleaned_data['test_site']:
+ if subscription.subscribed != form.cleaned_data["newsletter"]:
+ subscription.update(
+ ("unsubscribe", "subscribe")[
+ form.cleaned_data["newsletter"]
+ ]
+ )
+
+ perm = Permission.objects.get(
+ codename="test_site",
+ content_type=ContentType.objects.get_for_model(Profile),
+ )
+ if form.cleaned_data["test_site"]:
request.user.user_permissions.add(perm)
else:
request.user.user_permissions.remove(perm)
@@ -380,21 +520,29 @@ def edit_profile(request):
form = ProfileForm(instance=request.profile, user=request.user)
if newsletter_id is not None:
try:
- subscription = Subscription.objects.get(user=request.user, newsletter_id=newsletter_id)
+ subscription = Subscription.objects.get(
+ user=request.user, newsletter_id=newsletter_id
+ )
except Subscription.DoesNotExist:
- form.fields['newsletter'].initial = False
+ form.fields["newsletter"].initial = False
else:
- form.fields['newsletter'].initial = subscription.subscribed
- form.fields['test_site'].initial = request.user.has_perm('judge.test_site')
-
- return render(request, 'user/edit-profile.html', {
- 'require_staff_2fa': settings.DMOJ_REQUIRE_STAFF_2FA,
- 'form': form, 'title': _('Edit profile'), 'profile': request.profile,
- 'can_download_data': bool(settings.DMOJ_USER_DATA_DOWNLOAD),
- 'has_math_config': bool(settings.MATHOID_URL),
- 'ignore_user_script': True,
- 'TIMEZONE_MAP': settings.TIMEZONE_MAP,
- })
+ form.fields["newsletter"].initial = subscription.subscribed
+ form.fields["test_site"].initial = request.user.has_perm("judge.test_site")
+
+ return render(
+ request,
+ "user/edit-profile.html",
+ {
+ "require_staff_2fa": settings.DMOJ_REQUIRE_STAFF_2FA,
+ "form": form,
+ "title": _("Edit profile"),
+ "profile": request.profile,
+ "can_download_data": bool(settings.DMOJ_USER_DATA_DOWNLOAD),
+ "has_math_config": bool(settings.MATHOID_URL),
+ "ignore_user_script": True,
+ "TIMEZONE_MAP": settings.TIMEZONE_MAP,
+ },
+ )
@require_POST
@@ -403,8 +551,8 @@ def generate_api_token(request):
profile = request.profile
with revisions.create_revision(atomic=True):
revisions.set_user(request.user)
- revisions.set_comment(_('Generated API token for user'))
- return JsonResponse({'data': {'token': profile.generate_api_token()}})
+ revisions.set_comment(_("Generated API token for user"))
+ return JsonResponse({"data": {"token": profile.generate_api_token()}})
@require_POST
@@ -415,7 +563,7 @@ def remove_api_token(request):
profile.api_token = None
profile.save()
revisions.set_user(request.user)
- revisions.set_comment(_('Removed API token for user'))
+ revisions.set_comment(_("Removed API token for user"))
return JsonResponse({})
@@ -425,33 +573,50 @@ def generate_scratch_codes(request):
profile = request.profile
with revisions.create_revision(atomic=True):
revisions.set_user(request.user)
- revisions.set_comment(_('Generated scratch codes for user'))
- return JsonResponse({'data': {'codes': profile.generate_scratch_codes()}})
+ revisions.set_comment(_("Generated scratch codes for user"))
+ return JsonResponse({"data": {"codes": profile.generate_scratch_codes()}})
-class UserList(QueryStringSortMixin, InfinitePaginationMixin, DiggPaginatorMixin, TitleMixin, ListView):
+class UserList(
+ QueryStringSortMixin,
+ InfinitePaginationMixin,
+ DiggPaginatorMixin,
+ TitleMixin,
+ ListView,
+):
model = Profile
- title = gettext_lazy('Leaderboard')
- context_object_name = 'users'
- template_name = 'user/list.html'
+ title = gettext_lazy("Leaderboard")
+ context_object_name = "users"
+ template_name = "user/list.html"
paginate_by = 100
- all_sorts = frozenset(('points', 'problem_count', 'rating', 'performance_points'))
+ all_sorts = frozenset(("points", "problem_count", "rating", "performance_points"))
default_desc = all_sorts
- default_sort = '-performance_points'
+ default_sort = "-performance_points"
def get_queryset(self):
- return (Profile.objects.filter(is_unlisted=False).order_by(self.order, 'id').select_related('user')
- .only('display_rank', 'user__username', 'username_display_override', 'points', 'rating',
- 'performance_points', 'problem_count'))
+ return (
+ Profile.objects.filter(is_unlisted=False)
+ .order_by(self.order, "id")
+ .select_related("user")
+ .only(
+ "display_rank",
+ "user__username",
+ "username_display_override",
+ "points",
+ "rating",
+ "performance_points",
+ "problem_count",
+ )
+ )
def get_context_data(self, **kwargs):
context = super(UserList, self).get_context_data(**kwargs)
- context['users'] = ranker(
- context['users'],
- key=attrgetter('performance_points', 'problem_count'),
- rank=self.paginate_by * (context['page_obj'].number - 1),
+ context["users"] = ranker(
+ context["users"],
+ key=attrgetter("performance_points", "problem_count"),
+ rank=self.paginate_by * (context["page_obj"].number - 1),
)
- context['first_page_href'] = '.'
+ context["first_page_href"] = "."
context.update(self.get_sort_context())
context.update(self.get_sort_paginate_context())
return context
@@ -472,27 +637,36 @@ def users(request):
participation = request.profile.current_contest
if participation is not None:
contest = participation.contest
- return FixedContestRanking.as_view(contest=contest)(request, contest=contest.key)
+ return FixedContestRanking.as_view(contest=contest)(
+ request, contest=contest.key
+ )
return user_list_view(request)
def user_ranking_redirect(request):
try:
- username = request.GET['handle']
+ username = request.GET["handle"]
except KeyError:
raise Http404()
user = get_object_or_404(Profile, user__username=username)
- rank = Profile.objects.filter(is_unlisted=False, performance_points__gt=user.performance_points).count()
+ rank = Profile.objects.filter(
+ is_unlisted=False, performance_points__gt=user.performance_points
+ ).count()
rank += Profile.objects.filter(
- is_unlisted=False, performance_points__exact=user.performance_points, id__lt=user.id,
+ is_unlisted=False,
+ performance_points__exact=user.performance_points,
+ id__lt=user.id,
).count()
page = rank // UserList.paginate_by
- return HttpResponseRedirect('%s%s#!%s' % (reverse('user_list'), '?page=%d' % (page + 1) if page else '', username))
+ return HttpResponseRedirect(
+ "%s%s#!%s"
+ % (reverse("user_list"), "?page=%d" % (page + 1) if page else "", username)
+ )
class UserLogoutView(TitleMixin, TemplateView):
- template_name = 'registration/logout.html'
- title = gettext_lazy('You have been successfully logged out.')
+ template_name = "registration/logout.html"
+ title = gettext_lazy("You have been successfully logged out.")
def post(self, request, *args, **kwargs):
auth_logout(request)
@@ -503,50 +677,63 @@ def post(self, request, *args, **kwargs):
class CustomPasswordResetView(PasswordResetView):
- template_name = 'registration/password_reset.html'
- html_email_template_name = 'registration/password_reset_email.html'
- email_template_name = 'registration/password_reset_email.txt'
- extra_email_context = {'site_admin_email': settings.SITE_ADMIN_EMAIL}
+ template_name = "registration/password_reset.html"
+ html_email_template_name = "registration/password_reset_email.html"
+ email_template_name = "registration/password_reset_email.txt"
+ extra_email_context = {"site_admin_email": settings.SITE_ADMIN_EMAIL}
def post(self, request, *args, **kwargs):
key = f'pwreset!{request.META["REMOTE_ADDR"]}'
- cache.add(key, 0, timeout=settings.DMOJ_PASSWORD_RESET_LIMIT_WINDOW * MINUTES_TO_SECONDS)
+ cache.add(
+ key,
+ 0,
+ timeout=settings.DMOJ_PASSWORD_RESET_LIMIT_WINDOW * MINUTES_TO_SECONDS,
+ )
if cache.incr(key) > settings.DMOJ_PASSWORD_RESET_LIMIT_COUNT:
- return HttpResponse(_('You have sent too many password reset requests. Please try again later.'),
- content_type='text/plain', status=429)
+ return HttpResponse(
+ _(
+ "You have sent too many password reset requests. Please try again later."
+ ),
+ content_type="text/plain",
+ status=429,
+ )
return super().post(request, *args, **kwargs)
class EmailChangeRequestView(LoginRequiredMixin, TitleMixin, FormView):
- title = _('Change your email')
- template_name = 'registration/email_change.html'
+ title = _("Change your email")
+ template_name = "registration/email_change.html"
form_class = EmailChangeForm
- activate_html_email_template_name = 'registration/email_change_activate_email.html'
- activate_email_template_name = 'registration/email_change_activate_email.txt'
- activate_subject_template_name = 'registration/email_change_activate_subject.txt'
- notify_html_email_template_name = 'registration/email_change_notify_email.html'
- notify_email_template_name = 'registration/email_change_notify_email.txt'
- notify_subject_template_name = 'registration/email_change_notify_subject.txt'
+ activate_html_email_template_name = "registration/email_change_activate_email.html"
+ activate_email_template_name = "registration/email_change_activate_email.txt"
+ activate_subject_template_name = "registration/email_change_activate_subject.txt"
+ notify_html_email_template_name = "registration/email_change_notify_email.html"
+ notify_email_template_name = "registration/email_change_notify_email.txt"
+ notify_subject_template_name = "registration/email_change_notify_subject.txt"
def form_valid(self, form):
signer = signing.TimestampSigner()
- new_email = form.cleaned_data['email']
- activation_key = base64.urlsafe_b64encode(signer.sign_object({
- 'id': self.request.user.id,
- 'email': new_email,
- }).encode()).decode()
+ new_email = form.cleaned_data["email"]
+ activation_key = base64.urlsafe_b64encode(
+ signer.sign_object(
+ {
+ "id": self.request.user.id,
+ "email": new_email,
+ }
+ ).encode()
+ ).decode()
current_site = get_current_site(self.request)
context = {
- 'domain': current_site.domain,
- 'site_name': current_site.name,
- 'protocol': 'https' if self.request.is_secure() else 'http',
- 'site_admin_email': settings.SITE_ADMIN_EMAIL,
- 'expiry_minutes': settings.DMOJ_EMAIL_CHANGE_EXPIRY_MINUTES,
- 'user': self.request.user,
- 'activation_key': activation_key,
- 'new_email': new_email,
+ "domain": current_site.domain,
+ "site_name": current_site.name,
+ "protocol": "https" if self.request.is_secure() else "http",
+ "site_admin_email": settings.SITE_ADMIN_EMAIL,
+ "expiry_minutes": settings.DMOJ_EMAIL_CHANGE_EXPIRY_MINUTES,
+ "user": self.request.user,
+ "activation_key": activation_key,
+ "new_email": new_email,
}
send_mail(
context,
@@ -565,21 +752,28 @@ def form_valid(self, form):
return generic_message(
self.request,
- _('Email change requested'),
- _('Please click on the link sent to %s.') % new_email,
+ _("Email change requested"),
+ _("Please click on the link sent to %s.") % new_email,
)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
- kwargs['user'] = self.request.user
+ kwargs["user"] = self.request.user
return kwargs
def post(self, request, *args, **kwargs):
key = f'emailchange!{request.META["REMOTE_ADDR"]}'
- cache.add(key, 0, timeout=settings.DMOJ_EMAIL_CHANGE_LIMIT_WINDOW * MINUTES_TO_SECONDS)
+ cache.add(
+ key, 0, timeout=settings.DMOJ_EMAIL_CHANGE_LIMIT_WINDOW * MINUTES_TO_SECONDS
+ )
if cache.incr(key) > settings.DMOJ_EMAIL_CHANGE_LIMIT_COUNT:
- return HttpResponse(_('You have sent too many email change requests. Please try again later.'),
- content_type='text/plain', status=429)
+ return HttpResponse(
+ _(
+ "You have sent too many email change requests. Please try again later."
+ ),
+ content_type="text/plain",
+ status=429,
+ )
return super().post(request, *args, **kwargs)
@@ -595,35 +789,48 @@ def update_user_email(self, request, activation_key):
max_age=settings.DMOJ_EMAIL_CHANGE_EXPIRY_MINUTES * MINUTES_TO_SECONDS,
)
except (binascii.Error, signing.BadSignature):
- raise self.EmailChangeFailedError(_('Invalid activation key. Please try again.'))
+ raise self.EmailChangeFailedError(
+ _("Invalid activation key. Please try again.")
+ )
except signing.SignatureExpired:
- raise self.EmailChangeFailedError(_('This request has expired. Please try again.'))
- if data['id'] != request.user.id:
raise self.EmailChangeFailedError(
- _('Please try again while logged in to the account this email change was originally requested from.'),
+ _("This request has expired. Please try again.")
+ )
+ if data["id"] != request.user.id:
+ raise self.EmailChangeFailedError(
+ _(
+ "Please try again while logged in to the account this email change was originally requested from."
+ ),
)
from_email = request.user.email
- to_email = data['email']
+ to_email = data["email"]
with revisions.create_revision(atomic=True):
if User.objects.filter(email=to_email).exists():
raise self.EmailChangeFailedError(
- _('The email you originally requested has since been registered by another user. '
- 'Please try again with a new email.'),
+ _(
+ "The email you originally requested has since been registered by another user. "
+ "Please try again with a new email."
+ ),
)
request.user.email = to_email
request.user.save()
revisions.set_user(request.user)
- revisions.set_comment(_('Changed email address from %s to %s') % (from_email, to_email))
+ revisions.set_comment(
+ _("Changed email address from %s to %s") % (from_email, to_email)
+ )
return to_email
def get(self, request, *args, **kwargs):
try:
- to_email = self.update_user_email(request, kwargs['activation_key'])
+ to_email = self.update_user_email(request, kwargs["activation_key"])
except self.EmailChangeFailedError as e:
- return generic_message(request, _('Email change failed'), str(e), status=403)
+ return generic_message(
+ request, _("Email change failed"), str(e), status=403
+ )
else:
return generic_message(
request,
- _('Email successfully changed'),
- _('The email attached to your account has been changed to %s.') % to_email,
+ _("Email successfully changed"),
+ _("The email attached to your account has been changed to %s.")
+ % to_email,
)
diff --git a/judge/views/widgets.py b/judge/views/widgets.py
index f1aec4ef0f..9fd15f1ea9 100644
--- a/judge/views/widgets.py
+++ b/judge/views/widgets.py
@@ -6,23 +6,28 @@
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.core.files.storage import default_storage
-from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseRedirect
+from django.http import (
+ HttpResponse,
+ HttpResponseBadRequest,
+ HttpResponseForbidden,
+ HttpResponseRedirect,
+)
from django.views.decorators.http import require_POST
from martor.api import imgur_uploader
from judge.models import Submission
-__all__ = ['rejudge_submission']
+__all__ = ["rejudge_submission"]
@login_required
@require_POST
def rejudge_submission(request):
- if 'id' not in request.POST or not request.POST['id'].isdigit():
+ if "id" not in request.POST or not request.POST["id"].isdigit():
return HttpResponseBadRequest()
try:
- submission = Submission.objects.get(id=request.POST['id'])
+ submission = Submission.objects.get(id=request.POST["id"])
except Submission.DoesNotExist:
return HttpResponseBadRequest()
@@ -31,32 +36,43 @@ def rejudge_submission(request):
submission.judge(rejudge=True, rejudge_user=request.user)
- redirect = request.POST.get('path', None)
+ redirect = request.POST.get("path", None)
- return HttpResponseRedirect(redirect) if redirect else HttpResponse('success', content_type='text/plain')
+ return (
+ HttpResponseRedirect(redirect)
+ if redirect
+ else HttpResponse("success", content_type="text/plain")
+ )
def django_uploader(image):
ext = os.path.splitext(image.name)[1]
if ext not in settings.MARTOR_UPLOAD_SAFE_EXTS:
- ext = '.png'
+ ext = ".png"
name = str(uuid.uuid4()) + ext
default_storage.save(os.path.join(settings.MARTOR_UPLOAD_MEDIA_DIR, name), image)
- url_base = getattr(settings, 'MARTOR_UPLOAD_URL_PREFIX',
- urljoin(settings.MEDIA_URL, settings.MARTOR_UPLOAD_MEDIA_DIR))
- if not url_base.endswith('/'):
- url_base += '/'
- return json.dumps({'status': 200, 'name': '', 'link': urljoin(url_base, name)})
+ url_base = getattr(
+ settings,
+ "MARTOR_UPLOAD_URL_PREFIX",
+ urljoin(settings.MEDIA_URL, settings.MARTOR_UPLOAD_MEDIA_DIR),
+ )
+ if not url_base.endswith("/"):
+ url_base += "/"
+ return json.dumps({"status": 200, "name": "", "link": urljoin(url_base, name)})
@login_required
def martor_image_uploader(request):
- if request.method != 'POST' or not request.is_ajax() or 'markdown-image-upload' not in request.FILES:
- return HttpResponseBadRequest('Invalid request')
+ if (
+ request.method != "POST"
+ or not request.is_ajax()
+ or "markdown-image-upload" not in request.FILES
+ ):
+ return HttpResponseBadRequest("Invalid request")
- image = request.FILES['markdown-image-upload']
+ image = request.FILES["markdown-image-upload"]
if request.user.is_staff:
data = django_uploader(image)
else:
data = imgur_uploader(image)
- return HttpResponse(data, content_type='application/json')
+ return HttpResponse(data, content_type="application/json")
diff --git a/judge/widgets/checkbox.py b/judge/widgets/checkbox.py
index 0a7eebfb48..b4b2249794 100644
--- a/judge/widgets/checkbox.py
+++ b/judge/widgets/checkbox.py
@@ -6,17 +6,25 @@
class CheckboxSelectMultipleWithSelectAll(forms.CheckboxSelectMultiple):
def render(self, name, value, attrs=None, renderer=None):
- if 'id' not in attrs:
- raise FieldError('id required')
+ if "id" not in attrs:
+ raise FieldError("id required")
- select_all_id = attrs['id'] + '_all'
- select_all_name = name + '_all'
- original = super(CheckboxSelectMultipleWithSelectAll, self).render(name, value, attrs, renderer)
- template = get_template('widgets/select_all.html')
- return mark_safe(template.render({
- 'original_widget': original,
- 'select_all_id': select_all_id,
- 'select_all_name': select_all_name,
- 'all_selected': all(choice[0] in value for choice in self.choices) if value else False,
- 'empty': not self.choices,
- }))
+ select_all_id = attrs["id"] + "_all"
+ select_all_name = name + "_all"
+ original = super(CheckboxSelectMultipleWithSelectAll, self).render(
+ name, value, attrs, renderer
+ )
+ template = get_template("widgets/select_all.html")
+ return mark_safe(
+ template.render(
+ {
+ "original_widget": original,
+ "select_all_id": select_all_id,
+ "select_all_name": select_all_name,
+ "all_selected": all(choice[0] in value for choice in self.choices)
+ if value
+ else False,
+ "empty": not self.choices,
+ }
+ )
+ )
diff --git a/judge/widgets/martor.py b/judge/widgets/martor.py
index 9f8738e0bf..f9c096f9d4 100644
--- a/judge/widgets/martor.py
+++ b/judge/widgets/martor.py
@@ -1,17 +1,20 @@
-from martor.widgets import AdminMartorWidget as OldAdminMartorWidget, MartorWidget as OldMartorWidget
+from martor.widgets import (
+ AdminMartorWidget as OldAdminMartorWidget,
+ MartorWidget as OldMartorWidget,
+)
-__all__ = ['MartorWidget', 'AdminMartorWidget']
+__all__ = ["MartorWidget", "AdminMartorWidget"]
class MartorWidget(OldMartorWidget):
class Media:
css = {
- 'all': ['martor-description.css'],
+ "all": ["martor-description.css"],
}
- js = ['martor-mathjax.js']
+ js = ["martor-mathjax.js"]
class AdminMartorWidget(OldAdminMartorWidget):
class Media:
css = MartorWidget.Media.css
- js = ['admin/js/jquery.init.js', 'martor-mathjax.js']
+ js = ["admin/js/jquery.init.js", "martor-mathjax.js"]
diff --git a/judge/widgets/mixins.py b/judge/widgets/mixins.py
index c6e97e41ab..6317bac543 100644
--- a/judge/widgets/mixins.py
+++ b/judge/widgets/mixins.py
@@ -7,23 +7,27 @@
class CompressorWidgetMixin(object):
- __template_css = dedent("""\
+ __template_css = dedent(
+ """\
{% compress css %}
{{ media.css }}
{% endcompress %}
- """)
+ """
+ )
- __template_js = dedent("""\
+ __template_js = dedent(
+ """\
{% compress js %}
{{ media.js }}
{% endcompress %}
- """)
+ """
+ )
__templates = {
- (False, False): Template(''),
- (True, False): Template('{% load compress %}' + __template_css),
- (False, True): Template('{% load compress %}' + __template_js),
- (True, True): Template('{% load compress %}' + __template_js + __template_css),
+ (False, False): Template(""),
+ (True, False): Template("{% load compress %}" + __template_css),
+ (False, True): Template("{% load compress %}" + __template_js),
+ (True, True): Template("{% load compress %}" + __template_js + __template_css),
}
compress_css = False
@@ -34,14 +38,19 @@ class CompressorWidgetMixin(object):
except ImportError:
pass
else:
- if getattr(settings, 'COMPRESS_ENABLED', not settings.DEBUG):
+ if getattr(settings, "COMPRESS_ENABLED", not settings.DEBUG):
+
@property
def media(self):
media = super().media
template = self.__templates[self.compress_css, self.compress_js]
- result = html.fromstring(template.render(Context({'media': media})))
+ result = html.fromstring(template.render(Context({"media": media})))
return forms.Media(
- css={'all': [result.find('.//link').get('href')]} if self.compress_css else media._css,
- js=[result.find('.//script').get('src')] if self.compress_js else media._js,
+ css={"all": [result.find(".//link").get("href")]}
+ if self.compress_css
+ else media._css,
+ js=[result.find(".//script").get("src")]
+ if self.compress_js
+ else media._js,
)
diff --git a/judge/widgets/pagedown.py b/judge/widgets/pagedown.py
index dc07359215..daf41a1bd1 100644
--- a/judge/widgets/pagedown.py
+++ b/judge/widgets/pagedown.py
@@ -5,7 +5,7 @@
from judge.widgets.mixins import CompressorWidgetMixin
-__all__ = ['PagedownWidget', 'MathJaxPagedownWidget', 'HeavyPreviewPageDownWidget']
+__all__ = ["PagedownWidget", "MathJaxPagedownWidget", "HeavyPreviewPageDownWidget"]
try:
from pagedown.widgets import PagedownWidget as OldPagedownWidget
@@ -14,6 +14,7 @@
MathJaxPagedownWidget = None
HeavyPreviewPageDownWidget = None
else:
+
class PagedownWidget(CompressorWidgetMixin, OldPagedownWidget):
# The goal here is to compress all the pagedown JS into one file.
# We do not want any further compress down the chain, because
@@ -22,46 +23,46 @@ class PagedownWidget(CompressorWidgetMixin, OldPagedownWidget):
compress_js = True
def __init__(self, *args, **kwargs):
- kwargs.setdefault('css', ())
+ kwargs.setdefault("css", ())
super(PagedownWidget, self).__init__(*args, **kwargs)
-
class MathJaxPagedownWidget(PagedownWidget):
class Media:
js = [
- 'mathjax_config.js',
- 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.2.0/es5/tex-chtml.min.js',
- 'pagedown_math.js',
+ "mathjax_config.js",
+ "https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.2.0/es5/tex-chtml.min.js",
+ "pagedown_math.js",
]
-
class HeavyPreviewPageDownWidget(PagedownWidget):
def __init__(self, *args, **kwargs):
- kwargs.setdefault('template', 'pagedown.html')
- self.preview_url = kwargs.pop('preview')
- self.preview_timeout = kwargs.pop('preview_timeout', None)
- self.hide_preview_button = kwargs.pop('hide_preview_button', False)
+ kwargs.setdefault("template", "pagedown.html")
+ self.preview_url = kwargs.pop("preview")
+ self.preview_timeout = kwargs.pop("preview_timeout", None)
+ self.hide_preview_button = kwargs.pop("hide_preview_button", False)
super(HeavyPreviewPageDownWidget, self).__init__(*args, **kwargs)
def render(self, name, value, attrs=None, renderer=None):
if value is None:
- value = ''
- final_attrs = self.build_attrs(attrs, {'name': name})
- if 'class' not in final_attrs:
- final_attrs['class'] = ''
- final_attrs['class'] += ' wmd-input'
- return get_template(self.template).render(self.get_template_context(final_attrs, value))
+ value = ""
+ final_attrs = self.build_attrs(attrs, {"name": name})
+ if "class" not in final_attrs:
+ final_attrs["class"] = ""
+ final_attrs["class"] += " wmd-input"
+ return get_template(self.template).render(
+ self.get_template_context(final_attrs, value)
+ )
def get_template_context(self, attrs, value):
return {
- 'attrs': flatatt(attrs),
- 'body': conditional_escape(force_str(value)),
- 'id': attrs['id'],
- 'show_preview': self.show_preview,
- 'preview_url': self.preview_url,
- 'preview_timeout': self.preview_timeout,
- 'extra_classes': 'dmmd-no-button' if self.hide_preview_button else None,
+ "attrs": flatatt(attrs),
+ "body": conditional_escape(force_str(value)),
+ "id": attrs["id"],
+ "show_preview": self.show_preview,
+ "preview_url": self.preview_url,
+ "preview_timeout": self.preview_timeout,
+ "extra_classes": "dmmd-no-button" if self.hide_preview_button else None,
}
class Media:
- js = ['dmmd-preview.js']
+ js = ["dmmd-preview.js"]
diff --git a/judge/widgets/select2.py b/judge/widgets/select2.py
index 8ae0659851..64c39fe2ac 100644
--- a/judge/widgets/select2.py
+++ b/judge/widgets/select2.py
@@ -46,10 +46,18 @@
from django.forms.models import ModelChoiceIterator
from django.urls import reverse_lazy
-__all__ = ['Select2Widget', 'Select2MultipleWidget', 'Select2TagWidget',
- 'HeavySelect2Widget', 'HeavySelect2MultipleWidget', 'HeavySelect2TagWidget',
- 'AdminSelect2Widget', 'AdminSelect2MultipleWidget', 'AdminHeavySelect2Widget',
- 'AdminHeavySelect2MultipleWidget']
+__all__ = [
+ "Select2Widget",
+ "Select2MultipleWidget",
+ "Select2TagWidget",
+ "HeavySelect2Widget",
+ "HeavySelect2MultipleWidget",
+ "HeavySelect2TagWidget",
+ "AdminSelect2Widget",
+ "AdminSelect2MultipleWidget",
+ "AdminHeavySelect2Widget",
+ "AdminHeavySelect2MultipleWidget",
+]
class Select2Mixin(object):
@@ -64,24 +72,24 @@ class Select2Mixin(object):
def build_attrs(self, base_attrs, extra_attrs=None):
"""Add select2 data attributes."""
attrs = super(Select2Mixin, self).build_attrs(base_attrs, extra_attrs)
- attrs.setdefault('data-theme', settings.DMOJ_SELECT2_THEME)
+ attrs.setdefault("data-theme", settings.DMOJ_SELECT2_THEME)
if self.is_required:
- attrs.setdefault('data-allow-clear', 'false')
+ attrs.setdefault("data-allow-clear", "false")
else:
- attrs.setdefault('data-allow-clear', 'true')
- attrs.setdefault('data-placeholder', '')
+ attrs.setdefault("data-allow-clear", "true")
+ attrs.setdefault("data-placeholder", "")
- attrs.setdefault('data-minimum-input-length', 0)
- if 'class' in attrs:
- attrs['class'] += ' django-select2'
+ attrs.setdefault("data-minimum-input-length", 0)
+ if "class" in attrs:
+ attrs["class"] += " django-select2"
else:
- attrs['class'] = 'django-select2'
+ attrs["class"] = "django-select2"
return attrs
def optgroups(self, name, value, attrs=None):
"""Add empty option for clearable selects."""
if not self.is_required and not self.allow_multiple_selected:
- self.choices = list(chain([('', '')], self.choices))
+ self.choices = list(chain([("", "")], self.choices))
return super(Select2Mixin, self).optgroups(name, value, attrs=attrs)
@property
@@ -93,8 +101,8 @@ def media(self):
https://docs.djangoproject.com/en/1.8/topics/forms/media/#media-as-a-dynamic-property
"""
return forms.Media(
- js=[settings.SELECT2_JS_URL, 'django_select2.js'],
- css={'screen': [settings.SELECT2_CSS_URL]},
+ js=[settings.SELECT2_JS_URL, "django_select2.js"],
+ css={"screen": [settings.SELECT2_CSS_URL]},
)
@@ -102,8 +110,12 @@ class AdminSelect2Mixin(Select2Mixin):
@property
def media(self):
return forms.Media(
- js=['admin/js/jquery.init.js', settings.SELECT2_JS_URL, 'django_select2.js'],
- css={'screen': [settings.SELECT2_CSS_URL, 'select2-dmoj.css']},
+ js=[
+ "admin/js/jquery.init.js",
+ settings.SELECT2_JS_URL,
+ "django_select2.js",
+ ],
+ css={"screen": [settings.SELECT2_CSS_URL, "select2-dmoj.css"]},
)
@@ -113,9 +125,9 @@ class Select2TagMixin(object):
def build_attrs(self, base_attrs, extra_attrs=None):
"""Add select2's tag attributes."""
extra_attrs = extra_attrs or {}
- extra_attrs.setdefault('data-minimum-input-length', 1)
- extra_attrs.setdefault('data-tags', 'true')
- extra_attrs.setdefault('data-token-separators', [',', ' '])
+ extra_attrs.setdefault("data-minimum-input-length", 1)
+ extra_attrs.setdefault("data-tags", "true")
+ extra_attrs.setdefault("data-token-separators", [",", " "])
return super(Select2TagMixin, self).build_attrs(base_attrs, extra_attrs)
@@ -180,8 +192,8 @@ def __init__(self, attrs=None, choices=(), **kwargs):
else:
self.attrs = {}
- self.data_view = kwargs.pop('data_view', None)
- self.data_url = kwargs.pop('data_url', None)
+ self.data_view = kwargs.pop("data_view", None)
+ self.data_url = kwargs.pop("data_url", None)
if not (self.data_view or self.data_url):
raise ValueError('You must ether specify "data_view" or "data_url".')
@@ -199,24 +211,27 @@ def build_attrs(self, base_attrs, extra_attrs=None):
# encrypt instance Id
self.widget_id = signing.dumps(id(self))
- attrs['data-field_id'] = self.widget_id
- attrs.setdefault('data-ajax--url', self.get_url())
- attrs.setdefault('data-ajax--cache', 'true')
- attrs.setdefault('data-ajax--type', 'GET')
- attrs.setdefault('data-minimum-input-length', 2)
+ attrs["data-field_id"] = self.widget_id
+ attrs.setdefault("data-ajax--url", self.get_url())
+ attrs.setdefault("data-ajax--cache", "true")
+ attrs.setdefault("data-ajax--type", "GET")
+ attrs.setdefault("data-minimum-input-length", 2)
- attrs['class'] += ' django-select2-heavy'
+ attrs["class"] += " django-select2-heavy"
return attrs
def format_value(self, value):
result = super(HeavySelect2Mixin, self).format_value(value)
if isinstance(self.choices, ModelChoiceIterator):
chosen = copy(self.choices)
- chosen.queryset = chosen.queryset.filter(pk__in=[
- int(i) for i in result if isinstance(i, int) or i.isdigit()
- ])
+ chosen.queryset = chosen.queryset.filter(
+ pk__in=[int(i) for i in result if isinstance(i, int) or i.isdigit()]
+ )
# https://code.djangoproject.com/ticket/33155
- self.choices = {(value if isinstance(value, str) else value.value, label) for value, label in chosen}
+ self.choices = {
+ (value if isinstance(value, str) else value.value, label)
+ for value, label in chosen
+ }
return result
diff --git a/manage.py b/manage.py
index adaffbd994..3466717870 100755
--- a/manage.py
+++ b/manage.py
@@ -7,8 +7,9 @@
except ImportError:
import dmoj_install_pymysql # noqa: F401, imported for side effect
-if __name__ == '__main__':
- os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dmoj.settings')
+if __name__ == "__main__":
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dmoj.settings")
from django.core.management import execute_from_command_line
+
execute_from_command_line(sys.argv)