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

1import os 

2from datetime import date, datetime 

3 

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 

14 

15from queryable_properties.utils import prefetch_queryable_properties 

16 

17from members.decorators import membership_required 

18from utils.media.services import get_media_url 

19 

20from . import emails 

21from .forms import AddExamForm, AddSummaryForm 

22from .models import Category, Course, Exam, Summary 

23 

24 

25class CourseIndexView(ListView): 

26 """Render an overview of the courses.""" 

27 

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 

38 

39 template_name = "education/courses.html" 

40 

41 def get_ordering(self) -> str: 

42 return "name" 

43 

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 

71 

72 

73class CourseDetailView(DetailView): 

74 """Render the detail page of one specific course.""" 

75 

76 model = Course 

77 context_object_name = "course" 

78 template_name = "education/course.html" 

79 

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 

120 

121 

122@method_decorator(login_required, "dispatch") 

123@method_decorator(membership_required, "dispatch") 

124class ExamDetailView(DetailView): 

125 """Fetch and output the specified exam.""" 

126 

127 model = Exam 

128 

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() 

134 

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)) 

138 

139 

140@method_decorator(login_required, "dispatch") 

141@method_decorator(membership_required, "dispatch") 

142class SummaryDetailView(DetailView): 

143 """Fetch and output the specified summary.""" 

144 

145 model = Summary 

146 

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() 

152 

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)) 

156 

157 

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.""" 

162 

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.") 

168 

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 

174 

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) 

182 

183 

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.""" 

188 

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.") 

194 

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 

200 

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) 

208 

209 

210@method_decorator(login_required, "dispatch") 

211class BookInfoView(TemplateView): 

212 """Render a page with information about book sale. 

213 

214 Only available to members and to-be members 

215 """ 

216 

217 template_name = "education/books.html" 

218 

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