Add contest to course (#126)
This commit is contained in:
parent
72eada0a4e
commit
3d67fb274e
22 changed files with 1258 additions and 433 deletions
67
judge/migrations/0194_course_contest.py
Normal file
67
judge/migrations/0194_course_contest.py
Normal file
|
@ -0,0 +1,67 @@
|
|||
# Generated by Django 3.2.21 on 2024-09-30 22:31
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("judge", "0193_remove_old_course_problems"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="contest",
|
||||
name="is_in_course",
|
||||
field=models.BooleanField(default=False, verbose_name="contest in course"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="courselesson",
|
||||
name="is_visible",
|
||||
field=models.BooleanField(default=True, verbose_name="publicly visible"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="courselesson",
|
||||
name="content",
|
||||
field=models.TextField(verbose_name="lesson content"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="courselesson",
|
||||
name="title",
|
||||
field=models.TextField(verbose_name="lesson title"),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="CourseContest",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("order", models.IntegerField(default=0, verbose_name="order")),
|
||||
("points", models.IntegerField(verbose_name="points")),
|
||||
(
|
||||
"contest",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="course",
|
||||
to="judge.contest",
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"course",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="contests",
|
||||
to="judge.course",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -61,7 +61,13 @@ from judge.models.ticket import Ticket, TicketMessage
|
|||
from judge.models.volunteer import VolunteerProblemVote
|
||||
from judge.models.pagevote import PageVote, PageVoteVoter
|
||||
from judge.models.bookmark import BookMark, MakeBookMark
|
||||
from judge.models.course import Course, CourseRole, CourseLesson, CourseLessonProblem
|
||||
from judge.models.course import (
|
||||
Course,
|
||||
CourseRole,
|
||||
CourseLesson,
|
||||
CourseLessonProblem,
|
||||
CourseContest,
|
||||
)
|
||||
from judge.models.notification import Notification, NotificationProfile
|
||||
from judge.models.test_formatter import TestFormatterModel
|
||||
|
||||
|
|
|
@ -246,6 +246,10 @@ class Contest(models.Model, PageVotable, Bookmarkable):
|
|||
verbose_name=_("organizations"),
|
||||
help_text=_("If private, only these organizations may see the contest"),
|
||||
)
|
||||
is_in_course = models.BooleanField(
|
||||
verbose_name=_("contest in course"),
|
||||
default=False,
|
||||
)
|
||||
og_image = models.CharField(
|
||||
verbose_name=_("OpenGraph image"), default="", max_length=150, blank=True
|
||||
)
|
||||
|
@ -561,6 +565,14 @@ class Contest(models.Model, PageVotable, Bookmarkable):
|
|||
if not self.is_visible:
|
||||
raise self.Inaccessible()
|
||||
|
||||
if self.is_in_course:
|
||||
from judge.models import Course, CourseContest
|
||||
|
||||
course_contest = CourseContest.objects.filter(contest=self).first()
|
||||
if Course.is_accessible_by(course_contest.course, user.profile):
|
||||
return
|
||||
raise self.Inaccessible()
|
||||
|
||||
# Contest is not private
|
||||
if not self.is_private and not self.is_organization_private:
|
||||
return
|
||||
|
@ -612,7 +624,10 @@ class Contest(models.Model, PageVotable, Bookmarkable):
|
|||
if not user.is_authenticated:
|
||||
return (
|
||||
cls.objects.filter(
|
||||
is_visible=True, is_organization_private=False, is_private=False
|
||||
is_visible=True,
|
||||
is_organization_private=False,
|
||||
is_private=False,
|
||||
is_in_course=False,
|
||||
)
|
||||
.defer("description")
|
||||
.distinct()
|
||||
|
@ -626,7 +641,7 @@ class Contest(models.Model, PageVotable, Bookmarkable):
|
|||
)
|
||||
or show_own_contests_only
|
||||
):
|
||||
q = Q(is_visible=True)
|
||||
q = Q(is_visible=True, is_in_course=False)
|
||||
q &= (
|
||||
Q(view_contest_scoreboard=user.profile)
|
||||
| Q(is_organization_private=False, is_private=False)
|
||||
|
|
|
@ -4,7 +4,7 @@ from django.utils.translation import gettext, gettext_lazy as _
|
|||
from django.urls import reverse
|
||||
from django.db.models import Q
|
||||
|
||||
from judge.models import BlogPost, Problem
|
||||
from judge.models import BlogPost, Problem, Contest
|
||||
from judge.models.profile import Organization, Profile
|
||||
|
||||
|
||||
|
@ -160,10 +160,11 @@ class CourseLesson(models.Model):
|
|||
related_name="lessons",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
title = models.TextField(verbose_name=_("course title"))
|
||||
content = models.TextField(verbose_name=_("course content"))
|
||||
title = models.TextField(verbose_name=_("lesson title"))
|
||||
content = models.TextField(verbose_name=_("lesson content"))
|
||||
order = models.IntegerField(verbose_name=_("order"), default=0)
|
||||
points = models.IntegerField(verbose_name=_("points"))
|
||||
is_visible = models.BooleanField(verbose_name=_("publicly visible"), default=True)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse(
|
||||
|
@ -182,3 +183,19 @@ class CourseLessonProblem(models.Model):
|
|||
problem = models.ForeignKey(Problem, on_delete=models.CASCADE)
|
||||
order = models.IntegerField(verbose_name=_("order"), default=0)
|
||||
score = models.IntegerField(verbose_name=_("score"), default=0)
|
||||
|
||||
|
||||
class CourseContest(models.Model):
|
||||
course = models.ForeignKey(
|
||||
Course, on_delete=models.CASCADE, related_name="contests"
|
||||
)
|
||||
contest = models.ForeignKey(
|
||||
Contest, unique=True, on_delete=models.CASCADE, related_name="course"
|
||||
)
|
||||
order = models.IntegerField(verbose_name=_("order"), default=0)
|
||||
points = models.IntegerField(verbose_name=_("points"))
|
||||
|
||||
def get_course_of_contest(contest):
|
||||
course_contest = contest.course.get()
|
||||
course = course_contest.course
|
||||
return course
|
||||
|
|
32
judge/utils/contest.py
Normal file
32
judge/utils/contest.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
from django.db import transaction
|
||||
from judge.tasks import rescore_contest
|
||||
from judge.models import (
|
||||
Contest,
|
||||
)
|
||||
|
||||
|
||||
def maybe_trigger_contest_rescore(form, contest):
|
||||
if any(
|
||||
f in form.changed_data
|
||||
for f in (
|
||||
"start_time",
|
||||
"end_time",
|
||||
"time_limit",
|
||||
"format_config",
|
||||
"format_name",
|
||||
"freeze_after",
|
||||
)
|
||||
):
|
||||
transaction.on_commit(rescore_contest.s(contest.key).delay)
|
||||
|
||||
if any(
|
||||
f in form.changed_data
|
||||
for f in (
|
||||
"authors",
|
||||
"curators",
|
||||
"testers",
|
||||
)
|
||||
):
|
||||
Contest._author_ids.dirty(contest)
|
||||
Contest._curator_ids.dirty(contest)
|
||||
Contest._tester_ids.dirty(contest)
|
|
@ -534,6 +534,14 @@ class ContestDetail(
|
|||
)
|
||||
context["editable_organizations"] = self.get_editable_organizations()
|
||||
context["is_clonable"] = is_contest_clonable(self.request, self.object)
|
||||
|
||||
if self.object.is_in_course:
|
||||
from judge.models import Course, CourseContest
|
||||
|
||||
course = CourseContest.get_course_of_contest(self.object)
|
||||
if Course.is_editable_by(course, self.request.profile):
|
||||
context["editable_course"] = course
|
||||
|
||||
if self.request.in_contest:
|
||||
context["current_contest"] = self.request.participation.contest
|
||||
else:
|
||||
|
|
|
@ -12,28 +12,42 @@ from django.forms import (
|
|||
)
|
||||
from django.views.generic.edit import FormView
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse_lazy
|
||||
from django.db.models import Max, F
|
||||
from django.urls import reverse_lazy, reverse
|
||||
from django.db.models import Max, F, Sum
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
from judge.models import (
|
||||
Course,
|
||||
Contest,
|
||||
CourseLesson,
|
||||
Submission,
|
||||
Profile,
|
||||
CourseRole,
|
||||
CourseLessonProblem,
|
||||
CourseContest,
|
||||
ContestProblem,
|
||||
ContestParticipation,
|
||||
)
|
||||
from judge.models.course import RoleInCourse
|
||||
from judge.widgets import (
|
||||
HeavyPreviewPageDownWidget,
|
||||
HeavySelect2MultipleWidget,
|
||||
HeavySelect2Widget,
|
||||
DateTimePickerWidget,
|
||||
Select2MultipleWidget,
|
||||
Select2Widget,
|
||||
)
|
||||
from judge.forms import (
|
||||
ContestProblemFormSet,
|
||||
)
|
||||
from judge.utils.problems import (
|
||||
user_attempted_ids,
|
||||
user_completed_ids,
|
||||
)
|
||||
from judge.utils.contest import (
|
||||
maybe_trigger_contest_rescore,
|
||||
)
|
||||
from reversion import revisions
|
||||
|
||||
|
||||
def max_case_points_per_problem(profile, problems):
|
||||
|
@ -85,6 +99,65 @@ def calculate_lessons_progress(profile, lessons):
|
|||
return res
|
||||
|
||||
|
||||
def calculate_contests_progress(profile, course_contests):
|
||||
res = {}
|
||||
total_achieved_points = total_contest_points = 0
|
||||
for course_contest in course_contests:
|
||||
contest = course_contest.contest
|
||||
|
||||
achieved_points = 0
|
||||
participation = ContestParticipation.objects.filter(
|
||||
contest=contest, user=profile, virtual=0
|
||||
).first()
|
||||
|
||||
if participation:
|
||||
achieved_points = participation.score
|
||||
|
||||
total_points = (
|
||||
ContestProblem.objects.filter(contest=contest).aggregate(Sum("points"))[
|
||||
"points__sum"
|
||||
]
|
||||
or 0
|
||||
)
|
||||
|
||||
res[course_contest.id] = {
|
||||
"achieved_points": achieved_points,
|
||||
"total_points": total_points,
|
||||
"percentage": achieved_points / total_points * 100 if total_points else 0,
|
||||
}
|
||||
|
||||
if total_points:
|
||||
total_achieved_points += (
|
||||
achieved_points / total_points * course_contest.points
|
||||
)
|
||||
total_contest_points += course_contest.points
|
||||
|
||||
res["total"] = {
|
||||
"achieved_points": total_achieved_points,
|
||||
"total_points": total_contest_points,
|
||||
"percentage": total_achieved_points / total_contest_points * 100
|
||||
if total_contest_points
|
||||
else 0,
|
||||
}
|
||||
return res
|
||||
|
||||
|
||||
def calculate_total_progress(profile, lesson_progress, contest_progress):
|
||||
lesson_total = lesson_progress["total"]
|
||||
contest_total = contest_progress["total"]
|
||||
total_achieved_points = (
|
||||
lesson_total["achieved_points"] + contest_total["achieved_points"]
|
||||
)
|
||||
total_points = lesson_total["total_points"] + contest_total["total_points"]
|
||||
|
||||
res = {
|
||||
"achieved_points": total_achieved_points,
|
||||
"total_points": total_points,
|
||||
"percentage": total_achieved_points / total_points * 100 if total_points else 0,
|
||||
}
|
||||
return res
|
||||
|
||||
|
||||
class CourseList(ListView):
|
||||
model = Course
|
||||
template_name = "course/list.html"
|
||||
|
@ -130,13 +203,34 @@ class CourseDetail(CourseDetailMixin, DetailView):
|
|||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(CourseDetail, self).get_context_data(**kwargs)
|
||||
lessons = self.course.lessons.prefetch_related("lesson_problems").all()
|
||||
lessons = (
|
||||
self.course.lessons.filter(is_visible=True)
|
||||
.order_by("order")
|
||||
.prefetch_related("lesson_problems")
|
||||
.all()
|
||||
)
|
||||
course_contests = (
|
||||
self.course.contests.select_related("contest")
|
||||
.filter(contest__is_visible=True)
|
||||
.order_by("order")
|
||||
)
|
||||
context["title"] = self.course.name
|
||||
context["page_type"] = "home"
|
||||
context["lessons"] = lessons
|
||||
context["lesson_progress"] = calculate_lessons_progress(
|
||||
self.request.profile, lessons
|
||||
)
|
||||
context["course_contests"] = course_contests
|
||||
context["contest_progress"] = calculate_contests_progress(
|
||||
self.request.profile, course_contests
|
||||
)
|
||||
|
||||
context["total_progress"] = calculate_total_progress(
|
||||
self.request.profile,
|
||||
context["lesson_progress"],
|
||||
context["contest_progress"],
|
||||
)
|
||||
|
||||
return context
|
||||
|
||||
|
||||
|
@ -149,6 +243,11 @@ class CourseLessonDetail(CourseDetailMixin, DetailView):
|
|||
self.lesson = CourseLesson.objects.get(
|
||||
course=self.course, id=self.kwargs["id"]
|
||||
)
|
||||
|
||||
is_editable = Course.is_editable_by(self.course, self.request.profile)
|
||||
if not self.lesson.is_visible and not is_editable:
|
||||
raise Http404()
|
||||
|
||||
return self.lesson
|
||||
except ObjectDoesNotExist:
|
||||
raise Http404()
|
||||
|
@ -190,7 +289,7 @@ class CourseLessonDetail(CourseDetailMixin, DetailView):
|
|||
class CourseLessonForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = CourseLesson
|
||||
fields = ["order", "title", "points", "content"]
|
||||
fields = ["order", "title", "is_visible", "points", "content"]
|
||||
widgets = {
|
||||
"title": forms.TextInput(),
|
||||
"content": HeavyPreviewPageDownWidget(preview=reverse_lazy("blog_preview")),
|
||||
|
@ -312,9 +411,29 @@ class CourseStudentResults(CourseEditableMixin, DetailView):
|
|||
def get_grades(self):
|
||||
students = self.course.get_students()
|
||||
students.sort(key=lambda u: u.username.lower())
|
||||
lessons = self.course.lessons.prefetch_related("lesson_problems").all()
|
||||
grades = {s: calculate_lessons_progress(s, lessons) for s in students}
|
||||
return grades
|
||||
lessons = (
|
||||
self.course.lessons.filter(is_visible=True)
|
||||
.prefetch_related("lesson_problems")
|
||||
.all()
|
||||
)
|
||||
course_contests = (
|
||||
self.course.contests.select_related("contest")
|
||||
.filter(contest__is_visible=True)
|
||||
.order_by("order")
|
||||
)
|
||||
|
||||
grade_lessons = {}
|
||||
grade_contests = {}
|
||||
grade_total = {}
|
||||
for s in students:
|
||||
grade_lessons[s] = lesson_progress = calculate_lessons_progress(s, lessons)
|
||||
grade_contests[s] = contest_progress = calculate_contests_progress(
|
||||
s, course_contests
|
||||
)
|
||||
grade_total[s] = calculate_total_progress(
|
||||
s, lesson_progress, contest_progress
|
||||
)
|
||||
return grade_lessons, grade_contests, grade_total
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(CourseStudentResults, self).get_context_data(**kwargs)
|
||||
|
@ -329,7 +448,19 @@ class CourseStudentResults(CourseEditableMixin, DetailView):
|
|||
}
|
||||
)
|
||||
context["page_type"] = "grades"
|
||||
context["grades"] = self.get_grades()
|
||||
(
|
||||
context["grade_lessons"],
|
||||
context["grade_contests"],
|
||||
context["grade_total"],
|
||||
) = self.get_grades()
|
||||
context["lessons"] = self.course.lessons.filter(is_visible=True).order_by(
|
||||
"order"
|
||||
)
|
||||
context["course_contests"] = (
|
||||
self.course.contests.select_related("contest")
|
||||
.filter(contest__is_visible=True)
|
||||
.order_by("order")
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
|
@ -392,3 +523,255 @@ class CourseStudentResultsLesson(CourseEditableMixin, DetailView):
|
|||
context["page_type"] = "grades"
|
||||
context["grades"] = self.get_lesson_grades()
|
||||
return context
|
||||
|
||||
|
||||
class AddCourseContestForm(forms.ModelForm):
|
||||
order = forms.IntegerField(label=_("Order"))
|
||||
points = forms.IntegerField(label=_("Points"))
|
||||
|
||||
class Meta:
|
||||
model = Contest
|
||||
fields = [
|
||||
"order",
|
||||
"points",
|
||||
"key",
|
||||
"name",
|
||||
"start_time",
|
||||
"end_time",
|
||||
"problems",
|
||||
]
|
||||
widgets = {
|
||||
"start_time": DateTimePickerWidget(),
|
||||
"end_time": DateTimePickerWidget(),
|
||||
"problems": HeavySelect2MultipleWidget(data_view="problem_select2"),
|
||||
}
|
||||
|
||||
def save(self, course, profile, commit=True):
|
||||
contest = super().save(commit=False)
|
||||
contest.is_in_course = True
|
||||
|
||||
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)
|
||||
contest.authors.add(profile)
|
||||
old_save_m2m()
|
||||
|
||||
self.save_m2m = save_m2m
|
||||
contest.save()
|
||||
self.save_m2m()
|
||||
|
||||
CourseContest.objects.create(
|
||||
course=course,
|
||||
contest=contest,
|
||||
order=self.cleaned_data["order"],
|
||||
points=self.cleaned_data["points"],
|
||||
)
|
||||
|
||||
return contest
|
||||
|
||||
|
||||
class AddCourseContest(CourseEditableMixin, FormView):
|
||||
template_name = "course/add_contest.html"
|
||||
form_class = AddCourseContestForm
|
||||
|
||||
def get_title(self):
|
||||
return _("Add contest")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["title"] = self.get_title()
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
with revisions.create_revision():
|
||||
revisions.set_comment(_("Added from course") + " " + self.course.name)
|
||||
revisions.set_user(self.request.user)
|
||||
|
||||
self.contest = form.save(course=self.course, profile=self.request.profile)
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse(
|
||||
"edit_course_contest",
|
||||
args=[self.course.slug, self.contest.key],
|
||||
)
|
||||
|
||||
|
||||
class CourseContestList(CourseEditableMixin, ListView):
|
||||
template_name = "course/contest_list.html"
|
||||
context_object_name = "course_contests"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["title"] = _("Contest list")
|
||||
context["page_type"] = "contests"
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
return self.course.contests.select_related("contest").all().order_by("order")
|
||||
|
||||
|
||||
class EditCourseContestForm(ModelForm):
|
||||
order = forms.IntegerField(label=_("Order"))
|
||||
points = forms.IntegerField(label=_("Points"))
|
||||
|
||||
class Meta:
|
||||
model = Contest
|
||||
fields = (
|
||||
"order",
|
||||
"points",
|
||||
"is_visible",
|
||||
"key",
|
||||
"name",
|
||||
"start_time",
|
||||
"end_time",
|
||||
"format_name",
|
||||
"authors",
|
||||
"curators",
|
||||
"testers",
|
||||
"time_limit",
|
||||
"freeze_after",
|
||||
"use_clarifications",
|
||||
"hide_problem_tags",
|
||||
"public_scoreboard",
|
||||
"scoreboard_visibility",
|
||||
"points_precision",
|
||||
"rate_limit",
|
||||
"description",
|
||||
"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"
|
||||
),
|
||||
"tags": Select2MultipleWidget,
|
||||
"description": HeavyPreviewPageDownWidget(
|
||||
preview=reverse_lazy("contest_preview")
|
||||
),
|
||||
"start_time": DateTimePickerWidget(),
|
||||
"end_time": DateTimePickerWidget(),
|
||||
"format_name": Select2Widget(),
|
||||
"scoreboard_visibility": Select2Widget(),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.course_contest_instance = kwargs.pop("course_contest_instance", None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if self.course_contest_instance:
|
||||
self.fields["order"].initial = self.course_contest_instance.order
|
||||
self.fields["points"].initial = self.course_contest_instance.points
|
||||
|
||||
def save(self, commit=True):
|
||||
contest = super().save(commit=commit)
|
||||
|
||||
if self.course_contest_instance:
|
||||
self.course_contest_instance.order = self.cleaned_data["order"]
|
||||
self.course_contest_instance.points = self.cleaned_data["points"]
|
||||
if commit:
|
||||
self.course_contest_instance.save()
|
||||
|
||||
return contest
|
||||
|
||||
|
||||
class EditCourseContest(CourseEditableMixin, FormView):
|
||||
template_name = "course/edit_contest.html"
|
||||
form_class = EditCourseContestForm
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.contest = get_object_or_404(Contest, key=self.kwargs["contest"])
|
||||
res = super().dispatch(request, *args, **kwargs)
|
||||
if not self.contest.is_in_course:
|
||||
raise Http404()
|
||||
return res
|
||||
|
||||
def get_form_kwargs(self):
|
||||
self.course_contest = get_object_or_404(
|
||||
CourseContest, course=self.course, contest=self.contest
|
||||
)
|
||||
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs["instance"] = self.contest
|
||||
kwargs["course_contest_instance"] = self.course_contest
|
||||
return kwargs
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
problem_formset = self.get_problem_formset(True)
|
||||
if problem_formset.is_valid():
|
||||
for problem_form in problem_formset:
|
||||
if problem_form.cleaned_data.get("DELETE") and problem_form.instance.pk:
|
||||
problem_form.instance.delete()
|
||||
|
||||
for problem_form in problem_formset.save(commit=False):
|
||||
if problem_form:
|
||||
problem_form.contest = self.contest
|
||||
problem_form.save()
|
||||
|
||||
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 contest")
|
||||
|
||||
def form_valid(self, form):
|
||||
with revisions.create_revision():
|
||||
revisions.set_comment(_("Edited from course") + " " + self.course.name)
|
||||
revisions.set_user(self.request.user)
|
||||
|
||||
maybe_trigger_contest_rescore(form, self.contest)
|
||||
|
||||
form.save()
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
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)
|
||||
context["title"] = self.get_title()
|
||||
context["content_title"] = mark_safe(
|
||||
_("Edit <a href='%(url)s'>%(contest_name)s</a>")
|
||||
% {
|
||||
"contest_name": self.contest.name,
|
||||
"url": self.contest.get_absolute_url(),
|
||||
}
|
||||
)
|
||||
if "problems_form" not in context:
|
||||
context["problems_form"] = self.get_problem_formset()
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse(
|
||||
"edit_course_contest",
|
||||
args=[self.course.slug, self.contest.key],
|
||||
)
|
||||
|
|
|
@ -69,6 +69,7 @@ from judge.utils.views import (
|
|||
DiggPaginatorMixin,
|
||||
)
|
||||
from judge.utils.problems import user_attempted_ids, user_completed_ids
|
||||
from judge.utils.contest import maybe_trigger_contest_rescore
|
||||
from judge.views.problem import ProblemList
|
||||
from judge.views.contests import ContestList
|
||||
from judge.views.submission import SubmissionsListBase
|
||||
|
@ -1038,30 +1039,8 @@ class EditOrganizationContest(
|
|||
self.object.is_organization_private = True
|
||||
self.object.save()
|
||||
|
||||
if any(
|
||||
f in form.changed_data
|
||||
for f in (
|
||||
"start_time",
|
||||
"end_time",
|
||||
"time_limit",
|
||||
"format_config",
|
||||
"format_name",
|
||||
"freeze_after",
|
||||
)
|
||||
):
|
||||
transaction.on_commit(rescore_contest.s(self.object.key).delay)
|
||||
maybe_trigger_contest_rescore(form, self.object)
|
||||
|
||||
if any(
|
||||
f in form.changed_data
|
||||
for f in (
|
||||
"authors",
|
||||
"curators",
|
||||
"testers",
|
||||
)
|
||||
):
|
||||
Contest._author_ids.dirty(self.object)
|
||||
Contest._curator_ids.dirty(self.object)
|
||||
Contest._tester_ids.dirty(self.object)
|
||||
return res
|
||||
|
||||
def get_problem_formset(self, post=False):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue