NDOJ/judge/forms.py

600 lines
18 KiB
Python
Raw Permalink Normal View History

2023-03-10 04:31:55 +00:00
import os
import secrets
2020-01-21 06:35:58 +00:00
from operator import attrgetter
import pyotp
2023-04-05 03:41:04 +00:00
import time
import datetime
2023-03-10 04:31:55 +00:00
2020-01-21 06:35:58 +00:00
from django import forms
from django.conf import settings
2022-10-15 16:23:50 +00:00
from django.contrib.auth.models import User
2020-01-21 06:35:58 +00:00
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
from django.forms import (
CharField,
ChoiceField,
Form,
ModelForm,
formset_factory,
BaseModelFormSet,
2023-03-10 04:31:55 +00:00
FileField,
)
2023-03-10 04:31:55 +00:00
from django.urls import reverse_lazy, reverse
2020-01-21 06:35:58 +00:00
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,
2024-01-08 18:27:20 +00:00
TestFormatterModel,
2022-05-14 17:57:27 +00:00
Organization,
PrivateMessage,
Problem,
ProblemPointsVote,
Profile,
Submission,
2022-05-30 06:59:53 +00:00
BlogPost,
ContestProblem,
2024-01-08 18:27:20 +00:00
TestFormatterModel,
2024-04-27 03:51:16 +00:00
ProfileInfo,
2022-05-14 17:57:27 +00:00
)
2022-05-14 17:57:27 +00:00
from judge.widgets import (
HeavyPreviewPageDownWidget,
PagedownWidget,
Select2MultipleWidget,
Select2Widget,
HeavySelect2MultipleWidget,
HeavySelect2Widget,
Select2MultipleWidget,
DateTimePickerWidget,
2023-08-24 03:14:09 +00:00
ImageWidget,
2024-04-27 03:51:16 +00:00
DatePickerWidget,
2022-05-14 17:57:27 +00:00
)
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
2022-10-15 17:11:20 +00:00
2022-10-15 16:23:50 +00:00
class UserForm(ModelForm):
class Meta:
model = User
fields = [
"first_name",
2022-10-15 17:28:33 +00:00
"last_name",
2022-10-15 16:23:50 +00:00
]
2020-01-21 06:35:58 +00:00
2022-10-15 17:11:20 +00:00
2024-04-27 03:51:16 +00:00
class ProfileInfoForm(ModelForm):
class Meta:
model = ProfileInfo
fields = ["tshirt_size", "date_of_birth", "address"]
widgets = {
"tshirt_size": Select2Widget(attrs={"style": "width:100%"}),
"date_of_birth": DatePickerWidget,
"address": forms.TextInput(attrs={"style": "width:100%"}),
}
2020-01-21 06:35:58 +00:00
class ProfileForm(ModelForm):
class Meta:
model = Profile
2022-05-14 17:57:27 +00:00
fields = [
"about",
"timezone",
"language",
"ace_theme",
2023-08-24 03:14:09 +00:00
"profile_image",
2023-09-02 00:42:58 +00:00
"css_background",
2022-05-14 17:57:27 +00:00
]
2020-01-21 06:35:58 +00:00
widgets = {
2022-05-14 17:57:27 +00:00
"timezone": Select2Widget(attrs={"style": "width:200px"}),
"language": Select2Widget(attrs={"style": "width:200px"}),
"ace_theme": Select2Widget(attrs={"style": "width:200px"}),
2023-08-24 03:14:09 +00:00
"profile_image": ImageWidget,
2023-09-02 00:42:58 +00:00
"css_background": forms.TextInput(),
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 __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)
2023-08-24 03:14:09 +00:00
self.fields["profile_image"].required = False
def clean_profile_image(self):
profile_image = self.cleaned_data.get("profile_image")
if profile_image:
if profile_image.size > 5 * 1024 * 1024:
raise ValidationError(
_("File size exceeds the maximum allowed limit of 5MB.")
)
return profile_image
2020-01-21 06:35:58 +00:00
2023-03-10 04:31:55 +00:00
def file_size_validator(file):
2023-08-24 03:14:09 +00:00
limit = 10 * 1024 * 1024
2023-03-10 04:31:55 +00:00
if file.size > limit:
2023-08-24 03:14:09 +00:00
raise ValidationError("File too large. Size should not exceed 10MB.")
2023-03-10 04:31:55 +00:00
2020-01-21 06:35:58 +00:00
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)
2023-03-10 04:31:55 +00:00
source_file = FileField(required=False, validators=[file_size_validator])
2020-01-21 06:35:58 +00:00
2023-03-26 08:00:05 +00:00
def __init__(self, *args, judge_choices=(), request=None, problem=None, **kwargs):
2020-01-21 06:35:58 +00:00
super(ProblemSubmitForm, self).__init__(*args, **kwargs)
2023-03-10 04:31:55 +00:00
self.source_file_name = None
self.request = request
2023-03-26 08:00:05 +00:00
self.problem = problem
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
2023-04-05 03:41:04 +00:00
def allow_url_as_source(self):
key = self.cleaned_data["language"].key
filename = self.files["source_file"].name
if key == "OUTPUT" and self.problem.data_files.output_only:
return filename.endswith(".zip")
if key == "SCAT":
return filename.endswith(".sb3")
return False
2023-03-10 04:31:55 +00:00
def clean(self):
if "source_file" in self.files:
2023-04-05 03:41:04 +00:00
if self.allow_url_as_source():
2023-03-26 03:51:43 +00:00
filename = self.files["source_file"].name
2023-04-05 03:41:04 +00:00
now = datetime.datetime.now()
timestamp = str(int(time.mktime(now.timetuple())))
self.source_file_name = (
timestamp + secrets.token_hex(5) + "." + filename.split(".")[-1]
)
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,))
)
2023-03-10 04:31:55 +00:00
del self.files["source_file"]
return self.cleaned_data
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
fields = [
"name",
"slug",
"short_name",
"about",
"organization_image",
"admins",
"is_open",
]
widgets = {
"admins": Select2MultipleWidget(),
"organization_image": ImageWidget,
}
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")
)
2022-10-04 03:33:16 +00:00
def __init__(self, *args, **kwargs):
super(EditOrganizationForm, self).__init__(*args, **kwargs)
self.fields["organization_image"].required = False
def clean_organization_image(self):
organization_image = self.cleaned_data.get("organization_image")
if organization_image:
if organization_image.size > 5 * 1024 * 1024:
raise ValidationError(
_("File size exceeds the maximum allowed limit of 5MB.")
)
return organization_image
2022-10-04 03:33:16 +00:00
class AddOrganizationForm(ModelForm):
class Meta:
model = Organization
fields = [
"name",
"slug",
"short_name",
"about",
"organization_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)
self.fields["organization_image"].required = False
def save(self, commit=True):
res = super(AddOrganizationForm, self).save(commit=False)
res.registrant = self.request.profile
if commit:
res.save()
return res
2022-09-16 05:07:27 +00:00
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)
2022-09-16 05:07:27 +00:00
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() + f"?org_id={self.org_id}"
)
class Meta:
model = Contest
fields = (
2022-09-16 05:07:27 +00:00
"is_visible",
"key",
"name",
2022-09-16 05:07:27 +00:00
"start_time",
"end_time",
"format_name",
"authors",
"curators",
"testers",
2022-09-16 05:07:27 +00:00
"time_limit",
2023-02-14 23:36:45 +00:00
"freeze_after",
"use_clarifications",
"hide_problem_tags",
2023-09-17 04:55:24 +00:00
"public_scoreboard",
"scoreboard_visibility",
"points_precision",
2024-03-23 05:26:53 +00:00
"rate_limit",
"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(),
}
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_new_users(self):
2022-05-30 06:59:53 +00:00
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)
)
)
return valid_usernames
2022-05-30 06:59:53 +00:00
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:
2024-02-05 23:02:49 +00:00
widgets["content"] = PagedownWidget()
2020-01-21 06:35:58 +00:00
class CustomAuthenticationForm(AuthenticationForm):
def __init__(self, *args, **kwargs):
super(CustomAuthenticationForm, self).__init__(*args, **kwargs)
2023-11-28 01:49:38 +00:00
self.fields["username"].widget.attrs.update(
{"placeholder": _("Username/Email")}
)
2022-05-14 17:57:27 +00:00
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]+$"))],
)
2023-08-14 14:10:28 +00:00
organization = ChoiceField(choices=(), required=True)
def __init__(self, *args, org_choices=(), profile=None, **kwargs):
super(ContestCloneForm, self).__init__(*args, **kwargs)
self.fields["organization"].widget = Select2Widget(
attrs={"style": "width: 100%", "data-placeholder": _("Group")},
)
self.fields["organization"].choices = org_choices
self.profile = profile
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
2023-08-14 14:10:28 +00:00
def clean_organization(self):
organization_id = self.cleaned_data["organization"]
try:
organization = Organization.objects.get(id=organization_id)
except Exception:
raise ValidationError(_("Group doesn't exist."))
if not organization.admins.filter(id=self.profile.id).exists():
raise ValidationError(_("You don't have permission in this group."))
return organization
2022-03-10 05:38:29 +00:00
class ProblemPointsVoteForm(ModelForm):
class Meta:
model = ProblemPointsVote
2022-05-14 17:57:27 +00:00
fields = ["points"]
class ContestProblemForm(ModelForm):
class Meta:
model = ContestProblem
fields = (
"order",
"problem",
"points",
"partial",
"show_testcases",
"max_submissions",
)
widgets = {
"problem": HeavySelect2Widget(
2023-08-25 23:38:19 +00:00
data_view="problem_select2", attrs={"style": "width: 100%"}
),
}
class ContestProblemModelFormSet(BaseModelFormSet):
def is_valid(self):
valid = super().is_valid()
if not valid:
return valid
problems = set()
duplicates = []
for form in self.forms:
if form.cleaned_data and not form.cleaned_data.get("DELETE", False):
problem = form.cleaned_data.get("problem")
if problem in problems:
duplicates.append(problem)
else:
problems.add(problem)
if duplicates:
for form in self.forms:
problem = form.cleaned_data.get("problem")
if problem in duplicates:
form.add_error("problem", _("This problem is duplicated."))
return False
return True
class ContestProblemFormSet(
formset_factory(
ContestProblemForm, formset=ContestProblemModelFormSet, extra=6, can_delete=True
)
):
model = ContestProblem
2024-01-08 18:27:20 +00:00
class TestFormatterForm(ModelForm):
class Meta:
model = TestFormatterModel
fields = ["file"]