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