from django.utils.html import mark_safe from django.db import models from django.views.generic import ListView, DetailView, View from django.utils.translation import gettext, gettext_lazy as _ from django.http import Http404 from django import forms from django.forms import ( inlineformset_factory, ModelForm, modelformset_factory, BaseModelFormSet, ) from django.views.generic.edit import FormView from django.shortcuts import get_object_or_404 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): # return a dict {problem_id: {case_points, case_total}} q = ( Submission.objects.filter(user=profile, problem__in=problems) .values("problem") .annotate(case_points=Max("case_points"), case_total=F("case_total")) .order_by("problem") ) res = {} for problem in q: res[problem["problem"]] = problem return res def calculate_lessons_progress(profile, lessons): res = {} total_achieved_points = total_lesson_points = 0 for lesson in lessons: problems = lesson.lesson_problems.values_list("problem", flat=True) problem_points = max_case_points_per_problem(profile, problems) achieved_points = total_points = 0 for lesson_problem in lesson.lesson_problems.all(): val = problem_points.get(lesson_problem.problem.id) if val and val["case_total"]: achieved_points += ( val["case_points"] / val["case_total"] * lesson_problem.score ) total_points += lesson_problem.score res[lesson.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 * lesson.points total_lesson_points += lesson.points res["total"] = { "achieved_points": total_achieved_points, "total_points": total_lesson_points, "percentage": total_achieved_points / total_lesson_points * 100 if total_lesson_points else 0, } 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" queryset = Course.objects.filter(is_public=True).filter(is_open=True) def get_context_data(self, **kwargs): context = super(CourseList, self).get_context_data(**kwargs) context["courses"] = Course.get_accessible_courses(self.request.profile) context["title"] = _("Courses") context["page_type"] = "list" return context class CourseDetailMixin(object): def dispatch(self, request, *args, **kwargs): self.course = get_object_or_404(Course, slug=self.kwargs["slug"]) if not Course.is_accessible_by(self.course, self.request.profile): raise Http404() self.is_editable = Course.is_editable_by(self.course, self.request.profile) return super(CourseDetailMixin, self).dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): context = super(CourseDetailMixin, self).get_context_data(**kwargs) context["course"] = self.course context["is_editable"] = self.is_editable return context class CourseEditableMixin(CourseDetailMixin): def dispatch(self, request, *args, **kwargs): res = super(CourseEditableMixin, self).dispatch(request, *args, **kwargs) if not self.is_editable: raise Http404() return res class CourseDetail(CourseDetailMixin, DetailView): model = Course template_name = "course/course.html" def get_object(self): return self.course def get_context_data(self, **kwargs): context = super(CourseDetail, self).get_context_data(**kwargs) 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 class CourseLessonDetail(CourseDetailMixin, DetailView): model = CourseLesson template_name = "course/lesson.html" def get_object(self): try: 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() def get_profile(self): username = self.request.GET.get("user") if not username: return self.request.profile is_editable = Course.is_editable_by(self.course, self.request.profile) if not is_editable: raise Http404() try: profile = Profile.objects.get(user__username=username) is_student = profile.course_roles.filter( role=RoleInCourse.STUDENT, course=self.course ).exists() if not is_student: raise Http404() return profile except ObjectDoesNotExist: raise Http404() def get_context_data(self, **kwargs): context = super(CourseLessonDetail, self).get_context_data(**kwargs) profile = self.get_profile() context["profile"] = profile context["title"] = self.lesson.title context["lesson"] = self.lesson context["completed_problem_ids"] = user_completed_ids(profile) context["attempted_problems"] = user_attempted_ids(profile) context["problem_points"] = max_case_points_per_problem( profile, self.lesson.lesson_problems.values_list("problem", flat=True) ) return context class CourseLessonForm(forms.ModelForm): class Meta: model = CourseLesson fields = ["order", "title", "is_visible", "points", "content"] widgets = { "title": forms.TextInput(), "content": HeavyPreviewPageDownWidget(preview=reverse_lazy("blog_preview")), "problems": HeavySelect2MultipleWidget(data_view="problem_select2"), } CourseLessonFormSet = inlineformset_factory( Course, CourseLesson, form=CourseLessonForm, extra=1, can_delete=True ) class CourseLessonProblemForm(ModelForm): class Meta: model = CourseLessonProblem fields = ["order", "problem", "score", "lesson"] widgets = { "problem": HeavySelect2Widget( data_view="problem_select2", attrs={"style": "width: 100%"} ), "lesson": forms.HiddenInput(), } CourseLessonProblemFormSet = modelformset_factory( CourseLessonProblem, form=CourseLessonProblemForm, extra=5, can_delete=True ) class EditCourseLessonsView(CourseEditableMixin, FormView): template_name = "course/edit_lesson.html" form_class = CourseLessonFormSet def get_problem_formset(self, post=False, lesson=None): formset = CourseLessonProblemFormSet( data=self.request.POST if post else None, prefix=f"problems_{lesson.id}" if lesson else "problems", queryset=CourseLessonProblem.objects.filter(lesson=lesson).order_by( "order" ), ) if lesson: for form in formset: form.fields["lesson"].initial = lesson return formset def get_context_data(self, **kwargs): context = super(EditCourseLessonsView, self).get_context_data(**kwargs) if self.request.method == "POST": context["formset"] = self.form_class( self.request.POST, self.request.FILES, instance=self.course ) context["problem_formsets"] = { lesson.instance.id: self.get_problem_formset( post=True, lesson=lesson.instance ) for lesson in context["formset"].forms if lesson.instance.id } else: context["formset"] = self.form_class( instance=self.course, queryset=self.course.lessons.order_by("order") ) context["problem_formsets"] = { lesson.instance.id: self.get_problem_formset( post=False, lesson=lesson.instance ) for lesson in context["formset"].forms if lesson.instance.id } context["title"] = _("Edit lessons for %(course_name)s") % { "course_name": self.course.name } context["content_title"] = mark_safe( _("Edit lessons for %(course_name)s") % { "course_name": self.course.name, "url": self.course.get_absolute_url(), } ) context["page_type"] = "edit_lesson" return context def post(self, request, *args, **kwargs): formset = self.form_class(request.POST, instance=self.course) problem_formsets = [ self.get_problem_formset(post=True, lesson=lesson.instance) for lesson in formset.forms if lesson.instance.id ] for pf in problem_formsets: if not pf.is_valid(): return self.form_invalid(pf) if formset.is_valid(): formset.save() for problem_formset in problem_formsets: problem_formset.save() for obj in problem_formset.deleted_objects: if obj.pk is not None: obj.delete() return self.form_valid(formset) else: return self.form_invalid(formset) def get_success_url(self): return self.request.path class CourseStudentResults(CourseEditableMixin, DetailView): model = Course template_name = "course/grades.html" def get_object(self): return self.course def get_grades(self): students = self.course.get_students() students.sort(key=lambda u: u.username.lower()) 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) context["title"] = _("Grades in %(course_name)s") % { "course_name": self.course.name, } context["content_title"] = mark_safe( _("Grades in %(course_name)s") % { "course_name": self.course.name, "url": self.course.get_absolute_url(), } ) context["page_type"] = "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 class CourseStudentResultsLesson(CourseEditableMixin, DetailView): model = CourseLesson template_name = "course/grades_lesson.html" def get_object(self): try: self.lesson = CourseLesson.objects.get( course=self.course, id=self.kwargs["id"] ) return self.lesson except ObjectDoesNotExist: raise Http404() def get_lesson_grades(self): students = self.course.get_students() students.sort(key=lambda u: u.username.lower()) problems = self.lesson.lesson_problems.values_list("problem", flat=True) lesson_problems = self.lesson.lesson_problems.all() grades = {} for s in students: grades[s] = problem_points = max_case_points_per_problem(s, problems) achieved_points = total_points = 0 for lesson_problem in lesson_problems: val = problem_points.get(lesson_problem.problem.id) if val and val["case_total"]: achieved_points += ( val["case_points"] / val["case_total"] * lesson_problem.score ) total_points += lesson_problem.score grades[s]["total"] = { "achieved_points": achieved_points, "total_points": total_points, "percentage": achieved_points / total_points * 100 if total_points else 0, } return grades def get_context_data(self, **kwargs): context = super(CourseStudentResultsLesson, self).get_context_data(**kwargs) context["lesson"] = self.lesson context["title"] = _("Grades of %(lesson_name)s in %(course_name)s") % { "course_name": self.course.name, "lesson_name": self.lesson.title, } context["content_title"] = mark_safe( _( "Grades of %(lesson_name)s in %(course_name)s" ) % { "course_name": self.course.name, "lesson_name": self.lesson.title, "url_course": self.course.get_absolute_url(), "url_lesson": self.lesson.get_absolute_url(), } ) 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 %(contest_name)s") % { "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], )