Coverage for website/members/views.py: 55.67%

162 statements  

« prev     ^ index     » next       coverage.py v7.6.7, created at 2025-08-14 10:31 +0000

1import json 

2from datetime import date, datetime 

3 

4from django.contrib.auth.decorators import login_required 

5from django.contrib.auth.views import RedirectURLMixin 

6from django.contrib.messages.views import SuccessMessageMixin 

7from django.core.exceptions import PermissionDenied 

8from django.db.models import Q, QuerySet 

9from django.http import Http404, HttpResponse 

10from django.shortcuts import get_object_or_404 

11from django.template.response import TemplateResponse 

12from django.urls import reverse_lazy 

13from django.utils.decorators import method_decorator 

14from django.utils.translation import gettext_lazy as _ 

15from django.views.generic import CreateView, DetailView, UpdateView 

16from django.views.generic.base import TemplateResponseMixin, TemplateView, View 

17 

18import activemembers.services as activemembers_services 

19import events.services as event_services 

20import pizzas.services 

21import sales.services 

22from members import emails, services 

23from members.decorators import membership_required 

24from members.models import EmailChange, Member, Membership, Profile 

25from thaliawebsite.views import PagedView 

26from utils.media.services import fetch_thumbnails 

27from utils.snippets import datetime_to_lectureyear 

28 

29from . import models 

30from .forms import ProfileForm 

31from .services import member_achievements, member_societies 

32 

33 

34@method_decorator(login_required, "dispatch") 

35@method_decorator(membership_required, "dispatch") 

36class MembersIndex(PagedView): 

37 """View that renders the members overview.""" 

38 

39 model = Member 

40 paginate_by = 28 

41 template_name = "members/index.html" 

42 context_object_name = "members" 

43 keywords = None 

44 query_filter = "" 

45 year_range = [] 

46 

47 def setup(self, request, *args, **kwargs) -> None: 

48 super().setup(request, *args, **kwargs) 

49 current_lectureyear = datetime_to_lectureyear(date.today()) 

50 self.year_range = list( 

51 reversed(range(current_lectureyear - 5, current_lectureyear + 1)) 

52 ) 

53 self.keywords = request.GET.get("keywords", "").split() or None 

54 self.query_filter = kwargs.get("filter", None) 

55 

56 def get_queryset(self) -> QuerySet: 

57 memberships_query = Q(until__gt=datetime.now()) | Q(until=None) 

58 members_query = ~Q(id=None) 

59 

60 if self.query_filter and self.query_filter.isdigit(): 60 ↛ 61line 60 didn't jump to line 61 because the condition on line 60 was never true

61 members_query &= Q(profile__starting_year=int(self.query_filter)) 

62 memberships_query &= Q(type=Membership.MEMBER) 

63 elif self.query_filter == "older": 63 ↛ 64line 63 didn't jump to line 64 because the condition on line 63 was never true

64 members_query &= Q(profile__starting_year__lt=self.year_range[-1]) 

65 memberships_query &= Q(type=Membership.MEMBER) 

66 elif self.query_filter == "former": 

67 # Filter out all current active memberships 

68 memberships_query &= Q(type=Membership.MEMBER) | Q(type=Membership.HONORARY) 

69 memberships = Membership.objects.filter(memberships_query) 

70 members_query &= ~Q(pk__in=memberships.values("user__pk")) 

71 # Members_query contains users that are not currently (honorary)member 

72 elif self.query_filter == "benefactors": 72 ↛ 73line 72 didn't jump to line 73 because the condition on line 72 was never true

73 memberships_query &= Q(type=Membership.BENEFACTOR) 

74 elif self.query_filter == "honorary": 74 ↛ 78line 74 didn't jump to line 78 because the condition on line 74 was always true

75 memberships_query = Q(until__gt=datetime.now().date()) | Q(until=None) 

76 memberships_query &= Q(type=Membership.HONORARY) 

77 

78 if self.keywords: 78 ↛ 79line 78 didn't jump to line 79 because the condition on line 78 was never true

79 for key in self.keywords: 

80 # Works because relevant options all have `nick` in their key 

81 members_query &= ( 

82 ( 

83 Q(profile__nickname__icontains=key) 

84 & Q(profile__display_name_preference__contains="nick") 

85 ) 

86 | Q(first_name__icontains=key) 

87 | Q(last_name__icontains=key) 

88 | Q(username__icontains=key) 

89 ) 

90 

91 if self.query_filter == "former": 

92 memberships_query = Q(type=Membership.MEMBER) | Q(type=Membership.HONORARY) 

93 memberships = Membership.objects.filter(memberships_query) 

94 all_memberships = Membership.objects.all() 

95 # Only keep members that were once members, or are legacy users 

96 # that do not have any memberships at all 

97 members_query &= Q(pk__in=memberships.values("user__pk")) | ~Q( 

98 pk__in=all_memberships.values("user__pk") 

99 ) 

100 else: 

101 memberships = Membership.objects.filter(memberships_query) 

102 members_query &= Q(pk__in=memberships.values("user__pk")) 

103 members = ( 

104 Member.objects.filter(members_query) 

105 .order_by("first_name", "last_name", "pk") 

106 .select_related("profile") 

107 ) 

108 return members 

109 

110 def get_context_data(self, **kwargs) -> dict: 

111 context = super().get_context_data(**kwargs) 

112 

113 context.update( 

114 { 

115 "filter": self.query_filter, 

116 "year_range": self.year_range, 

117 "keywords": self.keywords, 

118 } 

119 ) 

120 

121 fetch_thumbnails( 

122 [x.profile.photo for x in context["object_list"] if x.profile.photo] 

123 ) 

124 

125 return context 

126 

127 

128@method_decorator(login_required, "dispatch") 

129class ProfileDetailView(DetailView): 

130 """View that renders a member's profile.""" 

131 

132 context_object_name = "member" 

133 model = Member 

134 template_name = "members/user/profile.html" 

135 

136 def setup(self, request, *args, **kwargs) -> None: 

137 if "pk" not in kwargs and request.member: 

138 kwargs["pk"] = request.member.pk 

139 elif not (request.member and request.member.has_active_membership()): 

140 raise PermissionDenied 

141 super().setup(request, *args, **kwargs) 

142 

143 def get_context_data(self, **kwargs) -> dict: 

144 context = super().get_context_data(**kwargs) 

145 member = context["member"] 

146 

147 achievements = member_achievements(member) 

148 societies = member_societies(member) 

149 

150 membership = member.current_membership 

151 membership_type = _("Unknown membership history") 

152 if membership: 

153 membership_type = membership.get_type_display() 

154 elif member.has_been_honorary_member(): 

155 membership_type = _("Former honorary member") 

156 elif member.has_been_member(): 

157 membership_type = _("Former member") 

158 elif member.latest_membership: 

159 membership_type = _("Former benefactor") 

160 

161 context.update( 

162 { 

163 "achievements": achievements, 

164 "societies": societies, 

165 "membership_type": membership_type, 

166 } 

167 ) 

168 

169 return context 

170 

171 

172@method_decorator(login_required, "dispatch") 

173class UserProfileUpdateView(RedirectURLMixin, SuccessMessageMixin, UpdateView): 

174 """View that allows a user to update their profile.""" 

175 

176 template_name = "members/user/edit_profile.html" 

177 model = Profile 

178 form_class = ProfileForm 

179 next_page = reverse_lazy("members:edit-profile") 

180 success_message = _("Your profile has been updated successfully.") 

181 

182 def get_object(self, queryset=None) -> Profile: 

183 return get_object_or_404(models.Profile, user=self.request.user) 

184 

185 def get_form_kwargs(self): 

186 kwargs = super().get_form_kwargs() 

187 kwargs["require_address"] = bool(self.request.GET.get("require_address", False)) 

188 return kwargs 

189 

190 

191@method_decorator(login_required, "dispatch") 

192class StatisticsView(TemplateView): 

193 """View that renders the statistics page.""" 

194 

195 template_name = "members/statistics.html" 

196 

197 def get_context_data(self, **kwargs) -> dict: 

198 context = super().get_context_data(**kwargs) 

199 context.update( 

200 { 

201 "total_members": models.Member.current_members.count(), 

202 "cohort_sizes": json.dumps(services.gen_stats_year()), 

203 "member_type_distribution": json.dumps( 

204 services.gen_stats_member_type() 

205 ), 

206 "committee_sizes": json.dumps( 

207 activemembers_services.generate_statistics() 

208 ), 

209 "event_categories": json.dumps( 

210 event_services.generate_category_statistics() 

211 ), 

212 "total_pizza_orders": json.dumps( 

213 pizzas.services.gen_stats_pizza_orders() 

214 ), 

215 "total_sales_orders": json.dumps( 

216 sales.services.gen_stats_sales_orders() 

217 ), 

218 "active_members": json.dumps(services.gen_stats_active_members()), 

219 } 

220 ) 

221 

222 return context 

223 

224 

225@method_decorator(login_required, name="dispatch") 

226class EmailChangeFormView(CreateView): 

227 """View that renders the email change form.""" 

228 

229 model = EmailChange 

230 fields = ["email", "member"] 

231 template_name = "members/user/email_change.html" 

232 

233 def get_initial(self) -> dict: 

234 initial = super().get_initial() 

235 initial["email"] = self.request.member.email 

236 return initial 

237 

238 def post(self, request, *args, **kwargs) -> HttpResponse: 

239 request.POST = request.POST.dict() 

240 request.POST["member"] = request.member.pk 

241 return super().post(request, *args, **kwargs) 

242 

243 def form_valid(self, form) -> HttpResponse: 

244 change_request = form.save() 

245 emails.send_email_change_confirmation_messages(change_request) 

246 return TemplateResponse( 

247 request=self.request, template="members/user/email_change_requested.html" 

248 ) 

249 

250 

251@method_decorator(login_required, name="dispatch") 

252class EmailChangeConfirmView(View, TemplateResponseMixin): 

253 """View that renders an HTML template and confirms the old email address.""" 

254 

255 template_name = "members/user/email_change_confirmed.html" 

256 

257 def get(self, request, *args, **kwargs) -> HttpResponse: 

258 if not EmailChange.objects.filter(confirm_key=kwargs["key"]).exists(): 

259 raise Http404 

260 

261 change_request = EmailChange.objects.get(confirm_key=kwargs["key"]) 

262 

263 services.confirm_email_change(change_request) 

264 

265 return self.render_to_response({}) 

266 

267 

268@method_decorator(login_required, name="dispatch") 

269class EmailChangeVerifyView(View, TemplateResponseMixin): 

270 """View that renders an HTML template and verifies the new email address.""" 

271 

272 template_name = "members/user/email_change_verified.html" 

273 

274 def get(self, request, *args, **kwargs) -> HttpResponse: 

275 if not EmailChange.objects.filter(verify_key=kwargs["key"]).exists(): 

276 raise Http404 

277 

278 change_request = EmailChange.objects.get(verify_key=kwargs["key"]) 

279 

280 services.verify_email_change(change_request) 

281 

282 return self.render_to_response({})