NDOJ/judge/forms.py

517 lines
16 KiB
Python

import os
import secrets
from operator import attrgetter
import pyotp
from django import forms
from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.auth.forms import AuthenticationForm
from django.core.exceptions import ValidationError, ObjectDoesNotExist
from django.core.validators import RegexValidator
from django.db import transaction
from django.db.models import Q
from django.forms import (
CharField,
ChoiceField,
Form,
ModelForm,
formset_factory,
BaseModelFormSet,
FileField,
)
from django.urls import reverse_lazy, reverse
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
from django_ace import AceWidget
from judge.models import (
Contest,
Language,
Organization,
PrivateMessage,
Problem,
ProblemPointsVote,
Profile,
Submission,
BlogPost,
ContestProblem,
)
from judge.widgets import (
HeavyPreviewPageDownWidget,
MathJaxPagedownWidget,
PagedownWidget,
Select2MultipleWidget,
Select2Widget,
HeavySelect2MultipleWidget,
HeavySelect2Widget,
Select2MultipleWidget,
DateTimePickerWidget,
)
from judge.tasks import rescore_contest
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 UserForm(ModelForm):
class Meta:
model = User
fields = [
"first_name",
"last_name",
]
class ProfileForm(ModelForm):
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 groups.").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()),
)
def file_size_validator(file):
limit = 1 * 1024 * 1024
if file.size > limit:
raise ValidationError("File too large. Size should not exceed 1MB.")
class ProblemSubmitForm(ModelForm):
source = CharField(
max_length=65536, widget=AceWidget(theme="twilight", no_ace_media=True)
)
judge = ChoiceField(choices=(), widget=forms.HiddenInput(), required=False)
source_file = FileField(required=False, validators=[file_size_validator])
def __init__(self, *args, judge_choices=(), request=None, **kwargs):
super(ProblemSubmitForm, self).__init__(*args, **kwargs)
self.source_file_name = None
self.request = request
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
def clean(self):
if "source_file" in self.files:
if self.cleaned_data["language"].key == "OUTPUT" and self.files[
"source_file"
].name.endswith(".zip"):
self.source_file_name = secrets.token_hex(16) + ".zip"
filepath = os.path.join(
settings.DMOJ_SUBMISSION_ROOT, self.source_file_name
)
with open(filepath, "wb+") as destination:
for chunk in self.files["source_file"].chunks():
destination.write(chunk)
self.cleaned_data["source"] = self.request.build_absolute_uri(
reverse("submission_source_file", args=(self.source_file_name,))
)
del self.files["source_file"]
return self.cleaned_data
class Meta:
model = Submission
fields = ["language"]
class EditOrganizationForm(ModelForm):
class Meta:
model = Organization
fields = [
"name",
"slug",
"short_name",
"about",
"logo_override_image",
"admins",
"is_open",
]
widgets = {"admins": Select2MultipleWidget()}
if HeavyPreviewPageDownWidget is not None:
widgets["about"] = HeavyPreviewPageDownWidget(
preview=reverse_lazy("organization_preview")
)
class AddOrganizationForm(ModelForm):
class Meta:
model = Organization
fields = [
"name",
"slug",
"short_name",
"about",
"logo_override_image",
"is_open",
]
widgets = {}
if HeavyPreviewPageDownWidget is not None:
widgets["about"] = HeavyPreviewPageDownWidget(
preview=reverse_lazy("organization_preview")
)
def __init__(self, *args, **kwargs):
self.request = kwargs.pop("request", None)
super(AddOrganizationForm, self).__init__(*args, **kwargs)
def save(self, commit=True):
res = super(AddOrganizationForm, self).save(commit=False)
res.registrant = self.request.profile
if commit:
res.save()
return res
class AddOrganizationContestForm(ModelForm):
def __init__(self, *args, **kwargs):
self.request = kwargs.pop("request", None)
super(AddOrganizationContestForm, self).__init__(*args, **kwargs)
def save(self, commit=True):
contest = super(AddOrganizationContestForm, self).save(commit=False)
old_save_m2m = self.save_m2m
def save_m2m():
for i, problem in enumerate(self.cleaned_data["problems"]):
contest_problem = ContestProblem(
contest=contest, problem=problem, points=100, order=i + 1
)
contest_problem.save()
contest.contest_problems.add(contest_problem)
old_save_m2m()
self.save_m2m = save_m2m
contest.save()
self.save_m2m()
return contest
class Meta:
model = Contest
fields = (
"key",
"name",
"start_time",
"end_time",
"problems",
)
widgets = {
"start_time": DateTimePickerWidget(),
"end_time": DateTimePickerWidget(),
"problems": HeavySelect2MultipleWidget(data_view="problem_select2"),
}
class EditOrganizationContestForm(ModelForm):
def __init__(self, *args, **kwargs):
self.org_id = kwargs.pop("org_id", 0)
super(EditOrganizationContestForm, self).__init__(*args, **kwargs)
for field in [
"authors",
"curators",
"testers",
"private_contestants",
"banned_users",
"view_contest_scoreboard",
]:
self.fields[field].widget.data_url = (
self.fields[field].widget.get_url() + "?org_id=1"
)
def save(self, commit=True):
res = super(EditOrganizationContestForm, self).save(commit=False)
if commit:
res.save()
transaction.on_commit(rescore_contest.s(res.key).delay)
return res
class Meta:
model = Contest
fields = (
"is_visible",
"key",
"name",
"start_time",
"end_time",
"format_name",
"authors",
"curators",
"testers",
"time_limit",
"freeze_after",
"use_clarifications",
"hide_problem_tags",
"scoreboard_visibility",
"run_pretests_only",
"points_precision",
"description",
"og_image",
"logo_override_image",
"summary",
"access_code",
"private_contestants",
"view_contest_scoreboard",
"banned_users",
)
widgets = {
"authors": HeavySelect2MultipleWidget(data_view="profile_select2"),
"curators": HeavySelect2MultipleWidget(data_view="profile_select2"),
"testers": HeavySelect2MultipleWidget(data_view="profile_select2"),
"private_contestants": HeavySelect2MultipleWidget(
data_view="profile_select2"
),
"banned_users": HeavySelect2MultipleWidget(data_view="profile_select2"),
"view_contest_scoreboard": HeavySelect2MultipleWidget(
data_view="profile_select2"
),
"organizations": HeavySelect2MultipleWidget(
data_view="organization_select2"
),
"tags": Select2MultipleWidget,
"description": HeavyPreviewPageDownWidget(
preview=reverse_lazy("contest_preview")
),
"start_time": DateTimePickerWidget(),
"end_time": DateTimePickerWidget(),
"format_name": Select2Widget(),
"scoreboard_visibility": Select2Widget(),
}
class AddOrganizationMemberForm(ModelForm):
new_users = CharField(
max_length=65536,
widget=forms.Textarea,
help_text=_("Enter usernames separating by space"),
label=_("New users"),
)
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")
)
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"]
class ContestProblemForm(ModelForm):
class Meta:
model = ContestProblem
fields = (
"order",
"problem",
"points",
"partial",
"output_prefix_override",
"max_submissions",
)
widgets = {
"problem": HeavySelect2Widget(
data_view="problem_select2", attrs={"style": "width:100%"}
),
}
class ContestProblemFormSet(
formset_factory(
ContestProblemForm, formset=BaseModelFormSet, extra=6, can_delete=True
)
):
model = ContestProblem