From bf2541651e053129d183d0a4745b62bb5e5d7c72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Ho=C3=A0ng=20Ph=C3=BAc?= Date: Sat, 14 Sep 2024 19:43:22 +0700 Subject: [PATCH 01/13] Add BadgeRequest model --- judge/admin/__init__.py | 39 ++- judge/models/__init__.py | 82 ++++- judge/models/profile.py | 652 +++++++++++++++++++++++++++------------ 3 files changed, 556 insertions(+), 217 deletions(-) diff --git a/judge/admin/__init__.py b/judge/admin/__init__.py index c4e2751bd..6397972bd 100644 --- a/judge/admin/__init__.py +++ b/judge/admin/__init__.py @@ -5,7 +5,13 @@ from judge.admin.comments import CommentAdmin from judge.admin.contest import ContestAdmin, ContestParticipationAdmin, ContestTagAdmin -from judge.admin.interface import BlogPostAdmin, FlatPageAdmin, LicenseAdmin, LogEntryAdmin, NavigationBarAdmin +from judge.admin.interface import ( + BlogPostAdmin, + FlatPageAdmin, + LicenseAdmin, + LogEntryAdmin, + NavigationBarAdmin, +) from judge.admin.organization import OrganizationAdmin, OrganizationRequestAdmin from judge.admin.problem import ProblemAdmin from judge.admin.profile import ProfileAdmin, UserAdmin @@ -14,10 +20,32 @@ from judge.admin.tag import TagAdmin, TagGroupAdmin, TagProblemAdmin from judge.admin.taxon import ProblemGroupAdmin, ProblemTypeAdmin from judge.admin.ticket import TicketAdmin -from judge.models import Badge, BlogPost, Comment, CommentLock, Contest, ContestParticipation, \ - ContestTag, Judge, Language, License, MiscConfig, NavigationBar, Organization, \ - OrganizationRequest, Problem, ProblemGroup, ProblemType, Profile, Submission, Tag, \ - TagGroup, TagProblem, Ticket +from judge.models import ( + Badge, + BlogPost, + Comment, + CommentLock, + Contest, + ContestParticipation, + ContestTag, + Judge, + Language, + License, + MiscConfig, + NavigationBar, + Organization, + OrganizationRequest, + Problem, + ProblemGroup, + ProblemType, + Profile, + Submission, + Tag, + TagGroup, + TagProblem, + Ticket, + BadgeRequest, +) admin.site.register(BlogPost, BlogPostAdmin) admin.site.register(Comment, CommentAdmin) @@ -45,5 +73,6 @@ admin.site.register(Tag, TagAdmin) admin.site.register(TagGroup, TagGroupAdmin) admin.site.register(TagProblem, TagProblemAdmin) +admin.site.register(BadgeRequest) admin.site.unregister(User) admin.site.register(User, UserAdmin) diff --git a/judge/models/__init__.py b/judge/models/__init__.py index 3644bc406..36b8f3593 100644 --- a/judge/models/__init__.py +++ b/judge/models/__init__.py @@ -1,32 +1,80 @@ 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, ContestAnnouncement, ContestMoss, ContestParticipation, ContestProblem, \ - ContestSubmission, ContestTag, Rating -from judge.models.interface import BlogPost, BlogVote, MiscConfig, NavigationBar, validate_regex -from judge.models.problem import LanguageLimit, License, Problem, ProblemClarification, ProblemGroup, \ - 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 Badge, Organization, OrganizationMonthlyUsage, OrganizationRequest, \ - Profile, WebAuthnCredential +from judge.models.contest import ( + Contest, + ContestAnnouncement, + ContestMoss, + ContestParticipation, + ContestProblem, + ContestSubmission, + ContestTag, + Rating, +) +from judge.models.interface import ( + BlogPost, + BlogVote, + MiscConfig, + NavigationBar, + validate_regex, +) +from judge.models.problem import ( + LanguageLimit, + License, + Problem, + ProblemClarification, + ProblemGroup, + 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 ( + Badge, + Organization, + OrganizationMonthlyUsage, + OrganizationRequest, + Profile, + WebAuthnCredential, + BadgeRequest, +) 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.tag import Tag, TagData, TagGroup, TagProblem from judge.models.ticket import GeneralIssue, 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"] +) revisions.register(TagProblem) -revisions.register(TagData, follow=['problem']) +revisions.register(TagData, follow=["problem"]) del revisions diff --git a/judge/models/profile.py b/judge/models/profile.py index aa736f4ed..7a3744946 100644 --- a/judge/models/profile.py +++ b/judge/models/profile.py @@ -27,7 +27,13 @@ from judge.utils.float_compare import float_compare_equal from judge.utils.two_factor import webauthn_decode -__all__ = ['Organization', 'OrganizationMonthlyUsage', 'Profile', 'OrganizationRequest', 'WebAuthnCredential'] +__all__ = [ + "Organization", + "OrganizationMonthlyUsage", + "Profile", + "OrganizationRequest", + "WebAuthnCredential", +] class EncryptedNullCharField(EncryptedCharField): @@ -38,43 +44,89 @@ 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.'), - validators=[RegexValidator(r'^[a-zA-Z]', - _('Organization slugs must begin with a letter.'))], - unique=True) - 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=False) - is_unlisted = models.BooleanField(verbose_name=_('is unlisted organization?'), - help_text=_('Organization will not be listed'), 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.')) + 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."), + validators=[ + RegexValidator( + r"^[a-zA-Z]", _("Organization slugs must begin with a letter.") + ) + ], + unique=True, + ) + 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=False, + ) + is_unlisted = models.BooleanField( + verbose_name=_("is unlisted organization?"), + help_text=_("Organization will not be listed"), + 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." + ), + ) performance_points = models.FloatField(default=0) member_count = models.IntegerField(default=0) - _pp_table = [pow(settings.VNOJ_ORG_PP_STEP, i) for i in range(settings.VNOJ_ORG_PP_ENTRIES)] + _pp_table = [ + pow(settings.VNOJ_ORG_PP_STEP, i) for i in range(settings.VNOJ_ORG_PP_ENTRIES) + ] def calculate_points(self, table=_pp_table): - data = self.members.get_queryset().order_by('-performance_points') \ - .values_list('performance_points', flat=True).filter(performance_points__gt=0) - pp = settings.VNOJ_ORG_PP_SCALE * sum(ratio * pp for ratio, pp in zip(table, data)) + data = ( + self.members.get_queryset() + .order_by("-performance_points") + .values_list("performance_points", flat=True) + .filter(performance_points__gt=0) + ) + pp = settings.VNOJ_ORG_PP_SCALE * sum( + ratio * pp for ratio, pp in zip(table, data) + ) if not float_compare_equal(self.performance_points, pp): self.performance_points = pp - self.save(update_fields=['performance_points']) + self.save(update_fields=["performance_points"]) return pp def on_user_changes(self): @@ -82,7 +134,7 @@ def on_user_changes(self): member_count = self.members.count() if self.member_count != member_count: self.member_count = member_count - self.save(update_fields=['member_count']) + self.save(update_fields=["member_count"]) @cached_property def admins_list(self): @@ -99,116 +151,217 @@ 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.slug]) + return reverse("organization_home", args=[self.slug]) def get_users_url(self): - return reverse('organization_users', args=[self.slug]) + return reverse("organization_users", args=[self.slug]) class Meta: - ordering = ['name'] + ordering = ["name"] permissions = ( - ('organization_admin', _('Administer organizations')), - ('edit_all_organization', _('Edit all organizations')), - ('change_open_organization', _('Change is_open field')), - ('spam_organization', _('Create organization without limit')), + ("organization_admin", _("Administer organizations")), + ("edit_all_organization", _("Edit all organizations")), + ("change_open_organization", _("Change is_open field")), + ("spam_organization", _("Create organization without limit")), ) - verbose_name = _('organization') - verbose_name_plural = _('organizations') + verbose_name = _("organization") + verbose_name_plural = _("organizations") class OrganizationMonthlyUsage(models.Model): - organization = models.ForeignKey(Organization, verbose_name=_('organization'), related_name='monthly_usages', - on_delete=models.CASCADE) - time = models.DateField(verbose_name=_('time')) - consumed_credit = models.FloatField(verbose_name=_('consumed credit'), default=0) + organization = models.ForeignKey( + Organization, + verbose_name=_("organization"), + related_name="monthly_usages", + on_delete=models.CASCADE, + ) + time = models.DateField(verbose_name=_("time")) + consumed_credit = models.FloatField(verbose_name=_("consumed credit"), default=0) class Meta: - verbose_name = _('organization monthly usage') - verbose_name_plural = _('organization monthly usages') - unique_together = ('organization', 'time') + verbose_name = _("organization monthly usage") + verbose_name_plural = _("organization monthly usages") + unique_together = ("organization", "time") class Badge(models.Model): - name = models.CharField(max_length=128, verbose_name=_('badge name')) - mini = models.URLField(verbose_name=_('mini badge URL'), blank=True) - full_size = models.URLField(verbose_name=_('full size badge URL'), blank=True) + name = models.CharField(max_length=128, verbose_name=_("badge name")) + mini = models.URLField(verbose_name=_("mini badge URL"), blank=True) + full_size = models.URLField(verbose_name=_("full size badge URL"), blank=True) def __str__(self): return self.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) contribution_points = models.IntegerField(default=0) vnoj_points = models.IntegerField(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='light') - last_access = models.DateTimeField(verbose_name=_('last access time'), default=now) - ip = models.GenericIPAddressField(verbose_name=_('last IP'), blank=True, null=True) - ip_auth = models.GenericIPAddressField(verbose_name=_('IP-based authentication'), - unique=True, blank=True, null=True) - badges = models.ManyToManyField(Badge, verbose_name=_('badges'), blank=True, related_name='users') - display_badge = models.ForeignKey(Badge, verbose_name=_('display badge'), null=True, on_delete=models.SET_NULL) - 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=settings.VNOJ_DISPLAY_RANKS) - 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) - ban_reason = models.TextField(null=True, blank=True, - help_text=_('Show to banned user in login page.')) - allow_tagging = models.BooleanField(verbose_name=_('Allow tagging'), - help_text=_('User will be allowed to tag problems.'), - default=True) + 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="light", + ) + last_access = models.DateTimeField(verbose_name=_("last access time"), default=now) + ip = models.GenericIPAddressField(verbose_name=_("last IP"), blank=True, null=True) + ip_auth = models.GenericIPAddressField( + verbose_name=_("IP-based authentication"), unique=True, blank=True, null=True + ) + badges = models.ManyToManyField( + Badge, verbose_name=_("badges"), blank=True, related_name="users" + ) + display_badge = models.ForeignKey( + Badge, verbose_name=_("display badge"), null=True, on_delete=models.SET_NULL + ) + 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=settings.VNOJ_DISPLAY_RANKS, + ) + 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, + ) + ban_reason = models.TextField( + null=True, blank=True, help_text=_("Show to banned user in login page.") + ) + allow_tagging = models.BooleanField( + verbose_name=_("Allow tagging"), + help_text=_("User will be allowed to tag problems."), + default=True, + ) 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): @@ -239,57 +392,78 @@ def is_banned(self): return not self.user.is_active and self.ban_reason is not None def can_be_banned_by(self, staff): - return self.user != staff and not self.user.is_superuser and staff.has_perm('judge.ban_user') + return ( + self.user != staff + and not self.user.is_superuser + and staff.has_perm("judge.ban_user") + ) @cached_property def can_tag_problems(self): if self.allow_tagging: - if self.user.has_perm('judge.add_tagproblem'): + if self.user.has_perm("judge.add_tagproblem"): return True - if self.rating is not None and self.rating >= settings.VNOJ_TAG_PROBLEM_MIN_RATING: + if ( + self.rating is not None + and self.rating >= settings.VNOJ_TAG_PROBLEM_MIN_RATING + ): return True return False @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 @cached_property def registered_contest_ids(self): - return set(self.contest_history.filter(virtual=0).values_list('contest_id', flat=True)) + return set( + self.contest_history.filter(virtual=0).values_list("contest_id", flat=True) + ) _pp_table = [pow(settings.DMOJ_PP_STEP, i) for i in range(settings.DMOJ_PP_ENTRIES)] 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) 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(x * y for x, y in zip(table, data)) + bonus_function(problems) - if not float_compare_equal(self.points, points) or \ - problems != self.problem_count or \ - not float_compare_equal(self.performance_points, pp): + if ( + not float_compare_equal(self.points, points) + or problems != self.problem_count + or not float_compare_equal(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"]) for org in self.organizations.get_queryset(): org.calculate_points() return points @@ -298,23 +472,35 @@ def calculate_points(self, table=_pp_table): def calculate_contribution_points(self): from judge.models import BlogPost, Comment, Ticket + old_pp = self.contribution_points # Because the aggregate function can return None # So we use `X or 0` to get 0 if X is None # Please note that `0 or X` will return None if X is None - total_comment_scores = Comment.objects.filter(author=self.id, hidden=False) \ - .aggregate(sum=Sum('score'))['sum'] or 0 - total_blog_scores = BlogPost.objects.filter(authors=self.id, visible=True, organization=None) \ - .aggregate(sum=Sum('score'))['sum'] or 0 - count_good_tickets = Ticket.objects.filter(user=self.id, is_contributive=True) \ - .count() + total_comment_scores = ( + Comment.objects.filter(author=self.id, hidden=False).aggregate( + sum=Sum("score") + )["sum"] + or 0 + ) + total_blog_scores = ( + BlogPost.objects.filter( + authors=self.id, visible=True, organization=None + ).aggregate(sum=Sum("score"))["sum"] + or 0 + ) + count_good_tickets = Ticket.objects.filter( + user=self.id, is_contributive=True + ).count() count_suggested_problem = self.suggested_problems.filter(is_public=True).count() - new_pp = (total_comment_scores + total_blog_scores) * settings.VNOJ_CP_COMMENT + \ - count_good_tickets * settings.VNOJ_CP_TICKET + \ - count_suggested_problem * settings.VNOJ_CP_PROBLEM + new_pp = ( + (total_comment_scores + total_blog_scores) * settings.VNOJ_CP_COMMENT + + count_good_tickets * settings.VNOJ_CP_TICKET + + count_suggested_problem * settings.VNOJ_CP_PROBLEM + ) if new_pp != old_pp: self.contribution_points = new_pp - self.save(update_fields=['contribution_points']) + self.save(update_fields=["contribution_points"]) return new_pp calculate_contribution_points.alters_data = True @@ -323,26 +509,33 @@ def update_contribution_points(self, delta): # this is just for testing the contribution # we should not use this function to update contribution points self.contribution_points += delta - self.save(update_fields=['contribution_points']) + self.save(update_fields=["contribution_points"]) return self.contribution_points update_contribution_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 @@ -355,7 +548,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 @@ -363,11 +558,16 @@ 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 @@ -375,36 +575,41 @@ def check_totp_code(self, code): def ban_user(self, reason): self.ban_reason = reason - self.display_rank = 'banned' + self.display_rank = "banned" self.is_unlisted = True - self.save(update_fields=['ban_reason', 'display_rank', 'is_unlisted']) + self.save(update_fields=["ban_reason", "display_rank", "is_unlisted"]) self.user.is_active = False - self.user.save(update_fields=['is_active']) + self.user.save(update_fields=["is_active"]) ban_user.alters_data = True def unban_user(self): self.ban_reason = None - self.display_rank = Profile._meta.get_field('display_rank').get_default() + self.display_rank = Profile._meta.get_field("display_rank").get_default() self.is_unlisted = False - self.save(update_fields=['ban_reason', 'display_rank', 'is_unlisted']) + self.save(update_fields=["ban_reason", "display_rank", "is_unlisted"]) self.user.is_active = True - self.user.save(update_fields=['is_active']) + self.user.save(update_fields=["is_active"]) unban_user.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 @@ -413,36 +618,49 @@ 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')), - ('can_upload_image', _('Can upload image directly to server via martor')), - ('high_problem_timelimit', _('Can set high problem timelimit')), - ('long_contest_duration', _('Can set long contest duration')), - ('create_mass_testcases', _('Can create unlimitted number of testcases for a problem')), - ('ban_user', _('Ban users')), + ("test_site", _("Shows in-progress development stuff")), + ("totp", _("Edit TOTP settings")), + ("can_upload_image", _("Can upload image directly to server via martor")), + ("high_problem_timelimit", _("Can set high problem timelimit")), + ("long_contest_duration", _("Can set long contest duration")), + ( + "create_mass_testcases", + _("Can create unlimitted number of testcases for a problem"), + ), + ("ban_user", _("Ban users")), ) - 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', '-contribution_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", "-contribution_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): @@ -460,25 +678,69 @@ 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')), - )) - 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")), + ), + ) + reason = models.TextField(verbose_name=_("reason")) + + class Meta: + verbose_name = _("organization join request") + verbose_name_plural = _("organization join requests") + + +class BadgeRequest(models.Model): + user = models.ForeignKey( + Profile, + verbose_name=_("user"), + related_name="badge_requests", + on_delete=models.CASCADE, + ) + badge = models.ForeignKey( + Badge, + verbose_name=_("badge"), + related_name="badge_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")), + ), + ) + desc = models.TextField(verbose_name=_("description")) + cert = models.FileField(verbose_name=_("certificate"), upload_to="certificates/") class Meta: - verbose_name = _('organization join request') - verbose_name_plural = _('organization join requests') + verbose_name = _("badge request") + verbose_name_plural = _("badge requests") From 69cb1f1f0f0214f75436994baab3e53d77037e07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Ho=C3=A0ng=20Ph=C3=BAc?= Date: Sun, 15 Sep 2024 18:52:53 +0700 Subject: [PATCH 02/13] Add badge request form --- judge/views/badge.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 judge/views/badge.py diff --git a/judge/views/badge.py b/judge/views/badge.py new file mode 100644 index 000000000..a17ceca64 --- /dev/null +++ b/judge/views/badge.py @@ -0,0 +1,28 @@ +from django import forms + +from judge.models import BadgeRequest, Badge + + +class BadgeRequestForm(forms.ModelForm): + new_badge_name = forms.CharField(required=False, label="New Badge Name") + + class Meta: + model = BadgeRequest + fields = ["badge", "desc", "cert"] + + def __init__(self, *args, **kwargs): + super(BadgeRequestForm, self).__init__(*args, **kwargs) + self.fields["badge"].queryset = Badge.objects.all() + self.fields["badge"].required = False + + def clean(self): + cleaned_data = super().clean() + badge = cleaned_data.get("badge") + new_badge_name = cleaned_data.get("new_badge_name") + + if not badge and not new_badge_name: + raise forms.ValidationError( + "You must select an existing badge or enter a new badge name." + ) + + return cleaned_data From af73957993b0bd91e90a3777412cbac6606be8a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Ho=C3=A0ng=20Ph=C3=BAc?= Date: Sun, 15 Sep 2024 20:20:00 +0700 Subject: [PATCH 03/13] Add necessary views --- judge/views/badge.py | 159 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) diff --git a/judge/views/badge.py b/judge/views/badge.py index a17ceca64..656753e4f 100644 --- a/judge/views/badge.py +++ b/judge/views/badge.py @@ -1,6 +1,18 @@ from django import forms +from django.contrib.auth.mixins import LoginRequiredMixin +from django.http import HttpResponseRedirect +from django.urls import reverse +from django.views.generic.detail import ( + SingleObjectMixin, + SingleObjectTemplateResponseMixin, +) +from django.utils.translation import gettext as _, gettext_lazy, ngettext +from django.core.exceptions import PermissionDenied +from django.utils.html import format_html +from django.contrib import messages from judge.models import BadgeRequest, Badge +from judge.utils.views import TitleMixin, generic_message class BadgeRequestForm(forms.ModelForm): @@ -26,3 +38,150 @@ def clean(self): ) return cleaned_data + + +class RequestAddBadge(LoginRequiredMixin, SingleObjectMixin, forms.FormView): + template_name = "badge/requests/request.html" + form_class = BadgeRequestForm + + def dispatch(self, request, *args, **kwargs): + if BadgeRequest.objects.filter(user=self.request.user, state="P").exists(): + return generic_message( + self.request, + _("Can't request a new badge"), + _("You already have a pending badge request."), + ) + return super(RequestAddBadge, self).dispatch(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + context = super(RequestAddBadge, self).get_context_data(**kwargs) + context["title"] = _("Request a new badge") + return context + + def form_valid(self, form): + badge_request = BadgeRequest() + badge_request.user = self.request.user + badge_request.badge = form.cleaned_data["badge"] + badge_request.desc = form.cleaned_data["desc"] + badge_request.cert = form.cleaned_data["cert"] + badge_request.state = "P" + badge_request.save() + return HttpResponseRedirect( + reverse("badge_request_detail", args=(badge_request.id,)) + ) + + +class BadgeRequestDetail(LoginRequiredMixin, TitleMixin, forms.DetailView): + model = BadgeRequest + template_name = "badge/requests/detail.html" + title = gettext_lazy("Badge request detail") + pk_url_kwarg = "rpk" + + def get_object(self, queryset=None): + object = super(BadgeRequestDetail, self).get_object(queryset) + profile = self.request.profile + if object.user_id != profile.id and not object.Badge.is_admin(profile): + raise PermissionDenied() + return object + + +BadgeRequestFormSet = forms.modelformset_factory( + BadgeRequest, extra=0, fields=("state",), can_delete=True +) + + +class BadgeRequestBaseView( + LoginRequiredMixin, SingleObjectTemplateResponseMixin, SingleObjectMixin, forms.View +): + model = Badge + tab = None + + def get_object(self, queryset=None): + badge = super(BadgeRequestBaseView, self).get_object(queryset) + if not badge.is_admin(self.request.profile): + raise PermissionDenied() + return badge + + def get_requests(self): + queryset = ( + self.object.requests.select_related("user__user") + .defer( + "user__about", + "user__notes", + "user__user_script", + ) + .order_by("-id") + ) + return queryset + + def get_context_data(self, **kwargs): + context = super(BadgeRequestBaseView, self).get_context_data(**kwargs) + context["title"] = _("Managing join requests for %s") % self.object.name + context["content_title"] = format_html( + _("Managing join requests for %s") % ' {0}', + self.object.name, + self.object.get_absolute_url(), + ) + context["tab"] = self.tab + return context + + +class BadgeRequestView(BadgeRequestBaseView): + template_name = "badge/requests/pending.html" + tab = "pending" + + def get_context_data(self, **kwargs): + context = super(BadgeRequestView, self).get_context_data(**kwargs) + context["formset"] = self.formset + return context + + def get(self, request, *args, **kwargs): + self.object = self.get_object() + self.formset = BadgeRequestFormSet(queryset=self.get_requests()) + context = self.get_context_data(object=self.object) + return self.render_to_response(context) + + def get_requests(self): + return super().get_requests().filter(state="P") + + def post(self, request, *args, **kwargs): + self.object = badge = self.get_object() + self.formset = formset = BadgeRequestFormSet( + request.POST, request.FILES, queryset=self.get_requests() + ) + if formset.is_valid(): + approved, rejected = 0, 0 + for obj in formset.save(): + if obj.state == "A": + obj.user.badges.add(obj.badge) + approved += 1 + elif obj.state == "R": + rejected += 1 + messages.success( + request, + ngettext("Approved %d request.", "Approved %d requests.", approved) + % approved + + "\n" + + ngettext("Rejected %d request.", "Rejected %d requests.", rejected) + % rejected, + ) + return HttpResponseRedirect(request.get_full_path()) + return self.render_to_response(self.get_context_data(object=badge)) + + put = post + + +class BadgeRequestLog(BadgeRequestBaseView): + states = ("A", "R") + tab = "log" + template_name = "badge/requests/log.html" + + def get(self, request, *args, **kwargs): + self.object = self.get_object() + context = self.get_context_data(object=self.object) + return self.render_to_response(context) + + def get_context_data(self, **kwargs): + context = super(BadgeRequestLog, self).get_context_data(**kwargs) + context["requests"] = self.get_requests().filter(state__in=self.states) + return context From 88067379055755d388876a4d2d93eec697601ab6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Ho=C3=A0ng=20Ph=C3=BAc?= Date: Sun, 15 Sep 2024 21:14:43 +0700 Subject: [PATCH 04/13] Add necessary templates for requesting badges --- dmoj/urls.py | 6 +++- judge/views/badge.py | 29 ++++++------------ templates/badge/detail.html | 34 ++++++++++++++++++++ templates/badge/log.html | 30 ++++++++++++++++++ templates/badge/pending.html | 58 +++++++++++++++++++++++++++++++++++ templates/badge/request.html | 22 +++++++++++++ templates/badge/tabs.html | 16 ++++++++++ templates/user/user-tabs.html | 1 + 8 files changed, 175 insertions(+), 21 deletions(-) create mode 100644 templates/badge/detail.html create mode 100644 templates/badge/log.html create mode 100644 templates/badge/pending.html create mode 100644 templates/badge/request.html create mode 100644 templates/badge/tabs.html diff --git a/dmoj/urls.py b/dmoj/urls.py index c7e6d1c1b..efe3ebab5 100644 --- a/dmoj/urls.py +++ b/dmoj/urls.py @@ -17,7 +17,7 @@ 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, tag, tasks, ticket, \ - two_factor, user, widgets + two_factor, user, widgets, badge from judge.views.magazine import MagazinePage from judge.views.problem_data import ProblemDataView, ProblemSubmissionDiff, \ problem_data_file, problem_init_view @@ -192,6 +192,10 @@ def paged_list_view(view, name): ])), path('user', user.UserAboutPage.as_view(), name='user_page'), + path('user', include([ + path('', user.UserAboutPage.as_view(), name='user_page'), + path('/request', badge.RequestAddBadge.as_view(), name='request_badge'), + ])), 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'), diff --git a/judge/views/badge.py b/judge/views/badge.py index 656753e4f..427c5386d 100644 --- a/judge/views/badge.py +++ b/judge/views/badge.py @@ -2,17 +2,14 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.http import HttpResponseRedirect from django.urls import reverse -from django.views.generic.detail import ( - SingleObjectMixin, - SingleObjectTemplateResponseMixin, -) from django.utils.translation import gettext as _, gettext_lazy, ngettext from django.core.exceptions import PermissionDenied from django.utils.html import format_html from django.contrib import messages +from django.views.generic import DetailView, FormView, View from judge.models import BadgeRequest, Badge -from judge.utils.views import TitleMixin, generic_message +from judge.utils.views import TitleMixin class BadgeRequestForm(forms.ModelForm): @@ -40,17 +37,11 @@ def clean(self): return cleaned_data -class RequestAddBadge(LoginRequiredMixin, SingleObjectMixin, forms.FormView): - template_name = "badge/requests/request.html" +class RequestAddBadge(LoginRequiredMixin, FormView): + template_name = "badge/request.html" form_class = BadgeRequestForm def dispatch(self, request, *args, **kwargs): - if BadgeRequest.objects.filter(user=self.request.user, state="P").exists(): - return generic_message( - self.request, - _("Can't request a new badge"), - _("You already have a pending badge request."), - ) return super(RequestAddBadge, self).dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): @@ -71,9 +62,9 @@ def form_valid(self, form): ) -class BadgeRequestDetail(LoginRequiredMixin, TitleMixin, forms.DetailView): +class BadgeRequestDetail(LoginRequiredMixin, TitleMixin, DetailView): model = BadgeRequest - template_name = "badge/requests/detail.html" + template_name = "badge/detail.html" title = gettext_lazy("Badge request detail") pk_url_kwarg = "rpk" @@ -90,9 +81,7 @@ def get_object(self, queryset=None): ) -class BadgeRequestBaseView( - LoginRequiredMixin, SingleObjectTemplateResponseMixin, SingleObjectMixin, forms.View -): +class BadgeRequestBaseView(LoginRequiredMixin, View): model = Badge tab = None @@ -127,7 +116,7 @@ def get_context_data(self, **kwargs): class BadgeRequestView(BadgeRequestBaseView): - template_name = "badge/requests/pending.html" + template_name = "badge/pending.html" tab = "pending" def get_context_data(self, **kwargs): @@ -174,7 +163,7 @@ def post(self, request, *args, **kwargs): class BadgeRequestLog(BadgeRequestBaseView): states = ("A", "R") tab = "log" - template_name = "badge/requests/log.html" + template_name = "badge/log.html" def get(self, request, *args, **kwargs): self.object = self.get_object() diff --git a/templates/badge/detail.html b/templates/badge/detail.html new file mode 100644 index 000000000..d226528d7 --- /dev/null +++ b/templates/badge/detail.html @@ -0,0 +1,34 @@ +{% extends "base.html" %} {% block media %} + +{% endblock %} {% block body %} + + + + + + + + + + + + + + + + + + + + + +
{{ _('User:') }}{{ link_user(object.user) }}
{{ _('Organization:') }} + {% with org=object.organization %} + {{ org.name }} + {% endwith %} +
{{ _('Time:') }}{{ object.time|date(_("N j, Y, g:i a")) }}
{{ _('State:') }}{{ object.get_state_display() }}
{{ _('Reason:') }}{{ object.reason|linebreaks }}
+{% endblock %} diff --git a/templates/badge/log.html b/templates/badge/log.html new file mode 100644 index 000000000..1f38b839d --- /dev/null +++ b/templates/badge/log.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} + +{% block body %} + {% include "organization/requests/tabs.html" %} + + {% if requests %} + + + + + + + + {% for r in requests %} + + + + + + + {% endfor %} +
{{ _('User') }}{{ _('Time') }}{{ _('State') }}{{ _('Reason') }}
{{ link_user(r.user) }} + + {{- r.time|date(_("N j, Y, H:i")) -}} + + {{ r.get_state_display() }}{{ r.reason|truncatechars(50) }}
+ {% else %} +

{{ _('There are no requests to approve.') }}

+ {% endif %} +{% endblock %} diff --git a/templates/badge/pending.html b/templates/badge/pending.html new file mode 100644 index 000000000..518cd6665 --- /dev/null +++ b/templates/badge/pending.html @@ -0,0 +1,58 @@ +{% extends "base.html" %} +{% block body %} + + {% include "messages.html" %} + {% include "organization/requests/tabs.html" %} + + {% if formset.forms %} +
+ {% csrf_token %} + {{ formset.management_form }} + + + + + + + {% if formset.can_delete %} + + {% endif %} + + {% for form in formset %} + + + + + + {% if formset.can_delete %} + + {% endif %} + + {% endfor %} +
{{ _('User') }}{{ _('Time') }}{{ _('State') }}{{ _('Reason') }}{{ _('Delete?') }}
{{ form.id }}{{ link_user(form.instance.user) }} + {{ form.instance.time|date(_("N j, Y, H:i")) }} + {{ form.state }}{{ form.instance.reason }}{{ form.DELETE }}
+ +
+ {% else %} +

{{ _('There are no requests to approve.') }}

+ {% endif %} +{% endblock %} diff --git a/templates/badge/request.html b/templates/badge/request.html new file mode 100644 index 000000000..2dd561275 --- /dev/null +++ b/templates/badge/request.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} + +{% block js_media %} + +{% endblock %} + +{% block content %} +

{{ title }}

+
+ {% csrf_token %} +

Hello

+ +
+{% endblock %} \ No newline at end of file diff --git a/templates/badge/tabs.html b/templates/badge/tabs.html new file mode 100644 index 000000000..bf3769863 --- /dev/null +++ b/templates/badge/tabs.html @@ -0,0 +1,16 @@ + diff --git a/templates/user/user-tabs.html b/templates/user/user-tabs.html index b7fbf2f95..a3a65c93c 100644 --- a/templates/user/user-tabs.html +++ b/templates/user/user-tabs.html @@ -4,6 +4,7 @@ {% if user.user == request.user and not request.official_contest_mode %} {{ make_tab('create', 'fa-plus', url('blog_post_new'), _('Create new blog post')) }} {% endif %} + {{ make_tab('create', 'fa-plus', url('request_badge'), _('Request badges')) }} {{ make_tab('about', 'fa-info-circle', url('user_page', user.user.username), _('About')) }} {{ make_tab('problems', 'fa-puzzle-piece', url('user_problems', user.user.username), _('Statistics')) }} {% if not request.official_contest_mode %} From 18b07f60059139dda5de5e42ee403cc968ce5133 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Ho=C3=A0ng=20Ph=C3=BAc?= Date: Tue, 17 Sep 2024 11:25:20 +0700 Subject: [PATCH 05/13] Add necessary urls for requesting badges --- dmoj/urls.py | 2 ++ judge/models/profile.py | 6 +++++- judge/views/badge.py | 21 ++++++++++++++++++- templates/badge/detail.html | 37 ++++----------------------------- templates/badge/request.html | 40 ++++++++++++++++++++++++++++++++---- 5 files changed, 67 insertions(+), 39 deletions(-) diff --git a/dmoj/urls.py b/dmoj/urls.py index efe3ebab5..73f350468 100644 --- a/dmoj/urls.py +++ b/dmoj/urls.py @@ -195,6 +195,8 @@ def paged_list_view(view, name): path('user', include([ path('', user.UserAboutPage.as_view(), name='user_page'), path('/request', badge.RequestAddBadge.as_view(), name='request_badge'), + path('/request/', badge.BadgeRequestDetail.as_view(), + name='request_badge_detail'), ])), path('edit/profile/', user.edit_profile, name='user_edit_profile'), path('data/prepare/', user.UserPrepareData.as_view(), name='user_prepare_data'), diff --git a/judge/models/profile.py b/judge/models/profile.py index 7a3744946..e6922bb4c 100644 --- a/judge/models/profile.py +++ b/judge/models/profile.py @@ -4,6 +4,7 @@ import secrets import struct +from django.forms import ValidationError import pyotp import webauthn from django.conf import settings @@ -739,7 +740,10 @@ class BadgeRequest(models.Model): ), ) desc = models.TextField(verbose_name=_("description")) - cert = models.FileField(verbose_name=_("certificate"), upload_to="certificates/") + cert = models.FileField( + verbose_name=_("certificate"), + upload_to="certificates/", + ) class Meta: verbose_name = _("badge request") diff --git a/judge/views/badge.py b/judge/views/badge.py index 427c5386d..4edacf0d7 100644 --- a/judge/views/badge.py +++ b/judge/views/badge.py @@ -12,6 +12,11 @@ from judge.utils.views import TitleMixin +def validate_pdf(value): + if not value.name.endswith(".pdf"): + raise forms.ValidationError(_("Only PDF files are allowed.")) + + class BadgeRequestForm(forms.ModelForm): new_badge_name = forms.CharField(required=False, label="New Badge Name") @@ -28,12 +33,21 @@ def clean(self): cleaned_data = super().clean() badge = cleaned_data.get("badge") new_badge_name = cleaned_data.get("new_badge_name") + cert = cleaned_data.get("cert") if not badge and not new_badge_name: raise forms.ValidationError( "You must select an existing badge or enter a new badge name." ) + if badge: + cleaned_data["badge"] = badge + + if cert: + validate_pdf(cert) + else: + raise forms.ValidationError("The certificate field is required.") + return cleaned_data @@ -57,10 +71,15 @@ def form_valid(self, form): badge_request.cert = form.cleaned_data["cert"] badge_request.state = "P" badge_request.save() + print("Form is valid. Redirecting...") return HttpResponseRedirect( - reverse("badge_request_detail", args=(badge_request.id,)) + reverse("request_badge_detail", args=(badge_request.id,)) ) + def form_invalid(self, form): + print("Form is invalid. Errors:", form.errors) + return super().form_invalid(form) + class BadgeRequestDetail(LoginRequiredMixin, TitleMixin, DetailView): model = BadgeRequest diff --git a/templates/badge/detail.html b/templates/badge/detail.html index d226528d7..de71643c4 100644 --- a/templates/badge/detail.html +++ b/templates/badge/detail.html @@ -1,34 +1,5 @@ -{% extends "base.html" %} {% block media %} - -{% endblock %} {% block body %} - - - - - - - - - - - - - - - - - - - - - -
{{ _('User:') }}{{ link_user(object.user) }}
{{ _('Organization:') }} - {% with org=object.organization %} - {{ org.name }} - {% endwith %} -
{{ _('Time:') }}{{ object.time|date(_("N j, Y, g:i a")) }}
{{ _('State:') }}{{ object.get_state_display() }}
{{ _('Reason:') }}{{ object.reason|linebreaks }}
+{% extends "base.html" %} + +{% block body %} +

{{ _('Badge details') }}

{% endblock %} diff --git a/templates/badge/request.html b/templates/badge/request.html index 2dd561275..4b1b0fffb 100644 --- a/templates/badge/request.html +++ b/templates/badge/request.html @@ -8,15 +8,47 @@ $(this).closest('form').submit(); } }); + + // Function to check the selected value of the badge dropdown + function checkBadgeValue() { + var badgeValue = $('#id_badge').val(); + if (badgeValue === "") { + $('#new-badge-name').show(); + } else { + $('#new-badge-name').hide(); + } + } + + // Initial check on page load + checkBadgeValue(); + + // Check on change event + $('#id_badge').change(function () { + checkBadgeValue(); + }); }); {% endblock %} -{% block content %} -

{{ title }}

-
+{% block body %} + {% csrf_token %} -

Hello

+

+

{{ form.badge }}

+
+

+

{{ form.new_badge_name }}

+
+ {% if form.errors %} +

{{form.errors.__all__[0]}}

+ {% endif %} +

+

{{ form.desc }}

+

+

{{ form.cert }}

+ {% if form.errors %} +

{{form.errors.cert[0]}}

+ {% endif %}
{% endblock %} \ No newline at end of file From 4e6660a28987b15e4ea230f2c9bffd2fa6f501ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Ho=C3=A0ng=20Ph=C3=BAc?= Date: Tue, 17 Sep 2024 14:18:41 +0700 Subject: [PATCH 06/13] Create badge request admin --- dmoj/urls.py | 1 + judge/admin/__init__.py | 3 +- judge/admin/badge.py | 11 +++++++ judge/migrations/0207_auto_20240917_0704.py | 35 +++++++++++++++++++++ judge/models/profile.py | 6 ++++ judge/views/badge.py | 24 +++++++++++++- 6 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 judge/admin/badge.py create mode 100644 judge/migrations/0207_auto_20240917_0704.py diff --git a/dmoj/urls.py b/dmoj/urls.py index 73f350468..3672b3f59 100644 --- a/dmoj/urls.py +++ b/dmoj/urls.py @@ -107,6 +107,7 @@ def paged_list_view(view, name): path('i18n/', include('django.conf.urls.i18n')), path('accounts/', include(register_patterns)), path('', include('social_django.urls')), + path('certificates/', badge.open_certificate, name='open_certificate'), path('problems', include([ path('/', problem.ProblemList.as_view(), name='problem_list'), diff --git a/judge/admin/__init__.py b/judge/admin/__init__.py index 6397972bd..0bee6a764 100644 --- a/judge/admin/__init__.py +++ b/judge/admin/__init__.py @@ -46,6 +46,7 @@ Ticket, BadgeRequest, ) +from judge.admin.badge import BadgeRequestAdmin admin.site.register(BlogPost, BlogPostAdmin) admin.site.register(Comment, CommentAdmin) @@ -73,6 +74,6 @@ admin.site.register(Tag, TagAdmin) admin.site.register(TagGroup, TagGroupAdmin) admin.site.register(TagProblem, TagProblemAdmin) -admin.site.register(BadgeRequest) +admin.site.register(BadgeRequest, BadgeRequestAdmin) admin.site.unregister(User) admin.site.register(User, UserAdmin) diff --git a/judge/admin/badge.py b/judge/admin/badge.py new file mode 100644 index 000000000..e691921dc --- /dev/null +++ b/judge/admin/badge.py @@ -0,0 +1,11 @@ +from django.contrib import admin +from django.utils.translation import gettext_lazy as _ + + +class BadgeRequestAdmin(admin.ModelAdmin): + list_display = ("username", "badge", "state", "time") + readonly_fields = ("user", "cert", "desc") + + @admin.display(description=_("username"), ordering="user__user__username") + def username(self, obj): + return obj.user.user.username diff --git a/judge/migrations/0207_auto_20240917_0704.py b/judge/migrations/0207_auto_20240917_0704.py new file mode 100644 index 000000000..63a84c521 --- /dev/null +++ b/judge/migrations/0207_auto_20240917_0704.py @@ -0,0 +1,35 @@ +# Generated by Django 3.2.25 on 2024-09-17 07:04 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('judge', '0206_monthly_credit'), + ] + + operations = [ + 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/Ciudad_Juarez', 'Ciudad_Juarez'), ('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='Asia/Ho_Chi_Minh', max_length=50, verbose_name='time zone'), + ), + migrations.CreateModel( + name='BadgeRequest', + 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')), + ('desc', models.TextField(verbose_name='description')), + ('cert', models.FileField(upload_to='certificates/', verbose_name='certificate')), + ('badge', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='badge_requests', to='judge.badge', verbose_name='badge')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='badge_requests', to='judge.profile', verbose_name='user')), + ], + options={ + 'verbose_name': 'badge request', + 'verbose_name_plural': 'badge requests', + }, + ), + ] diff --git a/judge/models/profile.py b/judge/models/profile.py index e6922bb4c..1e4c60dcb 100644 --- a/judge/models/profile.py +++ b/judge/models/profile.py @@ -716,6 +716,11 @@ class Meta: verbose_name_plural = _("organization join requests") +def validate_pdf(file): + if not file.name.endswith(".pdf"): + raise ValidationError("Only PDF files are allowed.") + + class BadgeRequest(models.Model): user = models.ForeignKey( Profile, @@ -743,6 +748,7 @@ class BadgeRequest(models.Model): cert = models.FileField( verbose_name=_("certificate"), upload_to="certificates/", + validators=[validate_pdf], ) class Meta: diff --git a/judge/views/badge.py b/judge/views/badge.py index 4edacf0d7..66ca9e280 100644 --- a/judge/views/badge.py +++ b/judge/views/badge.py @@ -1,6 +1,13 @@ +import os from django import forms +from django.conf import settings from django.contrib.auth.mixins import LoginRequiredMixin -from django.http import HttpResponseRedirect +from django.http import ( + FileResponse, + Http404, + HttpResponseForbidden, + HttpResponseRedirect, +) from django.urls import reverse from django.utils.translation import gettext as _, gettext_lazy, ngettext from django.core.exceptions import PermissionDenied @@ -193,3 +200,18 @@ def get_context_data(self, **kwargs): context = super(BadgeRequestLog, self).get_context_data(**kwargs) context["requests"] = self.get_requests().filter(state__in=self.states) return context + + +def open_certificate(request, filename): + # Check if the user is authenticated and an admin + if not request.user.is_authenticated or not request.user.is_staff: + return HttpResponseForbidden("You do not have permission to view this file.") + + # Path to the PDF file + file_path = os.path.join(settings.MEDIA_ROOT, "certificates", filename) + + # Check if the file exists + if os.path.exists(file_path): + return FileResponse(open(file_path, "rb"), content_type="application/pdf") + else: + raise Http404("File does not exist") From ab19d0bf1163f649e67c8c81262eff57ea756fab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Ho=C3=A0ng=20Ph=C3=BAc?= Date: Tue, 17 Sep 2024 14:40:07 +0700 Subject: [PATCH 07/13] Add new badge name into model --- judge/admin/badge.py | 2 +- judge/migrations/0208_auto_20240917_0738.py | 24 +++++++++ judge/models/profile.py | 5 ++ judge/views/badge.py | 4 +- templates/badge/log.html | 30 ----------- templates/badge/pending.html | 58 --------------------- templates/badge/request.html | 6 +-- 7 files changed, 33 insertions(+), 96 deletions(-) create mode 100644 judge/migrations/0208_auto_20240917_0738.py delete mode 100644 templates/badge/log.html delete mode 100644 templates/badge/pending.html diff --git a/judge/admin/badge.py b/judge/admin/badge.py index e691921dc..01255fad6 100644 --- a/judge/admin/badge.py +++ b/judge/admin/badge.py @@ -3,7 +3,7 @@ class BadgeRequestAdmin(admin.ModelAdmin): - list_display = ("username", "badge", "state", "time") + list_display = ("username", "badge", "new_badge_name", "state", "time") readonly_fields = ("user", "cert", "desc") @admin.display(description=_("username"), ordering="user__user__username") diff --git a/judge/migrations/0208_auto_20240917_0738.py b/judge/migrations/0208_auto_20240917_0738.py new file mode 100644 index 000000000..9bc39c106 --- /dev/null +++ b/judge/migrations/0208_auto_20240917_0738.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.25 on 2024-09-17 07:38 + +from django.db import migrations, models +import judge.models.profile + + +class Migration(migrations.Migration): + + dependencies = [ + ('judge', '0207_auto_20240917_0704'), + ] + + operations = [ + migrations.AddField( + model_name='badgerequest', + name='new_badge_name', + field=models.CharField(blank=True, max_length=128, verbose_name='new badge name'), + ), + migrations.AlterField( + model_name='badgerequest', + name='cert', + field=models.FileField(upload_to='certificates/', validators=[judge.models.profile.validate_pdf], verbose_name='certificate'), + ), + ] diff --git a/judge/models/profile.py b/judge/models/profile.py index 1e4c60dcb..c4d825280 100644 --- a/judge/models/profile.py +++ b/judge/models/profile.py @@ -734,6 +734,11 @@ class BadgeRequest(models.Model): related_name="badge_requests", on_delete=models.CASCADE, ) + new_badge_name = models.CharField( + max_length=128, + verbose_name=_("new badge name"), + blank=True, + ) time = models.DateTimeField(verbose_name=_("request time"), auto_now_add=True) state = models.CharField( max_length=1, diff --git a/judge/views/badge.py b/judge/views/badge.py index 66ca9e280..09be8b0ab 100644 --- a/judge/views/badge.py +++ b/judge/views/badge.py @@ -25,11 +25,9 @@ def validate_pdf(value): class BadgeRequestForm(forms.ModelForm): - new_badge_name = forms.CharField(required=False, label="New Badge Name") - class Meta: model = BadgeRequest - fields = ["badge", "desc", "cert"] + fields = ["badge", "desc", "cert", "new_badge_name"] def __init__(self, *args, **kwargs): super(BadgeRequestForm, self).__init__(*args, **kwargs) diff --git a/templates/badge/log.html b/templates/badge/log.html deleted file mode 100644 index 1f38b839d..000000000 --- a/templates/badge/log.html +++ /dev/null @@ -1,30 +0,0 @@ -{% extends "base.html" %} - -{% block body %} - {% include "organization/requests/tabs.html" %} - - {% if requests %} - - - - - - - - {% for r in requests %} - - - - - - - {% endfor %} -
{{ _('User') }}{{ _('Time') }}{{ _('State') }}{{ _('Reason') }}
{{ link_user(r.user) }} - - {{- r.time|date(_("N j, Y, H:i")) -}} - - {{ r.get_state_display() }}{{ r.reason|truncatechars(50) }}
- {% else %} -

{{ _('There are no requests to approve.') }}

- {% endif %} -{% endblock %} diff --git a/templates/badge/pending.html b/templates/badge/pending.html deleted file mode 100644 index 518cd6665..000000000 --- a/templates/badge/pending.html +++ /dev/null @@ -1,58 +0,0 @@ -{% extends "base.html" %} -{% block body %} - - {% include "messages.html" %} - {% include "organization/requests/tabs.html" %} - - {% if formset.forms %} -
- {% csrf_token %} - {{ formset.management_form }} - - - - - - - {% if formset.can_delete %} - - {% endif %} - - {% for form in formset %} - - - - - - {% if formset.can_delete %} - - {% endif %} - - {% endfor %} -
{{ _('User') }}{{ _('Time') }}{{ _('State') }}{{ _('Reason') }}{{ _('Delete?') }}
{{ form.id }}{{ link_user(form.instance.user) }} - {{ form.instance.time|date(_("N j, Y, H:i")) }} - {{ form.state }}{{ form.instance.reason }}{{ form.DELETE }}
- -
- {% else %} -

{{ _('There are no requests to approve.') }}

- {% endif %} -{% endblock %} diff --git a/templates/badge/request.html b/templates/badge/request.html index 4b1b0fffb..dd2edc89a 100644 --- a/templates/badge/request.html +++ b/templates/badge/request.html @@ -39,15 +39,13 @@

{{ form.new_badge_name }}

- {% if form.errors %} -

{{form.errors.__all__[0]}}

- {% endif %}

{{ form.desc }}

{{ form.cert }}

{% if form.errors %} -

{{form.errors.cert[0]}}

+

{{ _('Please correct the errors below.') }}

+ {{ form.errors}} {% endif %} From a1a4e424f3929e78bc899a4dd2a775b494fa54d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Ho=C3=A0ng=20Ph=C3=BAc?= Date: Tue, 17 Sep 2024 21:38:42 +0700 Subject: [PATCH 08/13] Handle validation error in requesting badge --- .../0209_alter_badgerequest_cert.py | 19 +++++++++++++++++++ .../0210_alter_badgerequest_cert.py | 19 +++++++++++++++++++ .../0211_alter_badgerequest_badge.py | 19 +++++++++++++++++++ judge/models/profile.py | 18 +++++++++++++++--- judge/views/badge.py | 18 +++++------------- templates/badge/detail.html | 6 ++---- templates/badge/request.html | 10 ++++++---- templates/badge/tabs.html | 8 ++++---- 8 files changed, 89 insertions(+), 28 deletions(-) create mode 100644 judge/migrations/0209_alter_badgerequest_cert.py create mode 100644 judge/migrations/0210_alter_badgerequest_cert.py create mode 100644 judge/migrations/0211_alter_badgerequest_badge.py diff --git a/judge/migrations/0209_alter_badgerequest_cert.py b/judge/migrations/0209_alter_badgerequest_cert.py new file mode 100644 index 000000000..2809a03cf --- /dev/null +++ b/judge/migrations/0209_alter_badgerequest_cert.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.25 on 2024-09-17 14:11 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('judge', '0208_auto_20240917_0738'), + ] + + operations = [ + migrations.AlterField( + model_name='badgerequest', + name='cert', + field=models.FileField(upload_to='', validators=[django.core.validators.FileExtensionValidator(allowed_extensions={'pdf'})]), + ), + ] diff --git a/judge/migrations/0210_alter_badgerequest_cert.py b/judge/migrations/0210_alter_badgerequest_cert.py new file mode 100644 index 000000000..4c178c4a0 --- /dev/null +++ b/judge/migrations/0210_alter_badgerequest_cert.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.25 on 2024-09-17 14:52 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('judge', '0209_alter_badgerequest_cert'), + ] + + operations = [ + migrations.AlterField( + model_name='badgerequest', + name='cert', + field=models.FileField(upload_to='certificates', validators=[django.core.validators.FileExtensionValidator(allowed_extensions={'pdf'})], verbose_name='Certificate'), + ), + ] diff --git a/judge/migrations/0211_alter_badgerequest_badge.py b/judge/migrations/0211_alter_badgerequest_badge.py new file mode 100644 index 000000000..0d3d33590 --- /dev/null +++ b/judge/migrations/0211_alter_badgerequest_badge.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.25 on 2024-09-17 15:00 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('judge', '0210_alter_badgerequest_cert'), + ] + + operations = [ + migrations.AlterField( + model_name='badgerequest', + name='badge', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='badge_requests', to='judge.badge', verbose_name='badge'), + ), + ] diff --git a/judge/models/profile.py b/judge/models/profile.py index c4d825280..0a3ecc69a 100644 --- a/judge/models/profile.py +++ b/judge/models/profile.py @@ -21,6 +21,7 @@ from fernet_fields import EncryptedCharField from pyotp.utils import strings_equal from sortedm2m.fields import SortedManyToManyField +from django.core.validators import FileExtensionValidator from judge.models.choices import ACE_THEMES, MATH_ENGINES_CHOICES, SITE_THEMES, TIMEZONE from judge.models.runtime import Language @@ -733,6 +734,7 @@ class BadgeRequest(models.Model): verbose_name=_("badge"), related_name="badge_requests", on_delete=models.CASCADE, + null=True, ) new_badge_name = models.CharField( max_length=128, @@ -751,11 +753,21 @@ class BadgeRequest(models.Model): ) desc = models.TextField(verbose_name=_("description")) cert = models.FileField( - verbose_name=_("certificate"), - upload_to="certificates/", - validators=[validate_pdf], + verbose_name=_("Certificate"), + upload_to="certificates", + validators=[ + FileExtensionValidator(allowed_extensions=settings.PDF_STATEMENT_SAFE_EXTS) + ], ) class Meta: verbose_name = _("badge request") verbose_name_plural = _("badge requests") + + def __str__(self): + badge_name = ( + self.badge.name + if self.badge and self.badge.name + else self.new_badge_name if self.new_badge_name else "No Badge" + ) + return f"{self.user.user.username} - {badge_name} - {self.get_state_display()}" diff --git a/judge/views/badge.py b/judge/views/badge.py index 09be8b0ab..c943704b7 100644 --- a/judge/views/badge.py +++ b/judge/views/badge.py @@ -15,7 +15,7 @@ from django.contrib import messages from django.views.generic import DetailView, FormView, View -from judge.models import BadgeRequest, Badge +from judge.models import BadgeRequest, Badge, Profile from judge.utils.views import TitleMixin @@ -33,26 +33,19 @@ def __init__(self, *args, **kwargs): super(BadgeRequestForm, self).__init__(*args, **kwargs) self.fields["badge"].queryset = Badge.objects.all() self.fields["badge"].required = False + self.fields["cert"].widget.attrs.update({"accept": "application/pdf"}) + self.fields["cert"].validators.append(validate_pdf) def clean(self): cleaned_data = super().clean() badge = cleaned_data.get("badge") new_badge_name = cleaned_data.get("new_badge_name") - cert = cleaned_data.get("cert") if not badge and not new_badge_name: raise forms.ValidationError( "You must select an existing badge or enter a new badge name." ) - if badge: - cleaned_data["badge"] = badge - - if cert: - validate_pdf(cert) - else: - raise forms.ValidationError("The certificate field is required.") - return cleaned_data @@ -70,19 +63,18 @@ def get_context_data(self, **kwargs): def form_valid(self, form): badge_request = BadgeRequest() - badge_request.user = self.request.user + badge_request.user = Profile.objects.get(user=self.request.user) badge_request.badge = form.cleaned_data["badge"] badge_request.desc = form.cleaned_data["desc"] badge_request.cert = form.cleaned_data["cert"] + badge_request.new_badge_name = form.cleaned_data["new_badge_name"] badge_request.state = "P" badge_request.save() - print("Form is valid. Redirecting...") return HttpResponseRedirect( reverse("request_badge_detail", args=(badge_request.id,)) ) def form_invalid(self, form): - print("Form is invalid. Errors:", form.errors) return super().form_invalid(form) diff --git a/templates/badge/detail.html b/templates/badge/detail.html index de71643c4..874d81a19 100644 --- a/templates/badge/detail.html +++ b/templates/badge/detail.html @@ -1,5 +1,3 @@ -{% extends "base.html" %} - -{% block body %} -

{{ _('Badge details') }}

+{% extends "base.html" %} {% block body %} +

Your request has been sent to the administrator.

{% endblock %} diff --git a/templates/badge/request.html b/templates/badge/request.html index dd2edc89a..8795b07d5 100644 --- a/templates/badge/request.html +++ b/templates/badge/request.html @@ -31,7 +31,7 @@ {% endblock %} {% block body %} -
+ {% csrf_token %}

{{ form.badge }}

@@ -39,13 +39,15 @@

{{ form.new_badge_name }}

+ {% if form.errors and form.errors.__all__ %} +

{{ _(form.errors.__all__[0]) }}

+ {% endif %}

{{ form.desc }}

{{ form.cert }}

- {% if form.errors %} -

{{ _('Please correct the errors below.') }}

- {{ form.errors}} + {% if form.errors and form.errors.cert %} +

{{ _(form.errors.cert[0]) }}

{% endif %}
diff --git a/templates/badge/tabs.html b/templates/badge/tabs.html index bf3769863..cb19b8a2e 100644 --- a/templates/badge/tabs.html +++ b/templates/badge/tabs.html @@ -1,16 +1,16 @@ From bf599924b16fa7a9c1e60b6c05e46c00335a6794 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Ho=C3=A0ng=20Ph=C3=BAc?= Date: Thu, 19 Sep 2024 11:43:04 +0700 Subject: [PATCH 09/13] Fix flake8 problems --- dmoj/urls.py | 302 ++++++++---- judge/admin/__init__.py | 30 +- judge/admin/badge.py | 6 +- judge/migrations/0207_auto_20240917_0704.py | 23 +- judge/migrations/0208_auto_20240917_0738.py | 7 +- .../0211_alter_badgerequest_badge.py | 5 +- judge/models/__init__.py | 23 +- judge/models/profile.py | 465 +++++++++--------- judge/views/badge.py | 111 +++-- 9 files changed, 536 insertions(+), 436 deletions(-) diff --git a/dmoj/urls.py b/dmoj/urls.py index 3672b3f59..22531d684 100644 --- a/dmoj/urls.py +++ b/dmoj/urls.py @@ -15,9 +15,10 @@ 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, tag, tasks, ticket, \ - two_factor, user, widgets, badge +from judge.views import (TitledTemplateView, api, badge, blog, comment, contests, + language, license, mailgun, organization, preview, problem, problem_manage, + ranked_submission, register, stats, status, submission, tag, tasks, ticket, + two_factor, user, widgets) from judge.views.magazine import MagazinePage from judge.views.problem_data import ProblemDataView, ProblemSubmissionDiff, \ problem_data_file, problem_init_view @@ -40,7 +41,8 @@ name='registration_activation_complete'), # Let's use , because a bad activation key should still get to the view; # that way, it can return a sensible "invalid key" message instead of a confusing 404. - path('activate//', ActivationView.as_view(), name='registration_activate'), + path('activate//', + ActivationView.as_view(), name='registration_activate'), path('register/', RegistrationView.as_view(), name='registration_register'), path('register/complete/', TitledTemplateView.as_view(template_name=REGISTRATION_COMPLETE_TEMPLATE, @@ -52,12 +54,14 @@ 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/', user.CustomPasswordChangeView.as_view(), + name='password_change'), path('password/change/done/', auth_views.PasswordChangeDoneView.as_view( template_name='registration/password_change_done.html', title=_('Password change successful'), ), name='password_change_done'), - path('password/reset/', user.CustomPasswordResetView.as_view(), name='password_reset'), + 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', @@ -77,10 +81,14 @@ path('2fa/enable/', two_factor.TOTPEnableView.as_view(), name='enable_2fa'), path('2fa/refresh/', two_factor.TOTPRefreshView.as_view(), name='refresh_2fa'), path('2fa/disable/', two_factor.TOTPDisableView.as_view(), name='disable_2fa'), - path('2fa/webauthn/attest/', two_factor.WebAuthnAttestationView.as_view(), name='webauthn_attest'), - path('2fa/webauthn/assert/', two_factor.WebAuthnAttestView.as_view(), name='webauthn_assert'), - path('2fa/webauthn/delete/', two_factor.WebAuthnDeleteView.as_view(), name='webauthn_delete'), - path('2fa/scratchcode/generate/', user.generate_scratch_codes, name='generate_scratch_codes'), + path('2fa/webauthn/attest/', + two_factor.WebAuthnAttestationView.as_view(), name='webauthn_attest'), + path('2fa/webauthn/assert/', two_factor.WebAuthnAttestView.as_view(), + name='webauthn_assert'), + path('2fa/webauthn/delete/', + two_factor.WebAuthnDeleteView.as_view(), name='webauthn_delete'), + path('2fa/scratchcode/generate/', user.generate_scratch_codes, + name='generate_scratch_codes'), path('api/token/generate/', user.generate_api_token, name='generate_api_token'), path('api/token/remove/', user.remove_api_token, name='remove_api_token'), @@ -101,52 +109,70 @@ def paged_list_view(view, name): urlpatterns = [ - path('', blog.PostList.as_view(template_name='home.html', title=_('Home')), kwargs={'page': 1}, name='home'), + path('', blog.PostList.as_view(template_name='home.html', + title=_('Home')), kwargs={'page': 1}, name='home'), path('500/', exception), path('admin/', admin.site.urls), path('i18n/', include('django.conf.urls.i18n')), path('accounts/', include(register_patterns)), path('', include('social_django.urls')), - path('certificates/', badge.open_certificate, name='open_certificate'), + path('certificates/', + badge.open_certificate, name='open_certificate'), path('problems', include([ path('/', problem.ProblemList.as_view(), name='problem_list'), path('/random/', problem.RandomProblem.as_view(), name='problem_random'), - path('/suggest_list/', problem.SuggestList.as_view(), name='problem_suggest_list'), + path('/suggest_list/', problem.SuggestList.as_view(), + name='problem_suggest_list'), path('/suggest', problem.ProblemSuggest.as_view(), name='problem_suggest'), path('/create', problem.ProblemCreate.as_view(), name='problem_create'), - path('/import-polygon', problem.ProblemImportPolygon.as_view(), name='problem_import_polygon'), + path('/import-polygon', problem.ProblemImportPolygon.as_view(), + name='problem_import_polygon'), ])), path('problem/', include([ path('', problem.ProblemDetail.as_view(), name='problem_detail'), path('/edit', problem.ProblemEdit.as_view(), name='problem_edit'), - path('/editorial', problem.ProblemSolution.as_view(), name='problem_editorial'), - path('/raw', xframe_options_sameorigin(problem.ProblemRaw.as_view()), name='problem_raw'), + path('/editorial', problem.ProblemSolution.as_view(), + name='problem_editorial'), + path('/raw', xframe_options_sameorigin(problem.ProblemRaw.as_view()), + name='problem_raw'), path('/pdf', problem.ProblemPdfView.as_view(), name='problem_pdf'), - 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('/update-polygon', problem.ProblemUpdatePolygon.as_view(), name='problem_update_polygon'), - - 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('/resubmit/', + problem.ProblemSubmit.as_view(), name='problem_submit'), + path('/update-polygon', problem.ProblemUpdatePolygon.as_view(), + name='problem_update_polygon'), + + 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('/', 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('/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('/tickets/', ticket.ProblemTicketListView.as_view(), + name='problem_ticket_list'), + path('/tickets/new', ticket.NewProblemTicketView.as_view(), + name='new_problem_ticket'), 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('', 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, @@ -168,15 +194,20 @@ def paged_list_view(view, name): path('tag/', include([ path('', tag.TagProblemDetail.as_view(), name='tagproblem_detail'), path('/assign', tag.TagProblemAssign.as_view(), name='tagproblem_assign'), - path('/', lambda _, tagproblem: HttpResponsePermanentRedirect(reverse('tagproblem_detail', args=[tagproblem]))), + path('/', lambda _, tagproblem: HttpResponsePermanentRedirect( + reverse('tagproblem_detail', args=[tagproblem]))), ])), path('submissions/', paged_list_view(submission.AllSubmissions, 'all_submissions')), - path('submissions/diff', submission.SubmissionSourceDiff, name='diff_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('submissions/diff', submission.SubmissionSourceDiff, + name='diff_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('src//download', submission.SubmissionSourceDownload.as_view(), name='submission_source_download'), @@ -201,7 +232,8 @@ def paged_list_view(view, name): ])), 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('data/download/', user.UserDownloadData.as_view(), + name='user_download_data'), path('user/', include([ path('', user.UserAboutPage.as_view(), name='user_page'), path('/ban', user.UserBan.as_view(), name='user_ban'), @@ -210,13 +242,16 @@ def paged_list_view(view, name): path('/comment/', paged_list_view(user.UserCommentPage, 'user_comment')), path('/solved/', include([ path('', user.UserProblemsPage.as_view(), name='user_problems'), - path('ajax', user.UserPerformancePointsAjax.as_view(), name='user_pp_ajax'), + path('ajax', user.UserPerformancePointsAjax.as_view(), + name='user_pp_ajax'), ])), - path('/submissions/', paged_list_view(submission.AllUserSubmissions, 'all_user_submissions_old')), + 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_page', args=[user]))), + path('/', lambda _, + user: HttpResponsePermanentRedirect(reverse('user_page', args=[user]))), ])), path('comments/upvote', comment.upvote_comment, name='comment_upvote'), @@ -224,38 +259,52 @@ def paged_list_view(view, name): 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('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/', paged_list_view(contests.ContestList, 'contest_list')), path('contests.ics', contests.ContestICal.as_view(), name='contest_ical'), - path('contests///', contests.ContestCalendar.as_view(), name='contest_calendar'), + path('contests///', + contests.ContestCalendar.as_view(), name='contest_calendar'), path('contests/new', contests.CreateContest.as_view(), name='contest_new'), 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('/ajax', contests.ContestTagDetailAjax.as_view(), + name='contest_tag_ajax'), ])), path('contest/', include([ path('', contests.ContestDetail.as_view(), name='contest_view'), - path('/all', contests.ContestAllProblems.as_view(), name='contest_all_problems'), + path('/all', contests.ContestAllProblems.as_view(), + name='contest_all_problems'), path('/edit', contests.EditContest.as_view(), name='contest_edit'), path('/moss', contests.ContestMossView.as_view(), name='contest_moss'), - path('/moss/delete', contests.ContestMossDelete.as_view(), name='contest_moss_delete'), - path('/announce', contests.ContestAnnounce.as_view(), name='contest_announce'), + path('/moss/delete', contests.ContestMossDelete.as_view(), + name='contest_moss_delete'), + path('/announce', contests.ContestAnnounce.as_view(), + name='contest_announce'), path('/clone', contests.ContestClone.as_view(), name='contest_clone'), - path('/ranking/', contests.ContestRanking.as_view(), name='contest_ranking'), - path('/public_ranking/', contests.ContestPublicRanking.as_view(), name='contest_public_ranking'), - path('/official_ranking/', contests.ContestOfficialRanking.as_view(), name='contest_official_ranking'), - path('/register', contests.ContestRegister.as_view(), name='contest_register'), + path('/ranking/', contests.ContestRanking.as_view(), + name='contest_ranking'), + path('/public_ranking/', contests.ContestPublicRanking.as_view(), + name='contest_public_ranking'), + path('/official_ranking/', contests.ContestOfficialRanking.as_view(), + name='contest_official_ranking'), + 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('/data/prepare/', contests.ContestPrepareData.as_view(), name='contest_prepare_data'), - path('/data/download/', contests.ContestDownloadData.as_view(), name='contest_download_data'), + path('/data/prepare/', contests.ContestPrepareData.as_view(), + name='contest_prepare_data'), + path('/data/download/', contests.ContestDownloadData.as_view(), + name='contest_download_data'), path('/rank//', paged_list_view(ranked_submission.ContestRankedSubmission, 'contest_ranked_submissions')), @@ -267,48 +316,68 @@ def paged_list_view(view, name): 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_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('/', lambda _, contest: HttpResponsePermanentRedirect( + reverse('contest_view', args=[contest]))), ])), path('contributors/', include([ path('', user.ContribList.as_view(), name='contributors_list'), path('', lambda request, page: HttpResponsePermanentRedirect('%s?page=%s' % (reverse('contributors_list'), page))), - path('find', user.user_contributor_redirect, name='user_contributor_redirect'), + path('find', user.user_contributor_redirect, + name='user_contributor_redirect'), ])), - path('organizations/', organization.OrganizationList.as_view(), name='organization_list'), - path('organizations/create', organization.CreateOrganization.as_view(), name='organization_create'), + path('organizations/', organization.OrganizationList.as_view(), + name='organization_list'), + path('organizations/create', organization.CreateOrganization.as_view(), + name='organization_create'), path('organization/-', lambda _, pk, suffix: HttpResponsePermanentRedirect('/organization/%s' % suffix)), path('organization/', include([ - path('', organization.OrganizationHome.as_view(), name='organization_home'), - 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('/usage', organization.MonthlyCreditUsageOrganization.as_view(), name='organization_monthly_usage'), - path('/problems/', organization.ProblemListOrganization.as_view(), name='problem_list_organization'), - path('/contests/', organization.ContestListOrganization.as_view(), name='contest_list_organization'), + path('', organization.OrganizationHome.as_view(), + name='organization_home'), + 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('/usage', organization.MonthlyCreditUsageOrganization.as_view(), + name='organization_monthly_usage'), + path('/problems/', organization.ProblemListOrganization.as_view(), + name='problem_list_organization'), + path('/contests/', organization.ContestListOrganization.as_view(), + name='contest_list_organization'), path('/submissions/', paged_list_view(organization.SubmissionListOrganization, 'submission_list_organization')), - path('/problem-create', organization.ProblemCreateOrganization.as_view(), name='problem_create_organization'), - path('/contest-create', organization.ContestCreateOrganization.as_view(), name='contest_create_organization'), + path('/problem-create', organization.ProblemCreateOrganization.as_view(), + name='problem_create_organization'), + path('/contest-create', organization.ContestCreateOrganization.as_view(), + name='contest_create_organization'), - path('/request', organization.RequestJoinOrganization.as_view(), name='request_organization'), + 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('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'), @@ -316,10 +385,12 @@ def paged_list_view(view, name): ])), path('/post/', include([ - path('new', organization.BlogPostCreateOrganization.as_view(), name='blog_post_create_organization'), + path('new', organization.BlogPostCreateOrganization.as_view(), + name='blog_post_create_organization'), ])), - path('/', lambda _, slug: HttpResponsePermanentRedirect(reverse('organization_home', args=[slug]))), + path('/', lambda _, slug: HttpResponsePermanentRedirect( + reverse('organization_home', args=[slug]))), ])), path('runtimes/', language.LanguageList.as_view(), name='runtime_list'), @@ -335,45 +406,65 @@ def paged_list_view(view, name): path('', blog.PostView.as_view(), name='blog_post'), path('/edit', blog.BlogPostEdit.as_view(), name='blog_post_edit'), path('/delete', blog.BlogPostDelete.as_view(), name='blog_post_delete'), - path('/', lambda _, id, slug: HttpResponsePermanentRedirect(reverse('blog_post', args=[id, slug]))), + path('/', lambda _, id, + slug: HttpResponsePermanentRedirect(reverse('blog_post', args=[id, slug]))), ])), path('license/', license.LicenseDetail.as_view(), name='license'), - path('mailgun/mail_activate/', mailgun.MailgunActivationView.as_view(), name='mailgun_activate'), + 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('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('template', problem.LanguageTemplateAjax.as_view(), + name='language_template_ajax'), path('select2/', include([ - path('user_search', UserSearchSelect2View.as_view(), name='user_search_select2_ajax'), + 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('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('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('upload-image', martor_image_uploader, + name='martor_image_uploader'), path('search-user', markdown_search_user, name='martor_search_user'), ])), ])), @@ -397,11 +488,16 @@ def paged_list_view(view, name): 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('/good', ticket.TicketStatusChangeView.as_view(contributive=True), name='ticket_good'), - path('/norm', ticket.TicketStatusChangeView.as_view(contributive=False), name='ticket_norm'), + 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('/good', ticket.TicketStatusChangeView.as_view(contributive=True), + name='ticket_good'), + path('/norm', ticket.TicketStatusChangeView.as_view(contributive=False), + name='ticket_norm'), path('/notes', ticket.TicketNotesEditView.as_view(), name='ticket_notes'), ])), @@ -411,7 +507,8 @@ def paged_list_view(view, name): path('profile/', UserSelect2View.as_view(), name='profile_select2'), path('organization_profile//', OrganizationUserSelect2View.as_view(), name='organization_profile_select2'), - path('organization/', OrganizationSelect2View.as_view(), name='organization_select2'), + path('organization/', OrganizationSelect2View.as_view(), + name='organization_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'), @@ -465,7 +562,8 @@ def paged_list_view(view, name): 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('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()), diff --git a/judge/admin/__init__.py b/judge/admin/__init__.py index 0bee6a764..3136f112c 100644 --- a/judge/admin/__init__.py +++ b/judge/admin/__init__.py @@ -3,6 +3,7 @@ from django.contrib.auth.models import User from django.contrib.flatpages.models import FlatPage +from judge.admin.badge import BadgeRequestAdmin from judge.admin.comments import CommentAdmin from judge.admin.contest import ContestAdmin, ContestParticipationAdmin, ContestTagAdmin from judge.admin.interface import ( @@ -21,32 +22,11 @@ from judge.admin.taxon import ProblemGroupAdmin, ProblemTypeAdmin from judge.admin.ticket import TicketAdmin from judge.models import ( - Badge, - BlogPost, - Comment, - CommentLock, - Contest, - ContestParticipation, - ContestTag, - Judge, - Language, - License, - MiscConfig, - NavigationBar, - Organization, - OrganizationRequest, - Problem, - ProblemGroup, - ProblemType, - Profile, - Submission, - Tag, - TagGroup, - TagProblem, - Ticket, - BadgeRequest, + Badge, BadgeRequest, BlogPost, Comment, CommentLock, Contest, + ContestParticipation, ContestTag, Judge, Language, License, MiscConfig, + NavigationBar, Organization, OrganizationRequest, Problem, ProblemGroup, + ProblemType, Profile, Submission, Tag, TagGroup, TagProblem, Ticket, ) -from judge.admin.badge import BadgeRequestAdmin admin.site.register(BlogPost, BlogPostAdmin) admin.site.register(Comment, CommentAdmin) diff --git a/judge/admin/badge.py b/judge/admin/badge.py index 01255fad6..191c927a4 100644 --- a/judge/admin/badge.py +++ b/judge/admin/badge.py @@ -3,9 +3,9 @@ class BadgeRequestAdmin(admin.ModelAdmin): - list_display = ("username", "badge", "new_badge_name", "state", "time") - readonly_fields = ("user", "cert", "desc") + list_display = ('username', 'badge', 'new_badge_name', 'state', 'time') + readonly_fields = ('user', 'cert', 'desc') - @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/migrations/0207_auto_20240917_0704.py b/judge/migrations/0207_auto_20240917_0704.py index 63a84c521..c91be96cb 100644 --- a/judge/migrations/0207_auto_20240917_0704.py +++ b/judge/migrations/0207_auto_20240917_0704.py @@ -1,7 +1,7 @@ # Generated by Django 3.2.25 on 2024-09-17 07:04 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): @@ -14,18 +14,25 @@ class Migration(migrations.Migration): 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/Ciudad_Juarez', 'Ciudad_Juarez'), ('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='Asia/Ho_Chi_Minh', max_length=50, verbose_name='time zone'), + 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/Ciudad_Juarez', 'Ciudad_Juarez'), ('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='Asia/Ho_Chi_Minh', max_length=50, verbose_name='time zone'), ), migrations.CreateModel( name='BadgeRequest', 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')), + ('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')), ('desc', models.TextField(verbose_name='description')), - ('cert', models.FileField(upload_to='certificates/', verbose_name='certificate')), - ('badge', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='badge_requests', to='judge.badge', verbose_name='badge')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='badge_requests', to='judge.profile', verbose_name='user')), + ('cert', models.FileField( + upload_to='certificates/', verbose_name='certificate')), + ('badge', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, + related_name='badge_requests', to='judge.badge', verbose_name='badge')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, + related_name='badge_requests', to='judge.profile', verbose_name='user')), ], options={ 'verbose_name': 'badge request', diff --git a/judge/migrations/0208_auto_20240917_0738.py b/judge/migrations/0208_auto_20240917_0738.py index 9bc39c106..f762ad8c4 100644 --- a/judge/migrations/0208_auto_20240917_0738.py +++ b/judge/migrations/0208_auto_20240917_0738.py @@ -1,6 +1,7 @@ # Generated by Django 3.2.25 on 2024-09-17 07:38 from django.db import migrations, models + import judge.models.profile @@ -14,11 +15,13 @@ class Migration(migrations.Migration): migrations.AddField( model_name='badgerequest', name='new_badge_name', - field=models.CharField(blank=True, max_length=128, verbose_name='new badge name'), + field=models.CharField( + blank=True, max_length=128, verbose_name='new badge name'), ), migrations.AlterField( model_name='badgerequest', name='cert', - field=models.FileField(upload_to='certificates/', validators=[judge.models.profile.validate_pdf], verbose_name='certificate'), + field=models.FileField(upload_to='certificates/', validators=[ + judge.models.profile.validate_pdf], verbose_name='certificate'), ), ] diff --git a/judge/migrations/0211_alter_badgerequest_badge.py b/judge/migrations/0211_alter_badgerequest_badge.py index 0d3d33590..b29408194 100644 --- a/judge/migrations/0211_alter_badgerequest_badge.py +++ b/judge/migrations/0211_alter_badgerequest_badge.py @@ -1,7 +1,7 @@ # Generated by Django 3.2.25 on 2024-09-17 15:00 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): @@ -14,6 +14,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='badgerequest', name='badge', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='badge_requests', to='judge.badge', verbose_name='badge'), + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, + related_name='badge_requests', to='judge.badge', verbose_name='badge'), ), ] diff --git a/judge/models/__init__.py b/judge/models/__init__.py index 36b8f3593..f1d7ce7be 100644 --- a/judge/models/__init__.py +++ b/judge/models/__init__.py @@ -44,13 +44,8 @@ problem_directory_file, ) from judge.models.profile import ( - Badge, - Organization, - OrganizationMonthlyUsage, - OrganizationRequest, - Profile, - WebAuthnCredential, - BadgeRequest, + Badge, BadgeRequest, Organization, OrganizationMonthlyUsage, + OrganizationRequest, Profile, WebAuthnCredential, ) from judge.models.runtime import Judge, Language, RuntimeVersion from judge.models.submission import ( @@ -62,19 +57,21 @@ from judge.models.tag import Tag, TagData, TagGroup, TagProblem from judge.models.ticket import GeneralIssue, 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"] + Comment, fields=['author', 'time', 'page', + 'score', 'body', 'hidden', 'parent'], ) revisions.register(TagProblem) -revisions.register(TagData, follow=["problem"]) +revisions.register(TagData, follow=['problem']) del revisions diff --git a/judge/models/profile.py b/judge/models/profile.py index 0a3ecc69a..5f2c91799 100644 --- a/judge/models/profile.py +++ b/judge/models/profile.py @@ -4,14 +4,14 @@ import secrets import struct -from django.forms import ValidationError import pyotp import webauthn from django.conf import settings from django.contrib.auth.models import User -from django.core.validators import RegexValidator +from django.core.validators import FileExtensionValidator, RegexValidator from django.db import models from django.db.models import F, Max, Sum +from django.forms import ValidationError from django.urls import reverse from django.utils import timezone from django.utils.encoding import force_bytes @@ -21,7 +21,6 @@ from fernet_fields import EncryptedCharField from pyotp.utils import strings_equal from sortedm2m.fields import SortedManyToManyField -from django.core.validators import FileExtensionValidator from judge.models.choices import ACE_THEMES, MATH_ENGINES_CHOICES, SITE_THEMES, TIMEZONE from judge.models.runtime import Language @@ -30,11 +29,11 @@ from judge.utils.two_factor import webauthn_decode __all__ = [ - "Organization", - "OrganizationMonthlyUsage", - "Profile", - "OrganizationRequest", - "WebAuthnCredential", + 'Organization', + 'OrganizationMonthlyUsage', + 'Profile', + 'OrganizationRequest', + 'WebAuthnCredential', ] @@ -46,67 +45,68 @@ def get_prep_value(self, value): class Organization(models.Model): - name = models.CharField(max_length=128, verbose_name=_("organization title")) + 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."), + verbose_name=_('organization slug'), + help_text=_('Organization name shown in URLs.'), validators=[ RegexValidator( - r"^[a-zA-Z]", _("Organization slugs must begin with a letter.") - ) + r'^[a-zA-Z]', _('Organization slugs must begin with a letter.'), + ), ], unique=True, ) short_name = models.CharField( max_length=20, - verbose_name=_("short name"), - help_text=_("Displayed beside user name during contests."), + verbose_name=_('short name'), + help_text=_('Displayed beside user name during contests.'), ) - about = models.TextField(verbose_name=_("organization description")) + 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."), + '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 + verbose_name=_('creation date'), auto_now_add=True, ) is_open = models.BooleanField( - verbose_name=_("is open organization?"), - help_text=_("Allow joining organization."), + verbose_name=_('is open organization?'), + help_text=_('Allow joining organization.'), default=False, ) is_unlisted = models.BooleanField( - verbose_name=_("is unlisted organization?"), - help_text=_("Organization will not be listed"), + verbose_name=_('is unlisted organization?'), + help_text=_('Organization will not be listed'), default=True, ) slots = models.IntegerField( - verbose_name=_("maximum size"), + verbose_name=_('maximum size'), null=True, blank=True, help_text=_( - "Maximum amount of users in this organization, " - "only applicable to private organizations." + '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"), + help_text=_('Student access code.'), + verbose_name=_('access code'), null=True, blank=True, ) logo_override_image = models.CharField( - verbose_name=_("logo override image"), - default="", + 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." + 'This image will replace the default site logo for users ' + 'viewing the organization.', ), ) performance_points = models.FloatField(default=0) @@ -119,8 +119,8 @@ class Organization(models.Model): def calculate_points(self, table=_pp_table): data = ( self.members.get_queryset() - .order_by("-performance_points") - .values_list("performance_points", flat=True) + .order_by('-performance_points') + .values_list('performance_points', flat=True) .filter(performance_points__gt=0) ) pp = settings.VNOJ_ORG_PP_SCALE * sum( @@ -128,7 +128,7 @@ def calculate_points(self, table=_pp_table): ) if not float_compare_equal(self.performance_points, pp): self.performance_points = pp - self.save(update_fields=["performance_points"]) + self.save(update_fields=['performance_points']) return pp def on_user_changes(self): @@ -136,7 +136,7 @@ def on_user_changes(self): member_count = self.members.count() if self.member_count != member_count: self.member_count = member_count - self.save(update_fields=["member_count"]) + self.save(update_fields=['member_count']) @cached_property def admins_list(self): @@ -154,50 +154,52 @@ def __contains__(self, item): return self.members.filter(id=item.id).exists() else: raise TypeError( - "Organization membership test must be Profile or primary key." + '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.slug]) + return reverse('organization_home', args=[self.slug]) def get_users_url(self): - return reverse("organization_users", args=[self.slug]) + return reverse('organization_users', args=[self.slug]) class Meta: - ordering = ["name"] + ordering = ['name'] permissions = ( - ("organization_admin", _("Administer organizations")), - ("edit_all_organization", _("Edit all organizations")), - ("change_open_organization", _("Change is_open field")), - ("spam_organization", _("Create organization without limit")), + ('organization_admin', _('Administer organizations')), + ('edit_all_organization', _('Edit all organizations')), + ('change_open_organization', _('Change is_open field')), + ('spam_organization', _('Create organization without limit')), ) - verbose_name = _("organization") - verbose_name_plural = _("organizations") + verbose_name = _('organization') + verbose_name_plural = _('organizations') class OrganizationMonthlyUsage(models.Model): organization = models.ForeignKey( Organization, - verbose_name=_("organization"), - related_name="monthly_usages", + verbose_name=_('organization'), + related_name='monthly_usages', on_delete=models.CASCADE, ) - time = models.DateField(verbose_name=_("time")) - consumed_credit = models.FloatField(verbose_name=_("consumed credit"), default=0) + time = models.DateField(verbose_name=_('time')) + consumed_credit = models.FloatField( + verbose_name=_('consumed credit'), default=0) class Meta: - verbose_name = _("organization monthly usage") - verbose_name_plural = _("organization monthly usages") - unique_together = ("organization", "time") + verbose_name = _('organization monthly usage') + verbose_name_plural = _('organization monthly usages') + unique_together = ('organization', 'time') class Badge(models.Model): - name = models.CharField(max_length=128, verbose_name=_("badge name")) - mini = models.URLField(verbose_name=_("mini badge URL"), blank=True) - full_size = models.URLField(verbose_name=_("full size badge URL"), blank=True) + name = models.CharField(max_length=128, verbose_name=_('badge name')) + mini = models.URLField(verbose_name=_('mini badge URL'), blank=True) + full_size = models.URLField(verbose_name=_( + 'full size badge URL'), blank=True) def __str__(self): return self.name @@ -205,18 +207,19 @@ def __str__(self): class Profile(models.Model): user = models.OneToOneField( - User, verbose_name=_("user associated"), on_delete=models.CASCADE + User, verbose_name=_('user associated'), on_delete=models.CASCADE, ) - about = models.TextField(verbose_name=_("self-description"), null=True, blank=True) + about = models.TextField(verbose_name=_( + 'self-description'), null=True, blank=True) timezone = models.CharField( max_length=50, - verbose_name=_("time zone"), + verbose_name=_('time zone'), choices=TIMEZONE, default=settings.DEFAULT_USER_TIME_ZONE, ) language = models.ForeignKey( - "Language", - verbose_name=_("preferred language"), + 'Language', + verbose_name=_('preferred language'), on_delete=models.SET_DEFAULT, default=Language.get_default_language_pk, ) @@ -226,143 +229,148 @@ class Profile(models.Model): vnoj_points = models.IntegerField(default=0) problem_count = models.IntegerField(default=0) ace_theme = models.CharField( - max_length=30, verbose_name=_("Ace theme"), choices=ACE_THEMES, default="auto" + max_length=30, verbose_name=_('Ace theme'), choices=ACE_THEMES, default='auto', ) site_theme = models.CharField( max_length=10, - verbose_name=_("site theme"), + verbose_name=_('site theme'), choices=SITE_THEMES, - default="light", + default='light', ) - last_access = models.DateTimeField(verbose_name=_("last access time"), default=now) - ip = models.GenericIPAddressField(verbose_name=_("last IP"), blank=True, null=True) + last_access = models.DateTimeField( + verbose_name=_('last access time'), default=now) + ip = models.GenericIPAddressField( + verbose_name=_('last IP'), blank=True, null=True) ip_auth = models.GenericIPAddressField( - verbose_name=_("IP-based authentication"), unique=True, blank=True, null=True + verbose_name=_('IP-based authentication'), unique=True, blank=True, null=True, ) badges = models.ManyToManyField( - Badge, verbose_name=_("badges"), blank=True, related_name="users" + Badge, verbose_name=_('badges'), blank=True, related_name='users', ) display_badge = models.ForeignKey( - Badge, verbose_name=_("display badge"), null=True, on_delete=models.SET_NULL + Badge, verbose_name=_('display badge'), null=True, on_delete=models.SET_NULL, ) organizations = SortedManyToManyField( Organization, - verbose_name=_("organization"), + verbose_name=_('organization'), blank=True, - related_name="members", - related_query_name="member", + related_name='members', + related_query_name='member', ) display_rank = models.CharField( max_length=10, - default="user", - verbose_name=_("display rank"), + default='user', + verbose_name=_('display rank'), choices=settings.VNOJ_DISPLAY_RANKS, ) mute = models.BooleanField( - verbose_name=_("comment mute"), - help_text=_("Some users are at their best when silent."), + 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."), + verbose_name=_('unlisted user'), + help_text=_('User will not be ranked.'), default=False, ) ban_reason = models.TextField( - null=True, blank=True, help_text=_("Show to banned user in login page.") + null=True, blank=True, help_text=_('Show to banned user in login page.'), ) allow_tagging = models.BooleanField( - verbose_name=_("Allow tagging"), - help_text=_("User will be allowed to tag problems."), + verbose_name=_('Allow tagging'), + help_text=_('User will be allowed to tag problems.'), default=True, ) rating = models.IntegerField(null=True, default=None) user_script = models.TextField( - verbose_name=_("user script"), - default="", + verbose_name=_('user script'), + default='', blank=True, max_length=65536, - help_text=_("User-defined JavaScript for site customization."), + help_text=_('User-defined JavaScript for site customization.'), ) current_contest = models.OneToOneField( - "ContestParticipation", - verbose_name=_("current contest"), + 'ContestParticipation', + verbose_name=_('current contest'), null=True, blank=True, - related_name="+", + related_name='+', on_delete=models.SET_NULL, ) math_engine = models.CharField( - verbose_name=_("math engine"), + 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."), + help_text=_('The rendering engine used to render math.'), ) is_totp_enabled = models.BooleanField( - verbose_name=_("TOTP 2FA enabled"), + verbose_name=_('TOTP 2FA enabled'), default=False, - help_text=_("Check to enable TOTP-based two-factor authentication."), + help_text=_('Check to enable TOTP-based two-factor authentication.'), ) is_webauthn_enabled = models.BooleanField( - verbose_name=_("WebAuthn 2FA enabled"), + verbose_name=_('WebAuthn 2FA enabled'), default=False, - help_text=_("Check to enable WebAuthn-based two-factor authentication."), + 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."), + 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.")) + 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"), + verbose_name=_('scratch codes'), help_text=_( - "JSON array of 16-character Base32-encoded codes " "for scratch codes." + 'JSON array of 16-character Base32-encoded codes " "for scratch codes.', ), validators=[ RegexValidator( - r'^(\[\])?$|^\[("[A-Z0-9]{16}", *)*"[A-Z0-9]{16}"\]$', + r"^(\[\])?$|^\[('[A-Z0-9]{16}', *)*'[A-Z0-9]{16}'\]$", _( - "Scratch codes must be empty or a JSON array of " - "16-character Base32 codes." + '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 + 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."), + 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")) + RegexValidator( + '^[a-f0-9]{64}$', _('API token must be None or hexadecimal')), ], ) notes = models.TextField( - verbose_name=_("internal notes"), + verbose_name=_('internal notes'), null=True, blank=True, - help_text=_("Notes for administrators regarding this user."), + help_text=_('Notes for administrators regarding this user.'), ) data_last_downloaded = models.DateTimeField( - verbose_name=_("last data download time"), null=True, blank=True + 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."), + verbose_name=_('display name override'), + help_text=_('Name displayed in place of username.'), ) @cached_property @@ -394,31 +402,26 @@ def is_banned(self): return not self.user.is_active and self.ban_reason is not None def can_be_banned_by(self, staff): - return ( - self.user != staff - and not self.user.is_superuser - and staff.has_perm("judge.ban_user") - ) + return (self.user != staff and not self.user.is_superuser and staff.has_perm('judge.ban_user')) @cached_property def can_tag_problems(self): if self.allow_tagging: - if self.user.has_perm("judge.add_tagproblem"): + if self.user.has_perm('judge.add_tagproblem'): return True if ( - self.rating is not None - and self.rating >= settings.VNOJ_TAG_PROBLEM_MIN_RATING + self.rating is not None and self.rating >= settings.VNOJ_TAG_PROBLEM_MIN_RATING ): return True return False @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 @@ -426,10 +429,12 @@ def resolved_ace_theme(self): @cached_property def registered_contest_ids(self): return set( - self.contest_history.filter(virtual=0).values_list("contest_id", flat=True) + self.contest_history.filter( + virtual=0).values_list('contest_id', flat=True), ) - _pp_table = [pow(settings.DMOJ_PP_STEP, i) for i in range(settings.DMOJ_PP_ENTRIES)] + _pp_table = [pow(settings.DMOJ_PP_STEP, i) + for i in range(settings.DMOJ_PP_ENTRIES)] def calculate_points(self, table=_pp_table): from judge.models import Problem @@ -437,11 +442,11 @@ def calculate_points(self, table=_pp_table): public_problems = Problem.get_public_problems() data = ( public_problems.filter( - submission__user=self, submission__points__isnull=False + submission__user=self, submission__points__isnull=False, ) - .annotate(max_points=Max("submission__points")) - .order_by("-max_points") - .values_list("max_points", flat=True) + .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 @@ -449,23 +454,24 @@ def calculate_points(self, table=_pp_table): problems = ( public_problems.filter( submission__user=self, - submission__result="AC", - submission__case_points__gte=F("submission__case_total"), + submission__result='AC', + submission__case_points__gte=F('submission__case_total'), ) - .values("id") + .values('id') .distinct() .count() ) pp = sum(x * y for x, y in zip(table, data)) + bonus_function(problems) if ( - not float_compare_equal(self.points, points) - or problems != self.problem_count - or not float_compare_equal(self.performance_points, pp) + not float_compare_equal(self.points, points) or + problems != self.problem_count or not + float_compare_equal(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']) for org in self.organizations.get_queryset(): org.calculate_points() return points @@ -481,28 +487,28 @@ def calculate_contribution_points(self): # Please note that `0 or X` will return None if X is None total_comment_scores = ( Comment.objects.filter(author=self.id, hidden=False).aggregate( - sum=Sum("score") - )["sum"] - or 0 + sum=Sum('score'), + )['sum'] or 0 ) total_blog_scores = ( BlogPost.objects.filter( - authors=self.id, visible=True, organization=None - ).aggregate(sum=Sum("score"))["sum"] - or 0 + authors=self.id, visible=True, organization=None, + ).aggregate(sum=Sum('score'))['sum'] or 0 ) count_good_tickets = Ticket.objects.filter( - user=self.id, is_contributive=True + user=self.id, is_contributive=True, ).count() - count_suggested_problem = self.suggested_problems.filter(is_public=True).count() + count_suggested_problem = self.suggested_problems.filter( + is_public=True).count() new_pp = ( - (total_comment_scores + total_blog_scores) * settings.VNOJ_CP_COMMENT - + count_good_tickets * settings.VNOJ_CP_TICKET - + count_suggested_problem * settings.VNOJ_CP_PROBLEM + (total_comment_scores + total_blog_scores) * + settings.VNOJ_CP_COMMENT + + count_good_tickets * settings.VNOJ_CP_TICKET + + count_suggested_problem * settings.VNOJ_CP_PROBLEM ) if new_pp != old_pp: self.contribution_points = new_pp - self.save(update_fields=["contribution_points"]) + self.save(update_fields=['contribution_points']) return new_pp calculate_contribution_points.alters_data = True @@ -511,7 +517,7 @@ def update_contribution_points(self, delta): # this is just for testing the contribution # we should not use this function to update contribution points self.contribution_points += delta - self.save(update_fields=["contribution_points"]) + self.save(update_fields=['contribution_points']) return self.contribution_points update_contribution_points.alters_data = True @@ -519,25 +525,26 @@ def update_contribution_points(self, delta): def generate_api_token(self): secret = secrets.token_bytes(32) self.api_token = hmac.new( - force_bytes(settings.SECRET_KEY), msg=secret, digestmod="sha256" + 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.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) + 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 @@ -565,11 +572,11 @@ def check_totp_code(self, code): 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, 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 @@ -577,39 +584,40 @@ def check_totp_code(self, code): def ban_user(self, reason): self.ban_reason = reason - self.display_rank = "banned" + self.display_rank = 'banned' self.is_unlisted = True - self.save(update_fields=["ban_reason", "display_rank", "is_unlisted"]) + self.save(update_fields=['ban_reason', 'display_rank', 'is_unlisted']) self.user.is_active = False - self.user.save(update_fields=["is_active"]) + self.user.save(update_fields=['is_active']) ban_user.alters_data = True def unban_user(self): self.ban_reason = None - self.display_rank = Profile._meta.get_field("display_rank").get_default() + self.display_rank = Profile._meta.get_field( + 'display_rank').get_default() self.is_unlisted = False - self.save(update_fields=["ban_reason", "display_rank", "is_unlisted"]) + self.save(update_fields=['ban_reason', 'display_rank', 'is_unlisted']) self.user.is_active = True - self.user.save(update_fields=["is_active"]) + self.user.save(update_fields=['is_active']) unban_user.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 + 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", + return 'rating %s %s' % ( + rating_class(rating) if rating is not None else 'rate-none', display_rank, ) return display_rank @@ -622,47 +630,47 @@ def css_class(self): def webauthn_id(self): return hmac.new( force_bytes(settings.SECRET_KEY), - msg=b"webauthn:%d" % (self.id,), - digestmod="sha256", + msg=b'webauthn:%d' % (self.id,), + digestmod='sha256', ).digest() class Meta: permissions = ( - ("test_site", _("Shows in-progress development stuff")), - ("totp", _("Edit TOTP settings")), - ("can_upload_image", _("Can upload image directly to server via martor")), - ("high_problem_timelimit", _("Can set high problem timelimit")), - ("long_contest_duration", _("Can set long contest duration")), + ('test_site', _('Shows in-progress development stuff')), + ('totp', _('Edit TOTP settings')), + ('can_upload_image', _('Can upload image directly to server via martor')), + ('high_problem_timelimit', _('Can set high problem timelimit')), + ('long_contest_duration', _('Can set long contest duration')), ( - "create_mass_testcases", - _("Can create unlimitted number of testcases for a problem"), + 'create_mass_testcases', + _('Can create unlimitted number of testcases for a problem'), ), - ("ban_user", _("Ban users")), + ('ban_user', _('Ban users')), ) - 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", "-contribution_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', '-contribution_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", + verbose_name=_('user'), + related_name='webauthn_credentials', on_delete=models.CASCADE, ) - name = models.CharField(verbose_name=_("device name"), max_length=100) + name = models.CharField(verbose_name=_('device name'), max_length=100) cred_id = models.CharField( - verbose_name=_("credential ID"), max_length=255, unique=True + verbose_name=_('credential ID'), max_length=255, unique=True, ) - public_key = models.TextField(verbose_name=_("public key")) - counter = models.BigIntegerField(verbose_name=_("sign counter")) + public_key = models.TextField(verbose_name=_('public key')) + counter = models.BigIntegerField(verbose_name=_('sign counter')) @cached_property def webauthn_user(self): @@ -680,94 +688,97 @@ 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", + verbose_name=_('user'), + related_name='requests', on_delete=models.CASCADE, ) organization = models.ForeignKey( Organization, - verbose_name=_("organization"), - related_name="requests", + verbose_name=_('organization'), + related_name='requests', on_delete=models.CASCADE, ) - time = models.DateTimeField(verbose_name=_("request time"), auto_now_add=True) + time = models.DateTimeField(verbose_name=_( + 'request time'), auto_now_add=True) state = models.CharField( max_length=1, - verbose_name=_("state"), + verbose_name=_('state'), choices=( - ("P", _("Pending")), - ("A", _("Approved")), - ("R", _("Rejected")), + ('P', _('Pending')), + ('A', _('Approved')), + ('R', _('Rejected')), ), ) - reason = models.TextField(verbose_name=_("reason")) + reason = models.TextField(verbose_name=_('reason')) class Meta: - verbose_name = _("organization join request") - verbose_name_plural = _("organization join requests") + verbose_name = _('organization join request') + verbose_name_plural = _('organization join requests') def validate_pdf(file): - if not file.name.endswith(".pdf"): - raise ValidationError("Only PDF files are allowed.") + if not file.name.endswith('.pdf'): + raise ValidationError('Only PDF files are allowed.') class BadgeRequest(models.Model): user = models.ForeignKey( Profile, - verbose_name=_("user"), - related_name="badge_requests", + verbose_name=_('user'), + related_name='badge_requests', on_delete=models.CASCADE, ) badge = models.ForeignKey( Badge, - verbose_name=_("badge"), - related_name="badge_requests", + verbose_name=_('badge'), + related_name='badge_requests', on_delete=models.CASCADE, null=True, ) new_badge_name = models.CharField( max_length=128, - verbose_name=_("new badge name"), + verbose_name=_('new badge name'), blank=True, ) - time = models.DateTimeField(verbose_name=_("request time"), auto_now_add=True) + time = models.DateTimeField(verbose_name=_( + 'request time'), auto_now_add=True) state = models.CharField( max_length=1, - verbose_name=_("state"), + verbose_name=_('state'), choices=( - ("P", _("Pending")), - ("A", _("Approved")), - ("R", _("Rejected")), + ('P', _('Pending')), + ('A', _('Approved')), + ('R', _('Rejected')), ), ) - desc = models.TextField(verbose_name=_("description")) + desc = models.TextField(verbose_name=_('description')) cert = models.FileField( - verbose_name=_("Certificate"), - upload_to="certificates", + verbose_name=_('Certificate'), + upload_to='certificates', validators=[ - FileExtensionValidator(allowed_extensions=settings.PDF_STATEMENT_SAFE_EXTS) + FileExtensionValidator( + allowed_extensions=settings.PDF_STATEMENT_SAFE_EXTS), ], ) class Meta: - verbose_name = _("badge request") - verbose_name_plural = _("badge requests") + verbose_name = _('badge request') + verbose_name_plural = _('badge requests') def __str__(self): badge_name = ( self.badge.name if self.badge and self.badge.name - else self.new_badge_name if self.new_badge_name else "No Badge" + else self.new_badge_name if self.new_badge_name else 'No Badge' ) - return f"{self.user.user.username} - {badge_name} - {self.get_state_display()}" + return f'{self.user.user.username} - {badge_name} - {self.get_state_display()}' diff --git a/judge/views/badge.py b/judge/views/badge.py index c943704b7..2a41aad08 100644 --- a/judge/views/badge.py +++ b/judge/views/badge.py @@ -1,7 +1,10 @@ import os + from django import forms from django.conf import settings +from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin +from django.core.exceptions import PermissionDenied from django.http import ( FileResponse, Http404, @@ -9,48 +12,46 @@ HttpResponseRedirect, ) from django.urls import reverse -from django.utils.translation import gettext as _, gettext_lazy, ngettext -from django.core.exceptions import PermissionDenied from django.utils.html import format_html -from django.contrib import messages +from django.utils.translation import gettext as _, gettext_lazy, ngettext from django.views.generic import DetailView, FormView, View -from judge.models import BadgeRequest, Badge, Profile +from judge.models import Badge, BadgeRequest, Profile from judge.utils.views import TitleMixin def validate_pdf(value): - if not value.name.endswith(".pdf"): - raise forms.ValidationError(_("Only PDF files are allowed.")) + if not value.name.endswith('.pdf'): + raise forms.ValidationError(_('Only PDF files are allowed.')) class BadgeRequestForm(forms.ModelForm): class Meta: model = BadgeRequest - fields = ["badge", "desc", "cert", "new_badge_name"] + fields = ['badge', 'desc', 'cert', 'new_badge_name'] def __init__(self, *args, **kwargs): super(BadgeRequestForm, self).__init__(*args, **kwargs) - self.fields["badge"].queryset = Badge.objects.all() - self.fields["badge"].required = False - self.fields["cert"].widget.attrs.update({"accept": "application/pdf"}) - self.fields["cert"].validators.append(validate_pdf) + self.fields['badge'].queryset = Badge.objects.all() + self.fields['badge'].required = False + self.fields['cert'].widget.attrs.update({'accept': 'application/pdf'}) + self.fields['cert'].validators.append(validate_pdf) def clean(self): cleaned_data = super().clean() - badge = cleaned_data.get("badge") - new_badge_name = cleaned_data.get("new_badge_name") + badge = cleaned_data.get('badge') + new_badge_name = cleaned_data.get('new_badge_name') if not badge and not new_badge_name: raise forms.ValidationError( - "You must select an existing badge or enter a new badge name." + 'You must select an existing badge or enter a new badge name.', ) return cleaned_data class RequestAddBadge(LoginRequiredMixin, FormView): - template_name = "badge/request.html" + template_name = 'badge/request.html' form_class = BadgeRequestForm def dispatch(self, request, *args, **kwargs): @@ -58,20 +59,20 @@ def dispatch(self, request, *args, **kwargs): def get_context_data(self, **kwargs): context = super(RequestAddBadge, self).get_context_data(**kwargs) - context["title"] = _("Request a new badge") + context['title'] = _('Request a new badge') return context def form_valid(self, form): badge_request = BadgeRequest() badge_request.user = Profile.objects.get(user=self.request.user) - badge_request.badge = form.cleaned_data["badge"] - badge_request.desc = form.cleaned_data["desc"] - badge_request.cert = form.cleaned_data["cert"] - badge_request.new_badge_name = form.cleaned_data["new_badge_name"] - badge_request.state = "P" + badge_request.badge = form.cleaned_data['badge'] + badge_request.desc = form.cleaned_data['desc'] + badge_request.cert = form.cleaned_data['cert'] + badge_request.new_badge_name = form.cleaned_data['new_badge_name'] + badge_request.state = 'P' badge_request.save() return HttpResponseRedirect( - reverse("request_badge_detail", args=(badge_request.id,)) + reverse('request_badge_detail', args=(badge_request.id,)), ) def form_invalid(self, form): @@ -80,9 +81,9 @@ def form_invalid(self, form): class BadgeRequestDetail(LoginRequiredMixin, TitleMixin, DetailView): model = BadgeRequest - template_name = "badge/detail.html" - title = gettext_lazy("Badge request detail") - pk_url_kwarg = "rpk" + template_name = 'badge/detail.html' + title = gettext_lazy('Badge request detail') + pk_url_kwarg = 'rpk' def get_object(self, queryset=None): object = super(BadgeRequestDetail, self).get_object(queryset) @@ -93,7 +94,7 @@ def get_object(self, queryset=None): BadgeRequestFormSet = forms.modelformset_factory( - BadgeRequest, extra=0, fields=("state",), can_delete=True + BadgeRequest, extra=0, fields=('state',), can_delete=True, ) @@ -109,35 +110,36 @@ def get_object(self, queryset=None): def get_requests(self): queryset = ( - self.object.requests.select_related("user__user") + self.object.requests.select_related('user__user') .defer( - "user__about", - "user__notes", - "user__user_script", + 'user__about', + 'user__notes', + 'user__user_script', ) - .order_by("-id") + .order_by('-id') ) return queryset def get_context_data(self, **kwargs): context = super(BadgeRequestBaseView, self).get_context_data(**kwargs) - context["title"] = _("Managing join requests for %s") % self.object.name - context["content_title"] = format_html( - _("Managing join requests for %s") % ' {0}', + context['title'] = _( + 'Managing join requests for %s') % self.object.name + context['content_title'] = format_html( + _('Managing join requests for %s') % " {0}", self.object.name, self.object.get_absolute_url(), ) - context["tab"] = self.tab + context['tab'] = self.tab return context class BadgeRequestView(BadgeRequestBaseView): - template_name = "badge/pending.html" - tab = "pending" + template_name = 'badge/pending.html' + tab = 'pending' def get_context_data(self, **kwargs): context = super(BadgeRequestView, self).get_context_data(**kwargs) - context["formset"] = self.formset + context['formset'] = self.formset return context def get(self, request, *args, **kwargs): @@ -147,27 +149,28 @@ 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 = badge = self.get_object() self.formset = formset = BadgeRequestFormSet( - request.POST, request.FILES, queryset=self.get_requests() + request.POST, request.FILES, queryset=self.get_requests(), ) if formset.is_valid(): approved, rejected = 0, 0 for obj in formset.save(): - if obj.state == "A": + if obj.state == 'A': obj.user.badges.add(obj.badge) approved += 1 - elif obj.state == "R": + elif obj.state == 'R': rejected += 1 messages.success( request, - ngettext("Approved %d request.", "Approved %d requests.", approved) - % approved - + "\n" - + ngettext("Rejected %d request.", "Rejected %d requests.", rejected) + ngettext('Approved %d request.', + 'Approved %d requests.', approved) + % approved + '\n' + + ngettext('Rejected %d request.', + 'Rejected %d requests.', rejected) % rejected, ) return HttpResponseRedirect(request.get_full_path()) @@ -177,9 +180,9 @@ def post(self, request, *args, **kwargs): class BadgeRequestLog(BadgeRequestBaseView): - states = ("A", "R") - tab = "log" - template_name = "badge/log.html" + states = ('A', 'R') + tab = 'log' + template_name = 'badge/log.html' def get(self, request, *args, **kwargs): self.object = self.get_object() @@ -188,20 +191,20 @@ def get(self, request, *args, **kwargs): def get_context_data(self, **kwargs): context = super(BadgeRequestLog, 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 def open_certificate(request, filename): # Check if the user is authenticated and an admin if not request.user.is_authenticated or not request.user.is_staff: - return HttpResponseForbidden("You do not have permission to view this file.") + return HttpResponseForbidden('You do not have permission to view this file.') # Path to the PDF file - file_path = os.path.join(settings.MEDIA_ROOT, "certificates", filename) + file_path = os.path.join(settings.MEDIA_ROOT, 'certificates', filename) # Check if the file exists if os.path.exists(file_path): - return FileResponse(open(file_path, "rb"), content_type="application/pdf") + return FileResponse(open(file_path, 'rb'), content_type='application/pdf') else: - raise Http404("File does not exist") + raise Http404('File does not exist') From fc32c3e4b4685b5582bf55479e85e673dd7c7812 Mon Sep 17 00:00:00 2001 From: Le Duy Thuc Date: Sat, 21 Sep 2024 00:55:46 +0000 Subject: [PATCH 10/13] fix migrations --- judge/migrations/0207_auto_20240917_0704.py | 42 ------------------- judge/migrations/0208_auto_20240917_0738.py | 27 ------------ judge/migrations/0208_badge_request.py | 32 ++++++++++++++ .../0209_alter_badgerequest_cert.py | 19 --------- .../0210_alter_badgerequest_cert.py | 19 --------- .../0211_alter_badgerequest_badge.py | 20 --------- 6 files changed, 32 insertions(+), 127 deletions(-) delete mode 100644 judge/migrations/0207_auto_20240917_0704.py delete mode 100644 judge/migrations/0208_auto_20240917_0738.py create mode 100644 judge/migrations/0208_badge_request.py delete mode 100644 judge/migrations/0209_alter_badgerequest_cert.py delete mode 100644 judge/migrations/0210_alter_badgerequest_cert.py delete mode 100644 judge/migrations/0211_alter_badgerequest_badge.py diff --git a/judge/migrations/0207_auto_20240917_0704.py b/judge/migrations/0207_auto_20240917_0704.py deleted file mode 100644 index c91be96cb..000000000 --- a/judge/migrations/0207_auto_20240917_0704.py +++ /dev/null @@ -1,42 +0,0 @@ -# Generated by Django 3.2.25 on 2024-09-17 07:04 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('judge', '0206_monthly_credit'), - ] - - operations = [ - 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/Ciudad_Juarez', 'Ciudad_Juarez'), ('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='Asia/Ho_Chi_Minh', max_length=50, verbose_name='time zone'), - ), - migrations.CreateModel( - name='BadgeRequest', - 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')), - ('desc', models.TextField(verbose_name='description')), - ('cert', models.FileField( - upload_to='certificates/', verbose_name='certificate')), - ('badge', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, - related_name='badge_requests', to='judge.badge', verbose_name='badge')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, - related_name='badge_requests', to='judge.profile', verbose_name='user')), - ], - options={ - 'verbose_name': 'badge request', - 'verbose_name_plural': 'badge requests', - }, - ), - ] diff --git a/judge/migrations/0208_auto_20240917_0738.py b/judge/migrations/0208_auto_20240917_0738.py deleted file mode 100644 index f762ad8c4..000000000 --- a/judge/migrations/0208_auto_20240917_0738.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 3.2.25 on 2024-09-17 07:38 - -from django.db import migrations, models - -import judge.models.profile - - -class Migration(migrations.Migration): - - dependencies = [ - ('judge', '0207_auto_20240917_0704'), - ] - - operations = [ - migrations.AddField( - model_name='badgerequest', - name='new_badge_name', - field=models.CharField( - blank=True, max_length=128, verbose_name='new badge name'), - ), - migrations.AlterField( - model_name='badgerequest', - name='cert', - field=models.FileField(upload_to='certificates/', validators=[ - judge.models.profile.validate_pdf], verbose_name='certificate'), - ), - ] diff --git a/judge/migrations/0208_badge_request.py b/judge/migrations/0208_badge_request.py new file mode 100644 index 000000000..8ccbd00f0 --- /dev/null +++ b/judge/migrations/0208_badge_request.py @@ -0,0 +1,32 @@ +# Generated by Django 3.2.19 on 2024-09-21 00:52 + +import django.core.validators +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('judge', '0207_org_credit'), + ] + + operations = [ + migrations.CreateModel( + name='BadgeRequest', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('new_badge_name', models.CharField(blank=True, max_length=128, verbose_name='new badge name')), + ('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')), + ('desc', models.TextField(verbose_name='description')), + ('cert', models.FileField(upload_to='certificates', validators=[django.core.validators.FileExtensionValidator(allowed_extensions={'pdf'})], verbose_name='Certificate')), + ('badge', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='badge_requests', to='judge.badge', verbose_name='badge')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='badge_requests', to='judge.profile', verbose_name='user')), + ], + options={ + 'verbose_name': 'badge request', + 'verbose_name_plural': 'badge requests', + }, + ), + ] diff --git a/judge/migrations/0209_alter_badgerequest_cert.py b/judge/migrations/0209_alter_badgerequest_cert.py deleted file mode 100644 index 2809a03cf..000000000 --- a/judge/migrations/0209_alter_badgerequest_cert.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 3.2.25 on 2024-09-17 14:11 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('judge', '0208_auto_20240917_0738'), - ] - - operations = [ - migrations.AlterField( - model_name='badgerequest', - name='cert', - field=models.FileField(upload_to='', validators=[django.core.validators.FileExtensionValidator(allowed_extensions={'pdf'})]), - ), - ] diff --git a/judge/migrations/0210_alter_badgerequest_cert.py b/judge/migrations/0210_alter_badgerequest_cert.py deleted file mode 100644 index 4c178c4a0..000000000 --- a/judge/migrations/0210_alter_badgerequest_cert.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 3.2.25 on 2024-09-17 14:52 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('judge', '0209_alter_badgerequest_cert'), - ] - - operations = [ - migrations.AlterField( - model_name='badgerequest', - name='cert', - field=models.FileField(upload_to='certificates', validators=[django.core.validators.FileExtensionValidator(allowed_extensions={'pdf'})], verbose_name='Certificate'), - ), - ] diff --git a/judge/migrations/0211_alter_badgerequest_badge.py b/judge/migrations/0211_alter_badgerequest_badge.py deleted file mode 100644 index b29408194..000000000 --- a/judge/migrations/0211_alter_badgerequest_badge.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 3.2.25 on 2024-09-17 15:00 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('judge', '0210_alter_badgerequest_cert'), - ] - - operations = [ - migrations.AlterField( - model_name='badgerequest', - name='badge', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, - related_name='badge_requests', to='judge.badge', verbose_name='badge'), - ), - ] From e297b3ca7ae7cb977caad097551e786b162faa05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Ho=C3=A0ng=20Ph=C3=BAc?= Date: Sat, 21 Sep 2024 09:30:19 +0700 Subject: [PATCH 11/13] Undo formatting on existing files --- dmoj/urls.py | 302 ++++++++------------- judge/admin/__init__.py | 18 +- judge/models/__init__.py | 71 +---- judge/models/profile.py | 547 ++++++++++----------------------------- 4 files changed, 259 insertions(+), 679 deletions(-) diff --git a/dmoj/urls.py b/dmoj/urls.py index 22531d684..d0a8b41e5 100644 --- a/dmoj/urls.py +++ b/dmoj/urls.py @@ -15,10 +15,9 @@ from judge.feed import AtomBlogFeed, AtomCommentFeed, AtomProblemFeed, BlogFeed, CommentFeed, ProblemFeed from judge.sitemap import sitemaps -from judge.views import (TitledTemplateView, api, badge, blog, comment, contests, - language, license, mailgun, organization, preview, problem, problem_manage, - ranked_submission, register, stats, status, submission, tag, tasks, ticket, - two_factor, user, widgets) +from judge.views import TitledTemplateView, api, badge, blog, comment, contests, language, license, mailgun, organization, \ + preview, problem, problem_manage, ranked_submission, register, stats, status, submission, tag, tasks, ticket, \ + two_factor, user, widgets from judge.views.magazine import MagazinePage from judge.views.problem_data import ProblemDataView, ProblemSubmissionDiff, \ problem_data_file, problem_init_view @@ -41,8 +40,7 @@ name='registration_activation_complete'), # Let's use , because a bad activation key should still get to the view; # that way, it can return a sensible "invalid key" message instead of a confusing 404. - path('activate//', - ActivationView.as_view(), name='registration_activate'), + path('activate//', ActivationView.as_view(), name='registration_activate'), path('register/', RegistrationView.as_view(), name='registration_register'), path('register/complete/', TitledTemplateView.as_view(template_name=REGISTRATION_COMPLETE_TEMPLATE, @@ -54,14 +52,12 @@ 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/', user.CustomPasswordChangeView.as_view(), name='password_change'), path('password/change/done/', auth_views.PasswordChangeDoneView.as_view( template_name='registration/password_change_done.html', title=_('Password change successful'), ), name='password_change_done'), - path('password/reset/', user.CustomPasswordResetView.as_view(), - name='password_reset'), + 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', @@ -81,14 +77,10 @@ path('2fa/enable/', two_factor.TOTPEnableView.as_view(), name='enable_2fa'), path('2fa/refresh/', two_factor.TOTPRefreshView.as_view(), name='refresh_2fa'), path('2fa/disable/', two_factor.TOTPDisableView.as_view(), name='disable_2fa'), - path('2fa/webauthn/attest/', - two_factor.WebAuthnAttestationView.as_view(), name='webauthn_attest'), - path('2fa/webauthn/assert/', two_factor.WebAuthnAttestView.as_view(), - name='webauthn_assert'), - path('2fa/webauthn/delete/', - two_factor.WebAuthnDeleteView.as_view(), name='webauthn_delete'), - path('2fa/scratchcode/generate/', user.generate_scratch_codes, - name='generate_scratch_codes'), + path('2fa/webauthn/attest/', two_factor.WebAuthnAttestationView.as_view(), name='webauthn_attest'), + path('2fa/webauthn/assert/', two_factor.WebAuthnAttestView.as_view(), name='webauthn_assert'), + path('2fa/webauthn/delete/', two_factor.WebAuthnDeleteView.as_view(), name='webauthn_delete'), + path('2fa/scratchcode/generate/', user.generate_scratch_codes, name='generate_scratch_codes'), path('api/token/generate/', user.generate_api_token, name='generate_api_token'), path('api/token/remove/', user.remove_api_token, name='remove_api_token'), @@ -109,70 +101,51 @@ def paged_list_view(view, name): urlpatterns = [ - path('', blog.PostList.as_view(template_name='home.html', - title=_('Home')), kwargs={'page': 1}, name='home'), + path('', blog.PostList.as_view(template_name='home.html', title=_('Home')), kwargs={'page': 1}, name='home'), path('500/', exception), path('admin/', admin.site.urls), path('i18n/', include('django.conf.urls.i18n')), path('accounts/', include(register_patterns)), path('', include('social_django.urls')), - path('certificates/', - badge.open_certificate, name='open_certificate'), path('problems', include([ path('/', problem.ProblemList.as_view(), name='problem_list'), path('/random/', problem.RandomProblem.as_view(), name='problem_random'), - path('/suggest_list/', problem.SuggestList.as_view(), - name='problem_suggest_list'), + path('/suggest_list/', problem.SuggestList.as_view(), name='problem_suggest_list'), path('/suggest', problem.ProblemSuggest.as_view(), name='problem_suggest'), path('/create', problem.ProblemCreate.as_view(), name='problem_create'), - path('/import-polygon', problem.ProblemImportPolygon.as_view(), - name='problem_import_polygon'), + path('/import-polygon', problem.ProblemImportPolygon.as_view(), name='problem_import_polygon'), ])), path('problem/', include([ path('', problem.ProblemDetail.as_view(), name='problem_detail'), path('/edit', problem.ProblemEdit.as_view(), name='problem_edit'), - path('/editorial', problem.ProblemSolution.as_view(), - name='problem_editorial'), - path('/raw', xframe_options_sameorigin(problem.ProblemRaw.as_view()), - name='problem_raw'), + path('/editorial', problem.ProblemSolution.as_view(), name='problem_editorial'), + path('/raw', xframe_options_sameorigin(problem.ProblemRaw.as_view()), name='problem_raw'), path('/pdf', problem.ProblemPdfView.as_view(), name='problem_pdf'), - 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('/update-polygon', problem.ProblemUpdatePolygon.as_view(), - name='problem_update_polygon'), - - 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('/resubmit/', problem.ProblemSubmit.as_view(), name='problem_submit'), + path('/update-polygon', problem.ProblemUpdatePolygon.as_view(), name='problem_update_polygon'), + + 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('/', 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('/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('/tickets/', ticket.ProblemTicketListView.as_view(), name='problem_ticket_list'), + path('/tickets/new', ticket.NewProblemTicketView.as_view(), name='new_problem_ticket'), 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('', 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, @@ -194,20 +167,15 @@ def paged_list_view(view, name): path('tag/', include([ path('', tag.TagProblemDetail.as_view(), name='tagproblem_detail'), path('/assign', tag.TagProblemAssign.as_view(), name='tagproblem_assign'), - path('/', lambda _, tagproblem: HttpResponsePermanentRedirect( - reverse('tagproblem_detail', args=[tagproblem]))), + path('/', lambda _, tagproblem: HttpResponsePermanentRedirect(reverse('tagproblem_detail', args=[tagproblem]))), ])), path('submissions/', paged_list_view(submission.AllSubmissions, 'all_submissions')), - path('submissions/diff', submission.SubmissionSourceDiff, - name='diff_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('submissions/diff', submission.SubmissionSourceDiff, name='diff_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('src//download', submission.SubmissionSourceDownload.as_view(), name='submission_source_download'), @@ -223,7 +191,6 @@ def paged_list_view(view, name): path('find', user.user_ranking_redirect, name='user_ranking_redirect'), ])), - path('user', user.UserAboutPage.as_view(), name='user_page'), path('user', include([ path('', user.UserAboutPage.as_view(), name='user_page'), path('/request', badge.RequestAddBadge.as_view(), name='request_badge'), @@ -232,8 +199,7 @@ def paged_list_view(view, name): ])), 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('data/download/', user.UserDownloadData.as_view(), name='user_download_data'), path('user/', include([ path('', user.UserAboutPage.as_view(), name='user_page'), path('/ban', user.UserBan.as_view(), name='user_ban'), @@ -242,16 +208,13 @@ def paged_list_view(view, name): path('/comment/', paged_list_view(user.UserCommentPage, 'user_comment')), path('/solved/', include([ path('', user.UserProblemsPage.as_view(), name='user_problems'), - path('ajax', user.UserPerformancePointsAjax.as_view(), - name='user_pp_ajax'), + path('ajax', user.UserPerformancePointsAjax.as_view(), name='user_pp_ajax'), ])), - path('/submissions/', paged_list_view(submission.AllUserSubmissions, - 'all_user_submissions_old')), + 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_page', args=[user]))), + path('/', lambda _, user: HttpResponsePermanentRedirect(reverse('user_page', args=[user]))), ])), path('comments/upvote', comment.upvote_comment, name='comment_upvote'), @@ -259,52 +222,38 @@ def paged_list_view(view, name): 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('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/', paged_list_view(contests.ContestList, 'contest_list')), path('contests.ics', contests.ContestICal.as_view(), name='contest_ical'), - path('contests///', - contests.ContestCalendar.as_view(), name='contest_calendar'), + path('contests///', contests.ContestCalendar.as_view(), name='contest_calendar'), path('contests/new', contests.CreateContest.as_view(), name='contest_new'), 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('/ajax', contests.ContestTagDetailAjax.as_view(), name='contest_tag_ajax'), ])), path('contest/', include([ path('', contests.ContestDetail.as_view(), name='contest_view'), - path('/all', contests.ContestAllProblems.as_view(), - name='contest_all_problems'), + path('/all', contests.ContestAllProblems.as_view(), name='contest_all_problems'), path('/edit', contests.EditContest.as_view(), name='contest_edit'), path('/moss', contests.ContestMossView.as_view(), name='contest_moss'), - path('/moss/delete', contests.ContestMossDelete.as_view(), - name='contest_moss_delete'), - path('/announce', contests.ContestAnnounce.as_view(), - name='contest_announce'), + path('/moss/delete', contests.ContestMossDelete.as_view(), name='contest_moss_delete'), + path('/announce', contests.ContestAnnounce.as_view(), name='contest_announce'), path('/clone', contests.ContestClone.as_view(), name='contest_clone'), - path('/ranking/', contests.ContestRanking.as_view(), - name='contest_ranking'), - path('/public_ranking/', contests.ContestPublicRanking.as_view(), - name='contest_public_ranking'), - path('/official_ranking/', contests.ContestOfficialRanking.as_view(), - name='contest_official_ranking'), - path('/register', contests.ContestRegister.as_view(), - name='contest_register'), + path('/ranking/', contests.ContestRanking.as_view(), name='contest_ranking'), + path('/public_ranking/', contests.ContestPublicRanking.as_view(), name='contest_public_ranking'), + path('/official_ranking/', contests.ContestOfficialRanking.as_view(), name='contest_official_ranking'), + 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('/data/prepare/', contests.ContestPrepareData.as_view(), - name='contest_prepare_data'), - path('/data/download/', contests.ContestDownloadData.as_view(), - name='contest_download_data'), + path('/data/prepare/', contests.ContestPrepareData.as_view(), name='contest_prepare_data'), + path('/data/download/', contests.ContestDownloadData.as_view(), name='contest_download_data'), path('/rank//', paged_list_view(ranked_submission.ContestRankedSubmission, 'contest_ranked_submissions')), @@ -316,68 +265,48 @@ def paged_list_view(view, name): 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_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('/', lambda _, contest: HttpResponsePermanentRedirect(reverse('contest_view', args=[contest]))), ])), path('contributors/', include([ path('', user.ContribList.as_view(), name='contributors_list'), path('', lambda request, page: HttpResponsePermanentRedirect('%s?page=%s' % (reverse('contributors_list'), page))), - path('find', user.user_contributor_redirect, - name='user_contributor_redirect'), + path('find', user.user_contributor_redirect, name='user_contributor_redirect'), ])), - path('organizations/', organization.OrganizationList.as_view(), - name='organization_list'), - path('organizations/create', organization.CreateOrganization.as_view(), - name='organization_create'), + path('organizations/', organization.OrganizationList.as_view(), name='organization_list'), + path('organizations/create', organization.CreateOrganization.as_view(), name='organization_create'), path('organization/-', lambda _, pk, suffix: HttpResponsePermanentRedirect('/organization/%s' % suffix)), path('organization/', include([ - path('', organization.OrganizationHome.as_view(), - name='organization_home'), - 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('/usage', organization.MonthlyCreditUsageOrganization.as_view(), - name='organization_monthly_usage'), - path('/problems/', organization.ProblemListOrganization.as_view(), - name='problem_list_organization'), - path('/contests/', organization.ContestListOrganization.as_view(), - name='contest_list_organization'), + path('', organization.OrganizationHome.as_view(), name='organization_home'), + 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('/usage', organization.MonthlyCreditUsageOrganization.as_view(), name='organization_monthly_usage'), + path('/problems/', organization.ProblemListOrganization.as_view(), name='problem_list_organization'), + path('/contests/', organization.ContestListOrganization.as_view(), name='contest_list_organization'), path('/submissions/', paged_list_view(organization.SubmissionListOrganization, 'submission_list_organization')), - path('/problem-create', organization.ProblemCreateOrganization.as_view(), - name='problem_create_organization'), - path('/contest-create', organization.ContestCreateOrganization.as_view(), - name='contest_create_organization'), + path('/problem-create', organization.ProblemCreateOrganization.as_view(), name='problem_create_organization'), + path('/contest-create', organization.ContestCreateOrganization.as_view(), name='contest_create_organization'), - path('/request', organization.RequestJoinOrganization.as_view(), - name='request_organization'), + 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('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'), @@ -385,12 +314,10 @@ def paged_list_view(view, name): ])), path('/post/', include([ - path('new', organization.BlogPostCreateOrganization.as_view(), - name='blog_post_create_organization'), + path('new', organization.BlogPostCreateOrganization.as_view(), name='blog_post_create_organization'), ])), - path('/', lambda _, slug: HttpResponsePermanentRedirect( - reverse('organization_home', args=[slug]))), + path('/', lambda _, slug: HttpResponsePermanentRedirect(reverse('organization_home', args=[slug]))), ])), path('runtimes/', language.LanguageList.as_view(), name='runtime_list'), @@ -406,65 +333,45 @@ def paged_list_view(view, name): path('', blog.PostView.as_view(), name='blog_post'), path('/edit', blog.BlogPostEdit.as_view(), name='blog_post_edit'), path('/delete', blog.BlogPostDelete.as_view(), name='blog_post_delete'), - path('/', lambda _, id, - slug: HttpResponsePermanentRedirect(reverse('blog_post', args=[id, slug]))), + path('/', lambda _, id, slug: HttpResponsePermanentRedirect(reverse('blog_post', args=[id, slug]))), ])), path('license/', license.LicenseDetail.as_view(), name='license'), - path('mailgun/mail_activate/', - mailgun.MailgunActivationView.as_view(), name='mailgun_activate'), + 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('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('template', problem.LanguageTemplateAjax.as_view(), name='language_template_ajax'), path('select2/', include([ - path('user_search', UserSearchSelect2View.as_view(), - name='user_search_select2_ajax'), + 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('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('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('upload-image', martor_image_uploader, name='martor_image_uploader'), path('search-user', markdown_search_user, name='martor_search_user'), ])), ])), @@ -488,16 +395,11 @@ def paged_list_view(view, name): 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('/good', ticket.TicketStatusChangeView.as_view(contributive=True), - name='ticket_good'), - path('/norm', ticket.TicketStatusChangeView.as_view(contributive=False), - name='ticket_norm'), + 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('/good', ticket.TicketStatusChangeView.as_view(contributive=True), name='ticket_good'), + path('/norm', ticket.TicketStatusChangeView.as_view(contributive=False), name='ticket_norm'), path('/notes', ticket.TicketNotesEditView.as_view(), name='ticket_notes'), ])), @@ -507,8 +409,7 @@ def paged_list_view(view, name): path('profile/', UserSelect2View.as_view(), name='profile_select2'), path('organization_profile//', OrganizationUserSelect2View.as_view(), name='organization_profile_select2'), - path('organization/', OrganizationSelect2View.as_view(), - name='organization_select2'), + path('organization/', OrganizationSelect2View.as_view(), name='organization_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'), @@ -562,8 +463,7 @@ def paged_list_view(view, name): 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('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()), diff --git a/judge/admin/__init__.py b/judge/admin/__init__.py index 3136f112c..0f566fca1 100644 --- a/judge/admin/__init__.py +++ b/judge/admin/__init__.py @@ -6,13 +6,7 @@ from judge.admin.badge import BadgeRequestAdmin from judge.admin.comments import CommentAdmin from judge.admin.contest import ContestAdmin, ContestParticipationAdmin, ContestTagAdmin -from judge.admin.interface import ( - BlogPostAdmin, - FlatPageAdmin, - LicenseAdmin, - LogEntryAdmin, - NavigationBarAdmin, -) +from judge.admin.interface import BlogPostAdmin, FlatPageAdmin, LicenseAdmin, LogEntryAdmin, NavigationBarAdmin from judge.admin.organization import OrganizationAdmin, OrganizationRequestAdmin from judge.admin.problem import ProblemAdmin from judge.admin.profile import ProfileAdmin, UserAdmin @@ -21,12 +15,10 @@ from judge.admin.tag import TagAdmin, TagGroupAdmin, TagProblemAdmin from judge.admin.taxon import ProblemGroupAdmin, ProblemTypeAdmin from judge.admin.ticket import TicketAdmin -from judge.models import ( - Badge, BadgeRequest, BlogPost, Comment, CommentLock, Contest, - ContestParticipation, ContestTag, Judge, Language, License, MiscConfig, - NavigationBar, Organization, OrganizationRequest, Problem, ProblemGroup, - ProblemType, Profile, Submission, Tag, TagGroup, TagProblem, Ticket, -) +from judge.models import Badge, BadgeRequest, BlogPost, Comment, CommentLock, Contest, ContestParticipation, \ + ContestTag, Judge, Language, License, MiscConfig, NavigationBar, Organization, \ + OrganizationRequest, Problem, ProblemGroup, ProblemType, Profile, Submission, Tag, \ + TagGroup, TagProblem, Ticket admin.site.register(BlogPost, BlogPostAdmin) admin.site.register(Comment, CommentAdmin) diff --git a/judge/models/__init__.py b/judge/models/__init__.py index f1d7ce7be..795994f48 100644 --- a/judge/models/__init__.py +++ b/judge/models/__init__.py @@ -1,59 +1,18 @@ 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, - ContestAnnouncement, - ContestMoss, - ContestParticipation, - ContestProblem, - ContestSubmission, - ContestTag, - Rating, -) -from judge.models.interface import ( - BlogPost, - BlogVote, - MiscConfig, - NavigationBar, - validate_regex, -) -from judge.models.problem import ( - LanguageLimit, - License, - Problem, - ProblemClarification, - ProblemGroup, - 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 ( - Badge, BadgeRequest, Organization, OrganizationMonthlyUsage, - OrganizationRequest, Profile, WebAuthnCredential, -) +from judge.models.contest import Contest, ContestAnnouncement, ContestMoss, ContestParticipation, ContestProblem, \ + ContestSubmission, ContestTag, Rating +from judge.models.interface import BlogPost, BlogVote, MiscConfig, NavigationBar, validate_regex +from judge.models.problem import LanguageLimit, License, Problem, ProblemClarification, ProblemGroup, \ + 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 Badge, BadgeRequest, Organization, OrganizationMonthlyUsage, 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.tag import Tag, TagData, TagGroup, TagProblem from judge.models.ticket import GeneralIssue, Ticket, TicketMessage @@ -65,13 +24,9 @@ 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']) revisions.register(TagProblem) revisions.register(TagData, follow=['problem']) del revisions diff --git a/judge/models/profile.py b/judge/models/profile.py index 1e6a6edbc..bb9fa1e9e 100644 --- a/judge/models/profile.py +++ b/judge/models/profile.py @@ -11,7 +11,6 @@ from django.core.validators import FileExtensionValidator, RegexValidator from django.db import models from django.db.models import F, Max, Sum -from django.forms import ValidationError from django.urls import reverse from django.utils import timezone from django.utils.encoding import force_bytes @@ -28,13 +27,7 @@ from judge.utils.float_compare import float_compare_equal from judge.utils.two_factor import webauthn_decode -__all__ = [ - 'Organization', - 'OrganizationMonthlyUsage', - 'Profile', - 'OrganizationRequest', - 'WebAuthnCredential', -] +__all__ = ['Organization', 'OrganizationMonthlyUsage', 'Profile', 'OrganizationRequest', 'WebAuthnCredential'] class EncryptedNullCharField(EncryptedCharField): @@ -45,90 +38,40 @@ 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.'), - validators=[ - RegexValidator( - r'^[a-zA-Z]', _('Organization slugs must begin with a letter.'), - ), - ], - unique=True, - ) - short_name = models.CharField( - max_length=20, - verbose_name=_('short name'), - help_text=_('Displayed beside user name during contests.'), - ) + 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.'), + validators=[RegexValidator(r'^[a-zA-Z]', + _('Organization slugs must begin with a letter.'))], + unique=True) + 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=False, - ) - is_unlisted = models.BooleanField( - verbose_name=_('is unlisted organization?'), - help_text=_('Organization will not be listed'), - 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.', - ), - ) + 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=False) + is_unlisted = models.BooleanField(verbose_name=_('is unlisted organization?'), + help_text=_('Organization will not be listed'), 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.')) performance_points = models.FloatField(default=0) member_count = models.IntegerField(default=0) - current_consumed_credit = models.FloatField(default=0, help_text='Total used credit this month') - available_credit = models.FloatField(default=0, help_text='Available credits') - monthly_credit = models.FloatField(default=0, help_text='Total monthly free credit left') - _pp_table = [ - pow(settings.VNOJ_ORG_PP_STEP, i) for i in range(settings.VNOJ_ORG_PP_ENTRIES) - ] + _pp_table = [pow(settings.VNOJ_ORG_PP_STEP, i) for i in range(settings.VNOJ_ORG_PP_ENTRIES)] def calculate_points(self, table=_pp_table): - data = ( - self.members.get_queryset() - .order_by('-performance_points') - .values_list('performance_points', flat=True) - .filter(performance_points__gt=0) - ) - pp = settings.VNOJ_ORG_PP_SCALE * sum( - ratio * pp for ratio, pp in zip(table, data) - ) + data = self.members.get_queryset().order_by('-performance_points') \ + .values_list('performance_points', flat=True).filter(performance_points__gt=0) + pp = settings.VNOJ_ORG_PP_SCALE * sum(ratio * pp for ratio, pp in zip(table, data)) if not float_compare_equal(self.performance_points, pp): self.performance_points = pp self.save(update_fields=['performance_points']) @@ -156,9 +99,7 @@ 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 @@ -169,23 +110,6 @@ def get_absolute_url(self): def get_users_url(self): return reverse('organization_users', args=[self.slug]) - def has_credit_left(self): - return self.current_consumed_credit < self.available_credit + self.monthly_credit - - def consume_credit(self, consumed): - # reduce credit in monthly credit first - # then reduce the left to available credit - if self.monthly_credit >= consumed: - self.monthly_credit -= consumed - else: - consumed -= self.monthly_credit - self.monthly_credit = 0 - # if available credit can be negative if we don't enable the monthly credit limitation - self.available_credit -= consumed - - self.current_consumed_credit += consumed - self.save(update_fields=['monthly_credit', 'available_credit', 'current_consumed_credit']) - class Meta: ordering = ['name'] permissions = ( @@ -199,15 +123,10 @@ class Meta: class OrganizationMonthlyUsage(models.Model): - organization = models.ForeignKey( - Organization, - verbose_name=_('organization'), - related_name='monthly_usages', - on_delete=models.CASCADE, - ) + organization = models.ForeignKey(Organization, verbose_name=_('organization'), related_name='monthly_usages', + on_delete=models.CASCADE) time = models.DateField(verbose_name=_('time')) - consumed_credit = models.FloatField( - verbose_name=_('consumed credit'), default=0) + consumed_credit = models.FloatField(verbose_name=_('consumed credit'), default=0) class Meta: verbose_name = _('organization monthly usage') @@ -218,180 +137,78 @@ class Meta: class Badge(models.Model): name = models.CharField(max_length=128, verbose_name=_('badge name')) mini = models.URLField(verbose_name=_('mini badge URL'), blank=True) - full_size = models.URLField(verbose_name=_( - 'full size badge URL'), blank=True) + full_size = models.URLField(verbose_name=_('full size badge URL'), blank=True) def __str__(self): return self.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) contribution_points = models.IntegerField(default=0) vnoj_points = models.IntegerField(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='light', - ) - last_access = models.DateTimeField( - verbose_name=_('last access time'), default=now) - ip = models.GenericIPAddressField( - verbose_name=_('last IP'), blank=True, null=True) - ip_auth = models.GenericIPAddressField( - verbose_name=_('IP-based authentication'), unique=True, blank=True, null=True, - ) - badges = models.ManyToManyField( - Badge, verbose_name=_('badges'), blank=True, related_name='users', - ) - display_badge = models.ForeignKey( - Badge, verbose_name=_('display badge'), null=True, on_delete=models.SET_NULL, - ) - 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=settings.VNOJ_DISPLAY_RANKS, - ) - 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, - ) - ban_reason = models.TextField( - null=True, blank=True, help_text=_('Show to banned user in login page.'), - ) - allow_tagging = models.BooleanField( - verbose_name=_('Allow tagging'), - help_text=_('User will be allowed to tag problems.'), - default=True, - ) + 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='light') + last_access = models.DateTimeField(verbose_name=_('last access time'), default=now) + ip = models.GenericIPAddressField(verbose_name=_('last IP'), blank=True, null=True) + ip_auth = models.GenericIPAddressField(verbose_name=_('IP-based authentication'), + unique=True, blank=True, null=True) + badges = models.ManyToManyField(Badge, verbose_name=_('badges'), blank=True, related_name='users') + display_badge = models.ForeignKey(Badge, verbose_name=_('display badge'), null=True, on_delete=models.SET_NULL) + 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=settings.VNOJ_DISPLAY_RANKS) + 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) + ban_reason = models.TextField(null=True, blank=True, + help_text=_('Show to banned user in login page.')) + allow_tagging = models.BooleanField(verbose_name=_('Allow tagging'), + help_text=_('User will be allowed to tag problems.'), + default=True) 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): @@ -422,16 +239,14 @@ def is_banned(self): return not self.user.is_active and self.ban_reason is not None def can_be_banned_by(self, staff): - return (self.user != staff and not self.user.is_superuser and staff.has_perm('judge.ban_user')) + return self.user != staff and not self.user.is_superuser and staff.has_perm('judge.ban_user') @cached_property def can_tag_problems(self): if self.allow_tagging: if self.user.has_perm('judge.add_tagproblem'): return True - if ( - self.rating is not None and self.rating >= settings.VNOJ_TAG_PROBLEM_MIN_RATING - ): + if self.rating is not None and self.rating >= settings.VNOJ_TAG_PROBLEM_MIN_RATING: return True return False @@ -448,50 +263,33 @@ def resolved_ace_theme(self): @cached_property def registered_contest_ids(self): - return set( - self.contest_history.filter( - virtual=0).values_list('contest_id', flat=True), - ) + return set(self.contest_history.filter(virtual=0).values_list('contest_id', flat=True)) - _pp_table = [pow(settings.DMOJ_PP_STEP, i) - for i in range(settings.DMOJ_PP_ENTRIES)] + _pp_table = [pow(settings.DMOJ_PP_STEP, i) for i in range(settings.DMOJ_PP_ENTRIES)] 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) 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(x * y for x, y in zip(table, data)) + bonus_function(problems) - if ( - not float_compare_equal(self.points, points) or - problems != self.problem_count or not - float_compare_equal(self.performance_points, pp) - ): + if not float_compare_equal(self.points, points) or \ + problems != self.problem_count or \ + not float_compare_equal(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']) for org in self.organizations.get_queryset(): org.calculate_points() return points @@ -500,32 +298,20 @@ def calculate_points(self, table=_pp_table): def calculate_contribution_points(self): from judge.models import BlogPost, Comment, Ticket - old_pp = self.contribution_points # Because the aggregate function can return None # So we use `X or 0` to get 0 if X is None # Please note that `0 or X` will return None if X is None - total_comment_scores = ( - Comment.objects.filter(author=self.id, hidden=False).aggregate( - sum=Sum('score'), - )['sum'] or 0 - ) - total_blog_scores = ( - BlogPost.objects.filter( - authors=self.id, visible=True, organization=None, - ).aggregate(sum=Sum('score'))['sum'] or 0 - ) - count_good_tickets = Ticket.objects.filter( - user=self.id, is_contributive=True, - ).count() - count_suggested_problem = self.suggested_problems.filter( - is_public=True).count() - new_pp = ( - (total_comment_scores + total_blog_scores) * - settings.VNOJ_CP_COMMENT + - count_good_tickets * settings.VNOJ_CP_TICKET + + total_comment_scores = Comment.objects.filter(author=self.id, hidden=False) \ + .aggregate(sum=Sum('score'))['sum'] or 0 + total_blog_scores = BlogPost.objects.filter(authors=self.id, visible=True, organization=None) \ + .aggregate(sum=Sum('score'))['sum'] or 0 + count_good_tickets = Ticket.objects.filter(user=self.id, is_contributive=True) \ + .count() + count_suggested_problem = self.suggested_problems.filter(is_public=True).count() + new_pp = (total_comment_scores + total_blog_scores) * settings.VNOJ_CP_COMMENT + \ + count_good_tickets * settings.VNOJ_CP_TICKET + \ count_suggested_problem * settings.VNOJ_CP_PROBLEM - ) if new_pp != old_pp: self.contribution_points = new_pp self.save(update_fields=['contribution_points']) @@ -544,25 +330,17 @@ def update_contribution_points(self, delta): 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.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)) + 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']) return codes @@ -577,9 +355,7 @@ 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 @@ -587,13 +363,8 @@ 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']) @@ -615,8 +386,7 @@ def ban_user(self, reason): def unban_user(self): self.ban_reason = None - self.display_rank = Profile._meta.get_field( - 'display_rank').get_default() + self.display_rank = Profile._meta.get_field('display_rank').get_default() self.is_unlisted = False self.save(update_fields=['ban_reason', 'display_rank', 'is_unlisted']) @@ -632,14 +402,9 @@ 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 @@ -648,11 +413,7 @@ 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 = ( @@ -661,10 +422,7 @@ class Meta: ('can_upload_image', _('Can upload image directly to server via martor')), ('high_problem_timelimit', _('Can set high problem timelimit')), ('long_contest_duration', _('Can set long contest duration')), - ( - 'create_mass_testcases', - _('Can create unlimitted number of testcases for a problem'), - ), + ('create_mass_testcases', _('Can create unlimitted number of testcases for a problem')), ('ban_user', _('Ban users')), ) verbose_name = _('user profile') @@ -679,16 +437,10 @@ class Meta: class WebAuthnCredential(models.Model): - user = models.ForeignKey( - Profile, - verbose_name=_('user'), - related_name='webauthn_credentials', - on_delete=models.CASCADE, - ) + 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, - ) + 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')) @@ -716,29 +468,15 @@ class Meta: 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')), - ), - ) + 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')), + )) reason = models.TextField(verbose_name=_('reason')) class Meta: @@ -746,11 +484,6 @@ class Meta: verbose_name_plural = _('organization join requests') -def validate_pdf(file): - if not file.name.endswith('.pdf'): - raise ValidationError('Only PDF files are allowed.') - - class BadgeRequest(models.Model): user = models.ForeignKey( Profile, From 2fd8f293f98beffb5127686dab53857ce2714fb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Ho=C3=A0ng=20Ph=C3=BAc?= Date: Sat, 21 Sep 2024 09:53:37 +0700 Subject: [PATCH 12/13] FIx lint problems --- dmoj/urls.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dmoj/urls.py b/dmoj/urls.py index d0a8b41e5..6aab40fcc 100644 --- a/dmoj/urls.py +++ b/dmoj/urls.py @@ -15,9 +15,9 @@ from judge.feed import AtomBlogFeed, AtomCommentFeed, AtomProblemFeed, BlogFeed, CommentFeed, ProblemFeed from judge.sitemap import sitemaps -from judge.views import TitledTemplateView, api, badge, blog, comment, contests, language, license, mailgun, organization, \ - preview, problem, problem_manage, ranked_submission, register, stats, status, submission, tag, tasks, ticket, \ - two_factor, user, widgets +from judge.views import TitledTemplateView, api, badge, blog, comment, contests, language, license, mailgun, \ + organization, preview, problem, problem_manage, ranked_submission, register, stats, status, submission, tag, \ + tasks, ticket, two_factor, user, widgets from judge.views.magazine import MagazinePage from judge.views.problem_data import ProblemDataView, ProblemSubmissionDiff, \ problem_data_file, problem_init_view From 3f6475e366e6c6b4d962491a513b1ba45536c012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Ho=C3=A0ng=20Ph=C3=BAc?= Date: Sat, 21 Sep 2024 10:03:48 +0700 Subject: [PATCH 13/13] Redo organization class --- judge/models/profile.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/judge/models/profile.py b/judge/models/profile.py index bb9fa1e9e..6406897e5 100644 --- a/judge/models/profile.py +++ b/judge/models/profile.py @@ -65,6 +65,9 @@ class Organization(models.Model): 'viewing the organization.')) performance_points = models.FloatField(default=0) member_count = models.IntegerField(default=0) + current_consumed_credit = models.FloatField(default=0, help_text='Total used credit this month') + available_credit = models.FloatField(default=0, help_text='Available credits') + monthly_credit = models.FloatField(default=0, help_text='Total monthly free credit left') _pp_table = [pow(settings.VNOJ_ORG_PP_STEP, i) for i in range(settings.VNOJ_ORG_PP_ENTRIES)] @@ -110,6 +113,23 @@ def get_absolute_url(self): def get_users_url(self): return reverse('organization_users', args=[self.slug]) + def has_credit_left(self): + return self.current_consumed_credit < self.available_credit + self.monthly_credit + + def consume_credit(self, consumed): + # reduce credit in monthly credit first + # then reduce the left to available credit + if self.monthly_credit >= consumed: + self.monthly_credit -= consumed + else: + consumed -= self.monthly_credit + self.monthly_credit = 0 + # if available credit can be negative if we don't enable the monthly credit limitation + self.available_credit -= consumed + + self.current_consumed_credit += consumed + self.save(update_fields=['monthly_credit', 'available_credit', 'current_consumed_credit']) + class Meta: ordering = ['name'] permissions = (