Add course

This commit is contained in:
cuom1999 2024-02-19 17:00:44 -06:00
parent d409f0e9b4
commit 83579891b9
27 changed files with 1308 additions and 484 deletions

View file

@ -1,24 +1,68 @@
from django.utils.html import mark_safe
from django.db import models
from judge.models.course import Course
from django.views.generic import ListView
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.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
__all__ = [
"CourseList",
"CourseDetail",
"CourseResource",
"CourseResourceDetail",
"CourseStudentResults",
"CourseEdit",
"CourseResourceDetailEdit",
"CourseResourceEdit",
]
course_directory_file = ""
from judge.models import Course, CourseLesson, Submission, Profile, CourseRole
from judge.models.course import RoleInCourse
from judge.widgets import HeavyPreviewPageDownWidget, HeavySelect2MultipleWidget
from judge.utils.problems import (
user_attempted_ids,
user_completed_ids,
)
class CourseListMixin(object):
def get_queryset(self):
return Course.objects.filter(is_open="true").values()
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 = 0
total_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
problem_points = max_case_points_per_problem(profile, problems)
num_problems = len(problems)
percentage = 0
for val in problem_points.values():
score = val["case_points"] / val["case_total"]
percentage += score / num_problems
res[lesson.id] = {
"achieved_points": percentage * lesson.points,
"percentage": percentage * 100,
}
total_achieved_points += percentage * lesson.points
total_points += lesson.points
res["total"] = {
"achieved_points": total_achieved_points,
"total_points": total_points,
"percentage": total_achieved_points / total_points * 100,
}
return res
class CourseList(ListView):
@ -28,12 +72,179 @@ class CourseList(ListView):
def get_context_data(self, **kwargs):
context = super(CourseList, self).get_context_data(**kwargs)
available, enrolling = [], []
for course in Course.objects.filter(is_public=True).filter(is_open=True):
if Course.is_accessible_by(course, self.request.profile):
enrolling.append(course)
else:
available.append(course)
context["available"] = available
context["enrolling"] = enrolling
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["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()
)
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 EditCourseLessonsView(CourseEditableMixin, FormView):
template_name = "course/edit_lesson.html"
form_class = CourseLessonFormSet
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
)
else:
context["formset"] = self.form_class(
instance=self.course, queryset=self.course.lessons.order_by("order")
)
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)
if formset.is_valid():
formset.save()
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"] = 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

View file

@ -131,6 +131,13 @@ class OrganizationMixin(OrganizationBase):
context["can_edit"] = self.can_edit_organization(self.organization)
context["organization"] = self.organization
context["logo_override_image"] = self.organization.logo_override_image
context["organization_subdomain"] = (
("http" if settings.DMOJ_SSL == 0 else "https")
+ "://"
+ self.organization.slug
+ "."
+ get_current_site(self.request).domain
)
if "organizations" in context:
context.pop("organizations")
return context
@ -274,14 +281,6 @@ class OrganizationHome(OrganizationHomeView, FeedView):
def get_context_data(self, **kwargs):
context = super(OrganizationHome, self).get_context_data(**kwargs)
context["title"] = self.organization.name
http = "http" if settings.DMOJ_SSL == 0 else "https"
context["organization_subdomain"] = (
http
+ "://"
+ self.organization.slug
+ "."
+ get_current_site(self.request).domain
)
now = timezone.now()
visible_contests = (
@ -737,7 +736,7 @@ class AddOrganizationMember(
def form_valid(self, form):
new_users = form.cleaned_data["new_users"]
self.object.members.add(*new_users)
with transaction.atomic(), revisions.create_revision():
with revisions.create_revision():
revisions.set_comment(_("Added members from site"))
revisions.set_user(self.request.user)
return super(AddOrganizationMember, self).form_valid(form)
@ -804,7 +803,7 @@ class EditOrganization(
return form
def form_valid(self, form):
with transaction.atomic(), revisions.create_revision():
with revisions.create_revision():
revisions.set_comment(_("Edited from site"))
revisions.set_user(self.request.user)
return super(EditOrganization, self).form_valid(form)
@ -836,7 +835,7 @@ class AddOrganization(LoginRequiredMixin, TitleMixin, CreateView):
% settings.DMOJ_USER_MAX_ORGANIZATION_ADD,
status=400,
)
with transaction.atomic(), revisions.create_revision():
with revisions.create_revision():
revisions.set_comment(_("Added from site"))
revisions.set_user(self.request.user)
res = super(AddOrganization, self).form_valid(form)
@ -861,7 +860,7 @@ class AddOrganizationContest(
return kwargs
def form_valid(self, form):
with transaction.atomic(), revisions.create_revision():
with revisions.create_revision():
revisions.set_comment(_("Added from site"))
revisions.set_user(self.request.user)
@ -954,7 +953,7 @@ class EditOrganizationContest(
return self.contest
def form_valid(self, form):
with transaction.atomic(), revisions.create_revision():
with revisions.create_revision():
revisions.set_comment(_("Edited from site"))
revisions.set_user(self.request.user)
res = super(EditOrganizationContest, self).form_valid(form)
@ -1015,7 +1014,7 @@ class AddOrganizationBlog(
return _("Add blog for %s") % self.organization.name
def form_valid(self, form):
with transaction.atomic(), revisions.create_revision():
with revisions.create_revision():
res = super(AddOrganizationBlog, self).form_valid(form)
self.object.is_organization_private = True
self.object.authors.add(self.request.profile)
@ -1038,6 +1037,11 @@ class AddOrganizationBlog(
)
return res
def get_success_url(self):
return reverse(
"organization_home", args=[self.organization.id, self.organization.slug]
)
class EditOrganizationBlog(
LoginRequiredMixin,
@ -1115,13 +1119,18 @@ class EditOrganizationBlog(
make_notification(posible_users, action, html, self.request.profile)
def form_valid(self, form):
with transaction.atomic(), revisions.create_revision():
with revisions.create_revision():
res = super(EditOrganizationBlog, self).form_valid(form)
revisions.set_comment(_("Edited from site"))
revisions.set_user(self.request.user)
self.create_notification("Edit blog")
return res
def get_success_url(self):
return reverse(
"organization_home", args=[self.organization.id, self.organization.slug]
)
class PendingBlogs(
LoginRequiredMixin,

View file

@ -409,7 +409,7 @@ def edit_profile(request):
request.POST, request.FILES, instance=profile, user=request.user
)
if form_user.is_valid() and form.is_valid():
with transaction.atomic(), revisions.create_revision():
with revisions.create_revision():
form_user.save()
form.save()
revisions.set_user(request.user)

View file

@ -19,15 +19,14 @@ def vote_problem(request):
except Exception as e:
return HttpResponseBadRequest()
with transaction.atomic():
vote, _ = VolunteerProblemVote.objects.get_or_create(
voter=request.profile,
problem=problem,
defaults={"knowledge_points": 0, "thinking_points": 0},
)
vote.knowledge_points = knowledge_points
vote.thinking_points = thinking_points
vote.feedback = feedback
vote.types.set(types)
vote.save()
vote, _ = VolunteerProblemVote.objects.get_or_create(
voter=request.profile,
problem=problem,
defaults={"knowledge_points": 0, "thinking_points": 0},
)
vote.knowledge_points = knowledge_points
vote.thinking_points = thinking_points
vote.feedback = feedback
vote.types.set(types)
vote.save()
return JsonResponse({})