Allow to create group and its contest on UI

This commit is contained in:
cuom1999 2022-09-15 02:05:02 -05:00
parent 196e2a9bb0
commit 67ef6b9111
28 changed files with 1029 additions and 556 deletions

View file

@ -47,6 +47,7 @@ class OrganizationAdmin(VersionAdmin):
"registrant",
"show_public",
)
search_fields = ("name", "short_name", "registrant__user__username")
prepopulated_fields = {"slug": ("name",)}
actions_on_top = True
actions_on_bottom = True

View file

@ -7,7 +7,14 @@ from django.contrib.auth.forms import AuthenticationForm
from django.core.exceptions import ValidationError, ObjectDoesNotExist
from django.core.validators import RegexValidator
from django.db.models import Q
from django.forms import CharField, ChoiceField, Form, ModelForm
from django.forms import (
CharField,
ChoiceField,
Form,
ModelForm,
formset_factory,
BaseModelFormSet,
)
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
@ -23,6 +30,7 @@ from judge.models import (
Profile,
Submission,
BlogPost,
ContestProblem,
)
from judge.utils.subscription import newsletter_id
from judge.widgets import (
@ -31,6 +39,10 @@ from judge.widgets import (
PagedownWidget,
Select2MultipleWidget,
Select2Widget,
HeavySelect2MultipleWidget,
HeavySelect2Widget,
Select2MultipleWidget,
DateTimePickerWidget,
)
@ -127,7 +139,15 @@ class ProblemSubmitForm(ModelForm):
class EditOrganizationForm(ModelForm):
class Meta:
model = Organization
fields = ["about", "logo_override_image", "admins", "is_open"]
fields = [
"name",
"slug",
"short_name",
"about",
"logo_override_image",
"admins",
"is_open",
]
widgets = {"admins": Select2MultipleWidget()}
if HeavyPreviewPageDownWidget is not None:
widgets["about"] = HeavyPreviewPageDownWidget(
@ -135,6 +155,105 @@ class EditOrganizationForm(ModelForm):
)
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 OrganizationContestForm(ModelForm):
def __init__(self, *args, **kwargs):
self.org_id = kwargs.pop("org_id", 0)
super(OrganizationContestForm, 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"
)
class Meta:
model = Contest
fields = (
"key",
"name",
"authors",
"curators",
"testers",
"is_visible",
"use_clarifications",
"hide_problem_tags",
"scoreboard_visibility",
"run_pretests_only",
"points_precision",
"start_time",
"end_time",
"time_limit",
"description",
"og_image",
"logo_override_image",
"summary",
"format_name",
"format_config",
"problem_label_script",
"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,
@ -291,3 +410,29 @@ 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

View file

@ -19,12 +19,15 @@ def gen_submissions():
with connection.cursor() as cursor:
cursor.execute(query)
headers = [i[0] for i in cursor.description]
with open(os.path.join(settings.ML_DATA_PATH, "submissions.csv"), "w") as csvfile:
with open(
os.path.join(settings.ML_DATA_PATH, "submissions.csv"), "w"
) as csvfile:
f = csv.writer(csvfile)
f.writerow(headers)
for row in cursor.fetchall():
f.writerow(row)
def gen_users():
print("Generating users")
headers = ["uid", "username", "rating", "points"]

View file

@ -0,0 +1,28 @@
# Generated by Django 2.2.25 on 2022-09-15 06:49
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0131_auto_20220905_0027"),
]
operations = [
migrations.AlterField(
model_name="contestproblem",
name="max_submissions",
field=models.IntegerField(
default=0,
help_text="Maximum number of submissions for this problem, or 0 for no limit.",
validators=[
django.core.validators.MinValueValidator(
0, "Why include a problem you can't submit to?"
)
],
verbose_name="max submissions",
),
),
]

View file

@ -759,6 +759,7 @@ class ContestProblem(models.Model):
help_text=_(
"Maximum number of submissions for this problem, " "or 0 for no limit."
),
verbose_name=_("max submissions"),
default=0,
validators=[
MinValueValidator(0, _("Why include a problem you " "can't submit to?"))

View file

@ -1,3 +1,4 @@
from itertools import chain
from django import forms
from django.conf import settings
from django.contrib import messages
@ -39,9 +40,12 @@ from reversion import revisions
from judge.forms import (
EditOrganizationForm,
AddOrganizationForm,
AddOrganizationMemberForm,
OrganizationBlogForm,
OrganizationAdminBlogForm,
OrganizationContestForm,
ContestProblemFormSet,
)
from judge.models import (
BlogPost,
@ -52,6 +56,7 @@ from judge.models import (
Profile,
Contest,
Notification,
ContestProblem,
)
from judge import event_poster as event
from judge.utils.ranker import ranker
@ -121,6 +126,7 @@ class OrganizationMixin(OrganizationBase):
context["logo_override_image"] = self.organization.logo_override_image
if "organizations" in context:
context.pop("organizations")
print(context)
return context
def dispatch(self, request, *args, **kwargs):
@ -374,16 +380,59 @@ class OrganizationProblems(LoginRequiredMixin, MemberOrganizationMixin, ProblemL
return context
class OrganizationContests(LoginRequiredMixin, MemberOrganizationMixin, ContestList):
class OrganizationContestMixin(
LoginRequiredMixin,
TitleMixin,
OrganizationMixin,
OrganizationHomeViewContext,
):
model = Contest
form_class = OrganizationContestForm
def is_contest_editable(self, request, contest):
return request.profile in contest.authors.all() or self.can_edit_organization(
self.organization
)
def get_form_kwargs(self):
kwargs = super(OrganizationContestMixin, self).get_form_kwargs()
kwargs["org_id"] = self.organization.id
return kwargs
class OrganizationContests(
OrganizationContestMixin, MemberOrganizationMixin, ContestList
):
template_name = "organization/contests.html"
def get_queryset(self):
self.org_query = [self.organization_id]
return super().get_queryset()
def set_editable_contest(self, contest):
if not contest:
return False
contest.is_editable = self.is_contest_editable(self.request, contest)
def get_context_data(self, **kwargs):
context = super(OrganizationContests, self).get_context_data(**kwargs)
context["page_type"] = "contests"
context["hide_contest_orgs"] = True
context.pop("organizations")
context["create_url"] = reverse(
"organization_contest_add",
args=[self.organization.id, self.organization.slug],
)
for participation in context["active_participations"]:
self.set_editable_contest(participation.contest)
for contest in context["past_contests"]:
self.set_editable_contest(contest)
for contest in context["current_contests"]:
self.set_editable_contest(contest)
for contest in context["future_contests"]:
self.set_editable_contest(contest)
print(context)
return context
@ -772,6 +821,153 @@ class EditOrganization(
return super(EditOrganization, self).form_valid(form)
class AddOrganization(LoginRequiredMixin, TitleMixin, CreateView):
template_name = "organization/add.html"
model = Organization
form_class = AddOrganizationForm
def get_title(self):
return _("Create group")
def get_form_kwargs(self):
kwargs = super(AddOrganization, self).get_form_kwargs()
kwargs["request"] = self.request
return kwargs
def form_valid(self, form):
if (
not self.request.user.is_staff
and Organization.objects.filter(registrant=self.request.profile).count()
>= settings.DMOJ_USER_MAX_ORGANIZATION_ADD
):
return generic_message(
self.request,
_("Exceeded limit"),
_("You created too many groups. You can only create at most %d groups")
% settings.DMOJ_USER_MAX_ORGANIZATION_ADD,
status=400,
)
with transaction.atomic(), revisions.create_revision():
revisions.set_comment(_("Added from site"))
revisions.set_user(self.request.user)
res = super(AddOrganization, self).form_valid(form)
self.object.admins.add(self.request.profile)
self.object.members.add(self.request.profile)
self.object.save()
return res
class AddOrganizationContest(
AdminOrganizationMixin, OrganizationContestMixin, CreateView
):
template_name = "organization/contest/add.html"
def get_title(self):
return _("Add contest")
def form_valid(self, form):
with transaction.atomic(), revisions.create_revision():
revisions.set_comment(_("Added from site"))
revisions.set_user(self.request.user)
res = super(AddOrganizationContest, self).form_valid(form)
self.object.organizations.add(self.organization)
self.object.is_organization_private = True
self.object.save()
return res
def get_success_url(self):
return reverse(
"organization_contest_edit",
args=[self.organization.id, self.organization.slug, self.object.key],
)
class EditOrganizationContest(
OrganizationContestMixin, MemberOrganizationMixin, UpdateView
):
template_name = "organization/contest/add.html"
def setup_contest(self, request, *args, **kwargs):
contest_key = kwargs.get("contest", None)
if not contest_key:
raise Http404()
self.contest = get_object_or_404(Contest, key=contest_key)
if self.organization not in self.contest.organizations.all():
raise Http404()
if not self.is_contest_editable(request, self.contest):
return generic_message(
self.request,
_("Permission denied"),
_("You are not allowed to edit this contest"),
status=400,
)
def get(self, request, *args, **kwargs):
res = self.setup_contest(request, *args, **kwargs)
if res:
return res
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
res = self.setup_contest(request, *args, **kwargs)
if res:
return res
problem_formset = self.get_problem_formset(True)
if problem_formset.is_valid():
for problem_form in problem_formset.save(commit=False):
if problem_form:
problem_form.contest = self.contest
problem_form.save()
for problem_form in problem_formset.deleted_objects:
problem_form.delete()
return super().post(request, *args, **kwargs)
self.object = self.contest
return self.render_to_response(
self.get_context_data(
problems_form=problem_formset,
)
)
def get_title(self):
return _("Edit %s") % self.contest.key
def get_content_title(self):
href = reverse("contest_view", args=[self.contest.key])
return mark_safe(f'Edit <a href="{href}">{self.contest.key}</a>')
def get_object(self):
return self.contest
def form_valid(self, form):
with transaction.atomic(), revisions.create_revision():
revisions.set_comment(_("Edited from site"))
revisions.set_user(self.request.user)
res = super(EditOrganizationContest, self).form_valid(form)
self.object.organizations.add(self.organization)
self.object.is_organization_private = True
self.object.save()
return res
def get_problem_formset(self, post=False):
return ContestProblemFormSet(
data=self.request.POST if post else None,
prefix="problems",
queryset=ContestProblem.objects.filter(contest=self.contest).order_by(
"order"
),
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if "problems_form" not in context:
context["problems_form"] = self.get_problem_formset()
return context
def get_success_url(self):
return self.request.path
class AddOrganizationBlog(
LoginRequiredMixin,
TitleMixin,

View file

@ -10,8 +10,14 @@ from judge.jinja2.gravatar import gravatar
from judge.models import Comment, Contest, Organization, Problem, Profile
def _get_user_queryset(term):
qs = Profile.objects
def _get_user_queryset(term, org_id):
if org_id:
try:
qs = Organization.objects.get(id=org_id).members.all()
except Exception:
raise Http404()
else:
qs = Profile.objects
if term.endswith(" "):
qs = qs.filter(user__username=term.strip())
else:
@ -46,9 +52,14 @@ class Select2View(BaseListView):
class UserSelect2View(Select2View):
def get(self, request, *args, **kwargs):
self.org_id = kwargs.get("org_id", request.GET.get("org_id", ""))
print(self.org_id)
return super(UserSelect2View, self).get(request, *args, **kwargs)
def get_queryset(self):
return (
_get_user_queryset(self.term)
_get_user_queryset(self.term, self.org_id)
.annotate(username=F("user__username"))
.only("id")
)

View file

@ -2,3 +2,4 @@ from judge.widgets.checkbox import CheckboxSelectMultipleWithSelectAll
from judge.widgets.mixins import CompressorWidgetMixin
from judge.widgets.pagedown import *
from judge.widgets.select2 import *
from judge.widgets.datetime import *

24
judge/widgets/datetime.py Normal file
View file

@ -0,0 +1,24 @@
from django import forms
class DateTimePickerWidget(forms.DateTimeInput):
template_name = "widgets/datetimepicker.html"
def get_context(self, name, value, attrs):
datetimepicker_id = "datetimepicker_{name}".format(name=name)
if attrs is None:
attrs = dict()
attrs["data-target"] = "#{id}".format(id=datetimepicker_id)
attrs["class"] = "form-control datetimepicker-input"
context = super().get_context(name, value, attrs)
context["widget"]["datetimepicker_id"] = datetimepicker_id
return context
@property
def media(self):
css_url = "https://cdnjs.cloudflare.com/ajax/libs/jquery-datetimepicker/2.5.20/jquery.datetimepicker.min.css"
js_url = "https://cdnjs.cloudflare.com/ajax/libs/jquery-datetimepicker/2.5.20/jquery.datetimepicker.full.min.js"
return forms.Media(
js=[js_url],
css={"screen": [css_url]},
)

View file

@ -45,6 +45,7 @@ from django.conf import settings
from django.core import signing
from django.forms.models import ModelChoiceIterator
from django.urls import reverse_lazy
from django.utils.http import urlencode
DEFAULT_SELECT2_JS = "//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/select2.min.js"
DEFAULT_SELECT2_CSS = (