Coverage for website/education/views.py: 45.93%
123 statements
« prev ^ index » next coverage.py v7.6.7, created at 2025-08-14 10:31 +0000
« prev ^ index » next coverage.py v7.6.7, created at 2025-08-14 10:31 +0000
1import os
2from datetime import date, datetime
4from django.contrib.auth.decorators import login_required
5from django.contrib.messages.views import SuccessMessageMixin
6from django.core.exceptions import PermissionDenied
7from django.http import HttpResponse
8from django.shortcuts import redirect
9from django.urls import reverse_lazy
10from django.utils import timezone
11from django.utils.decorators import method_decorator
12from django.utils.translation import gettext_lazy as _
13from django.views.generic import CreateView, DetailView, ListView, TemplateView
15from queryable_properties.utils import prefetch_queryable_properties
17from members.decorators import membership_required
18from utils.media.services import get_media_url
20from . import emails
21from .forms import AddExamForm, AddSummaryForm
22from .models import Category, Course, Exam, Summary
25class CourseIndexView(ListView):
26 """Render an overview of the courses."""
28 def get_queryset(self):
29 queryset = (
30 Course.objects.filter(until=None)
31 .prefetch_related("categories", "old_courses")
32 .select_properties("summary_count", "exam_count")
33 .order_by("name")
34 )
35 prefetch_queryable_properties(queryset, "old_courses__summary_count")
36 prefetch_queryable_properties(queryset, "old_courses__exam_count")
37 return queryset
39 template_name = "education/courses.html"
41 def get_ordering(self) -> str:
42 return "name"
44 def get_context_data(self, **kwargs) -> dict:
45 context = super().get_context_data(**kwargs)
46 context.update(
47 {
48 "courses": [
49 {
50 "course_code": x.course_code,
51 "name": x.name,
52 "categories": x.categories.all(),
53 "document_count": sum(
54 [
55 x.summary_count,
56 x.exam_count,
57 ]
58 + [
59 c.summary_count + c.exam_count
60 for c in x.old_courses.all()
61 ]
62 ),
63 "url": x.get_absolute_url(),
64 }
65 for x in context["object_list"]
66 ],
67 "categories": Category.objects.all(),
68 }
69 )
70 return context
73class CourseDetailView(DetailView):
74 """Render the detail page of one specific course."""
76 model = Course
77 context_object_name = "course"
78 template_name = "education/course.html"
80 def get_context_data(self, **kwargs) -> dict:
81 context = super().get_context_data(**kwargs)
82 obj = context["course"]
83 courses = list(obj.old_courses.all())
84 courses.append(obj)
85 items = {}
86 for course in courses:
87 for summary in course.summary_set.filter(accepted=True):
88 if summary.year not in items:
89 items[summary.year] = {
90 "summaries": [],
91 "exams": [],
92 "legacy": course if course.pk != obj.pk else None,
93 }
94 items[summary.year]["summaries"].append(
95 {
96 "year": summary.year,
97 "name": summary.name,
98 "language": summary.language,
99 "id": summary.id,
100 }
101 )
102 for exam in course.exam_set.filter(accepted=True):
103 if exam.year not in items:
104 items[exam.year] = {
105 "summaries": [],
106 "exams": [],
107 "legacy": course if course.pk != obj.pk else None,
108 }
109 items[exam.year]["exams"].append(
110 {
111 "type": "exam",
112 "year": exam.year,
113 "name": f"{exam.get_type_display()} {exam.name}",
114 "language": exam.language,
115 "id": exam.id,
116 }
117 )
118 context.update({"items": sorted(items.items(), key=lambda x: x[0])})
119 return context
122@method_decorator(login_required, "dispatch")
123@method_decorator(membership_required, "dispatch")
124class ExamDetailView(DetailView):
125 """Fetch and output the specified exam."""
127 model = Exam
129 def get(self, request, *args, **kwargs) -> HttpResponse:
130 response = super().get(request, *args, **kwargs)
131 obj = response.context_data["object"]
132 obj.download_count += 1
133 obj.save()
135 ext = os.path.splitext(obj.file.name)[1]
136 filename = f"{obj.course.name}-summary{obj.year}{ext}"
137 return redirect(get_media_url(obj.file, attachment=filename))
140@method_decorator(login_required, "dispatch")
141@method_decorator(membership_required, "dispatch")
142class SummaryDetailView(DetailView):
143 """Fetch and output the specified summary."""
145 model = Summary
147 def get(self, request, *args, **kwargs) -> HttpResponse:
148 response = super().get(request, *args, **kwargs)
149 obj = response.context_data["object"]
150 obj.download_count += 1
151 obj.save()
153 ext = os.path.splitext(obj.file.name)[1]
154 filename = f"{obj.course.name}-summary{obj.year}{ext}"
155 return redirect(get_media_url(obj.file, attachment=filename))
158@method_decorator(login_required, "dispatch")
159@method_decorator(membership_required, "dispatch")
160class ExamCreateView(SuccessMessageMixin, CreateView):
161 """Render the form to submit a new exam."""
163 model = Exam
164 form_class = AddExamForm
165 template_name = "education/add_exam.html"
166 success_url = reverse_lazy("education:submit-exam")
167 success_message = _("Exam submitted successfully.")
169 def get_initial(self) -> dict:
170 initial = super().get_initial()
171 initial["exam_date"] = date.today()
172 initial["course"] = self.kwargs.get("pk", None)
173 return initial
175 def form_valid(self, form) -> HttpResponse:
176 self.object = form.save(commit=False)
177 self.object.uploader = self.request.member
178 self.object.uploader_date = datetime.now()
179 self.object.save()
180 emails.send_document_notification(self.object)
181 return super().form_valid(form)
184@method_decorator(login_required, "dispatch")
185@method_decorator(membership_required, "dispatch")
186class SummaryCreateView(SuccessMessageMixin, CreateView):
187 """Render the form to submit a new summary."""
189 model = Summary
190 form_class = AddSummaryForm
191 template_name = "education/add_summary.html"
192 success_url = reverse_lazy("education:submit-summary")
193 success_message = _("Summary submitted successfully.")
195 def get_initial(self):
196 initial = super().get_initial()
197 initial["author"] = self.request.member.get_full_name()
198 initial["course"] = self.kwargs.get("pk", None)
199 return initial
201 def form_valid(self, form) -> HttpResponse:
202 self.object = form.save(commit=False)
203 self.object.uploader = self.request.member
204 self.object.uploader_date = datetime.now()
205 self.object.save()
206 emails.send_document_notification(self.object)
207 return super().form_valid(form)
210@method_decorator(login_required, "dispatch")
211class BookInfoView(TemplateView):
212 """Render a page with information about book sale.
214 Only available to members and to-be members
215 """
217 template_name = "education/books.html"
219 def dispatch(self, request, *args, **kwargs) -> HttpResponse:
220 if request.member.has_active_membership() or (
221 request.member.earliest_membership
222 and request.member.earliest_membership.since > timezone.now().date()
223 ):
224 return super().dispatch(request, *args, **kwargs)
225 raise PermissionDenied