Add course
This commit is contained in:
parent
d409f0e9b4
commit
83579891b9
27 changed files with 1308 additions and 484 deletions
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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({})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue