NDOJ/judge/forms.py

294 lines
9.3 KiB
Python
Raw Normal View History

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
2022-05-30 06:59:53 +00:00
from django.core.exceptions import ValidationError, ObjectDoesNotExist
2020-01-21 06:35:58 +00:00
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 _
2022-05-30 06:59:53 +00:00
from django.utils import timezone
2020-01-21 06:35:58 +00:00
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,
2022-05-30 06:59:53 +00:00
BlogPost,
2022-05-14 17:57:27 +00:00
)
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-30 06:59:53 +00:00
_("You may not be part of more than {count} public groups.").format(
count=max_orgs
)
2022-05-14 17:57:27 +00:00
)
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-30 06:59:53 +00:00
fields = ["about", "logo_override_image", "admins", "is_open"]
2022-05-14 17:57:27 +00:00
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
2022-05-30 06:59:53 +00:00
class AddOrganizationMemberForm(ModelForm):
new_users = CharField(
max_length=65536,
widget=forms.Textarea,
help_text=_("Enter usernames separating by space"),
2022-05-30 07:07:09 +00:00
label=_("New users"),
2022-05-30 06:59:53 +00:00
)
def clean(self):
new_users = self.cleaned_data.get("new_users") or ""
usernames = new_users.split()
invalid_usernames = []
valid_usernames = []
for username in usernames:
try:
valid_usernames.append(Profile.objects.get(user__username=username))
except ObjectDoesNotExist:
invalid_usernames.append(username)
if invalid_usernames:
raise ValidationError(
_("These usernames don't exist: {usernames}").format(
usernames=str(invalid_usernames)
)
)
self.cleaned_data["new_users"] = valid_usernames
return self.cleaned_data
class Meta:
model = Organization
fields = ()
class OrganizationBlogForm(ModelForm):
class Meta:
model = BlogPost
fields = ("title", "content", "publish_on")
widgets = {
"publish_on": forms.HiddenInput,
}
if HeavyPreviewPageDownWidget is not None:
widgets["content"] = HeavyPreviewPageDownWidget(
preview=reverse_lazy("organization_preview")
)
def __init__(self, *args, **kwargs):
super(OrganizationBlogForm, self).__init__(*args, **kwargs)
self.fields["publish_on"].required = False
self.fields["publish_on"].is_hidden = True
def clean(self):
self.cleaned_data["publish_on"] = timezone.now()
return self.cleaned_data
class OrganizationAdminBlogForm(OrganizationBlogForm):
class Meta:
model = BlogPost
fields = ("visible", "sticky", "title", "content", "publish_on")
widgets = {
"publish_on": forms.HiddenInput,
}
if HeavyPreviewPageDownWidget is not None:
widgets["content"] = 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"]