Add order and score for course problems (#124)

* Add order and grade for course problems

* Fix delete problems bug
This commit is contained in:
Phuoc Anh Kha Le 2024-09-03 21:26:20 +07:00 committed by GitHub
parent 67888bcd27
commit c833dc06d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 483 additions and 102 deletions

View file

@ -0,0 +1,44 @@
# Generated by Django 3.2.21 on 2024-09-02 05:28
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("judge", "0191_deprecate_old_org_image"),
]
operations = [
migrations.CreateModel(
name="CourseLessonProblem",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("order", models.IntegerField(default=0, verbose_name="order")),
("score", models.IntegerField(default=0, verbose_name="score")),
(
"lesson",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="lesson_problems",
to="judge.courselesson",
),
),
(
"problem",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="judge.problem"
),
),
],
),
]

View file

@ -61,7 +61,7 @@ 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
from judge.models.course import Course, CourseRole, CourseLesson, CourseLessonProblem
from judge.models.notification import Notification, NotificationProfile
from judge.models.test_formatter import TestFormatterModel

View file

@ -165,3 +165,12 @@ class CourseLesson(models.Model):
problems = models.ManyToManyField(Problem, verbose_name=_("problem"), blank=True)
order = models.IntegerField(verbose_name=_("order"), default=0)
points = models.IntegerField(verbose_name=_("points"))
class CourseLessonProblem(models.Model):
lesson = models.ForeignKey(
CourseLesson, on_delete=models.CASCADE, related_name="lesson_problems"
)
problem = models.ForeignKey(Problem, on_delete=models.CASCADE)
order = models.IntegerField(verbose_name=_("order"), default=0)
score = models.IntegerField(verbose_name=_("score"), default=0)

View file

@ -4,16 +4,32 @@ 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
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
from judge.models import (
Course,
CourseLesson,
Submission,
Profile,
CourseRole,
CourseLessonProblem,
)
from judge.models.course import RoleInCourse
from judge.widgets import HeavyPreviewPageDownWidget, HeavySelect2MultipleWidget
from judge.widgets import (
HeavyPreviewPageDownWidget,
HeavySelect2MultipleWidget,
HeavySelect2Widget,
)
from judge.utils.problems import (
user_attempted_ids,
user_completed_ids,
@ -36,32 +52,35 @@ def max_case_points_per_problem(profile, problems):
def calculate_lessons_progress(profile, lessons):
res = {}
total_achieved_points = 0
total_points = 0
total_achieved_points = total_lesson_points = 0
for lesson in lessons:
problems = list(lesson.problems.all())
if not problems:
res[lesson.id] = {"achieved_points": 0, "percentage": 0}
total_points += lesson.points
continue
problems = lesson.lesson_problems.values_list("problem", flat=True)
problem_points = max_case_points_per_problem(profile, problems)
num_problems = len(problems)
percentage = 0
for val in problem_points.values():
if val["case_total"] > 0:
score = val["case_points"] / val["case_total"]
percentage += score / num_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": percentage * lesson.points,
"percentage": percentage * 100,
"achieved_points": achieved_points,
"total_points": total_points,
"percentage": achieved_points / total_points * 100 if total_points else 0,
}
total_achieved_points += percentage * lesson.points
total_points += lesson.points
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_points,
"percentage": total_achieved_points / total_points * 100 if total_points else 0,
"total_points": total_lesson_points,
"percentage": total_achieved_points / total_lesson_points * 100
if total_lesson_points
else 0,
}
return res
@ -157,12 +176,13 @@ class CourseLessonDetail(CourseDetailMixin, DetailView):
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.problems.all()
profile, self.lesson.lesson_problems.values_list("problem", flat=True)
)
return context
@ -183,20 +203,65 @@ CourseLessonFormSet = inlineformset_factory(
)
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
}
@ -213,8 +278,22 @@ class EditCourseLessonsView(CourseEditableMixin, FormView):
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)
@ -239,7 +318,10 @@ class CourseStudentResults(CourseEditableMixin, DetailView):
def get_context_data(self, **kwargs):
context = super(CourseStudentResults, self).get_context_data(**kwargs)
context["title"] = mark_safe(
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,
@ -249,3 +331,61 @@ class CourseStudentResults(CourseEditableMixin, DetailView):
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