from operator import attrgetter import pyotp from django import forms from django.conf import settings from django.contrib.auth.forms import AuthenticationForm from django.core.exceptions import ValidationError from django.core.validators import RegexValidator from django.db.models import Q from django.forms import CharField, ChoiceField, Form, ModelForm from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ from django_ace import AceWidget from judge.models import ( Contest, Language, Organization, PrivateMessage, Problem, ProblemPointsVote, Profile, Submission, ) from judge.utils.subscription import newsletter_id from judge.widgets import ( HeavyPreviewPageDownWidget, MathJaxPagedownWidget, PagedownWidget, Select2MultipleWidget, Select2Widget, ) def fix_unicode(string, unsafe=tuple("\u202a\u202b\u202d\u202e")): return ( string + (sum(k in unsafe for k in string) - string.count("\u202c")) * "\u202c" ) class ProfileForm(ModelForm): if newsletter_id is not None: newsletter = forms.BooleanField( label=_("Subscribe to contest updates"), initial=False, required=False ) test_site = forms.BooleanField( label=_("Enable experimental features"), initial=False, required=False ) class Meta: model = Profile fields = [ "about", "organizations", "timezone", "language", "ace_theme", "user_script", ] widgets = { "user_script": AceWidget(theme="github"), "timezone": Select2Widget(attrs={"style": "width:200px"}), "language": Select2Widget(attrs={"style": "width:200px"}), "ace_theme": Select2Widget(attrs={"style": "width:200px"}), } has_math_config = bool(settings.MATHOID_URL) if has_math_config: fields.append("math_engine") widgets["math_engine"] = Select2Widget(attrs={"style": "width:200px"}) if HeavyPreviewPageDownWidget is not None: widgets["about"] = HeavyPreviewPageDownWidget( preview=reverse_lazy("profile_preview"), attrs={"style": "max-width:700px;min-width:700px;width:700px"}, ) def clean(self): organizations = self.cleaned_data.get("organizations") or [] max_orgs = settings.DMOJ_USER_MAX_ORGANIZATION_COUNT if sum(org.is_open for org in organizations) > max_orgs: raise ValidationError( _( "You may not be part of more than {count} public organizations." ).format(count=max_orgs) ) return self.cleaned_data def __init__(self, *args, **kwargs): user = kwargs.pop("user", None) super(ProfileForm, self).__init__(*args, **kwargs) if not user.has_perm("judge.edit_all_organization"): self.fields["organizations"].queryset = Organization.objects.filter( Q(is_open=True) | Q(id__in=user.profile.organizations.all()), ) class ProblemSubmitForm(ModelForm): source = CharField( max_length=65536, widget=AceWidget(theme="twilight", no_ace_media=True) ) judge = ChoiceField(choices=(), widget=forms.HiddenInput(), required=False) def __init__(self, *args, judge_choices=(), **kwargs): super(ProblemSubmitForm, self).__init__(*args, **kwargs) self.fields["language"].empty_label = None self.fields["language"].label_from_instance = attrgetter("display_name") self.fields["language"].queryset = Language.objects.filter( judges__online=True ).distinct() if judge_choices: self.fields["judge"].widget = Select2Widget( attrs={"style": "width: 150px", "data-placeholder": _("Any judge")}, ) self.fields["judge"].choices = judge_choices class Meta: model = Submission fields = ["language"] class EditOrganizationForm(ModelForm): class Meta: model = Organization fields = ["about", "logo_override_image", "admins"] widgets = {"admins": Select2MultipleWidget()} if HeavyPreviewPageDownWidget is not None: widgets["about"] = HeavyPreviewPageDownWidget( preview=reverse_lazy("organization_preview") ) class NewMessageForm(ModelForm): class Meta: model = PrivateMessage fields = ["title", "content"] widgets = {} if PagedownWidget is not None: widgets["content"] = MathJaxPagedownWidget() class CustomAuthenticationForm(AuthenticationForm): def __init__(self, *args, **kwargs): super(CustomAuthenticationForm, self).__init__(*args, **kwargs) self.fields["username"].widget.attrs.update({"placeholder": _("Username")}) self.fields["password"].widget.attrs.update({"placeholder": _("Password")}) self.has_google_auth = self._has_social_auth("GOOGLE_OAUTH2") self.has_facebook_auth = self._has_social_auth("FACEBOOK") self.has_github_auth = self._has_social_auth("GITHUB_SECURE") def _has_social_auth(self, key): return getattr(settings, "SOCIAL_AUTH_%s_KEY" % key, None) and getattr( settings, "SOCIAL_AUTH_%s_SECRET" % key, None ) class NoAutoCompleteCharField(forms.CharField): def widget_attrs(self, widget): attrs = super(NoAutoCompleteCharField, self).widget_attrs(widget) attrs["autocomplete"] = "off" return attrs class TOTPForm(Form): TOLERANCE = settings.DMOJ_TOTP_TOLERANCE_HALF_MINUTES totp_token = NoAutoCompleteCharField( validators=[ RegexValidator( "^[0-9]{6}$", _("Two Factor Authentication tokens must be 6 decimal digits."), ), ] ) def __init__(self, *args, **kwargs): self.totp_key = kwargs.pop("totp_key") super(TOTPForm, self).__init__(*args, **kwargs) def clean_totp_token(self): if not pyotp.TOTP(self.totp_key).verify( self.cleaned_data["totp_token"], valid_window=self.TOLERANCE ): raise ValidationError(_("Invalid Two Factor Authentication token.")) class ProblemCloneForm(Form): code = CharField( max_length=20, validators=[ RegexValidator("^[a-z0-9]+$", _("Problem code must be ^[a-z0-9]+$")) ], ) def clean_code(self): code = self.cleaned_data["code"] if Problem.objects.filter(code=code).exists(): raise ValidationError(_("Problem with code already exists.")) return code class ContestCloneForm(Form): key = CharField( max_length=20, validators=[RegexValidator("^[a-z0-9]+$", _("Contest id must be ^[a-z0-9]+$"))], ) def clean_key(self): key = self.cleaned_data["key"] if Contest.objects.filter(key=key).exists(): raise ValidationError(_("Contest with key already exists.")) return key class ProblemPointsVoteForm(ModelForm): class Meta: model = ProblemPointsVote fields = ["points"]