NDOJ/judge/views/course.py
Phuoc Anh Kha Le c833dc06d9
Add order and score for course problems (#124)
* Add order and grade for course problems

* Fix delete problems bug
2024-09-03 09:26:20 -05:00

391 lines
13 KiB
Python

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
from django.db.models import Max, F
from django.core.exceptions import ObjectDoesNotExist
from judge.models import (
Course,
CourseLesson,
Submission,
Profile,
CourseRole,
CourseLessonProblem,
)
from judge.models.course import RoleInCourse
from judge.widgets import (
HeavyPreviewPageDownWidget,
HeavySelect2MultipleWidget,
HeavySelect2Widget,
)
from judge.utils.problems import (
user_attempted_ids,
user_completed_ids,
)
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
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.prefetch_related("problems").all()
context["title"] = self.course.name
context["page_type"] = "home"
context["lessons"] = lessons
context["lesson_progress"] = calculate_lessons_progress(
self.request.profile, lessons
)
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"]
)
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", "points", "content", "problems"]
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 <a href='%(url)s'>%(course_name)s</a>")
% {
"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.prefetch_related("problems").all()
grades = {s: calculate_lessons_progress(s, lessons) for s in students}
return grades
def get_context_data(self, **kwargs):
context = super(CourseStudentResults, self).get_context_data(**kwargs)
context["title"] = _("Grades in %(course_name)s</a>") % {
"course_name": self.course.name,
}
context["content_title"] = mark_safe(
_("Grades in <a href='%(url)s'>%(course_name)s</a>")
% {
"course_name": self.course.name,
"url": self.course.get_absolute_url(),
}
)
context["page_type"] = "grades"
context["grades"] = self.get_grades()
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</a> in %(course_name)s</a>") % {
"course_name": self.course.name,
"lesson_name": self.lesson.title,
}
context["content_title"] = mark_safe(
_("Grades of %(lesson_name)s</a> in <a href='%(url)s'>%(course_name)s</a>")
% {
"course_name": self.course.name,
"lesson_name": self.lesson.title,
"url": self.course.get_absolute_url(),
}
)
context["page_type"] = "grades"
context["grades"] = self.get_lesson_grades()
return context