Coverage for website/events/api/v2/views.py: 61.24%

154 statements  

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

1from django.db.models import Prefetch, Q 

2from django.utils import timezone 

3 

4from oauth2_provider.contrib.rest_framework import IsAuthenticatedOrTokenHasScope 

5from rest_framework import filters as framework_filters 

6from rest_framework import status 

7from rest_framework.exceptions import PermissionDenied, ValidationError 

8from rest_framework.generics import ( 

9 DestroyAPIView, 

10 ListAPIView, 

11 RetrieveAPIView, 

12 get_object_or_404, 

13) 

14from rest_framework.response import Response 

15from rest_framework.views import APIView 

16 

17from events import services 

18from events.api.v2 import filters 

19from events.api.v2.serializers.event import EventListSerializer, EventSerializer 

20from events.api.v2.serializers.event_registration import EventRegistrationSerializer 

21from events.api.v2.serializers.external_event import ExternalEventSerializer 

22from events.exceptions import RegistrationError 

23from events.models import Event, EventRegistration, ExternalEvent 

24from events.services import is_user_registered 

25from members.models import Membership 

26from thaliawebsite.api.v2.permissions import IsAuthenticatedOrTokenHasScopeForMethod 

27from thaliawebsite.api.v2.serializers import EmptySerializer 

28from utils.media.services import fetch_thumbnails 

29 

30 

31class EventListView(ListAPIView): 

32 """Returns an overview of all upcoming events.""" 

33 

34 serializer_class = EventListSerializer 

35 filter_backends = ( 

36 framework_filters.OrderingFilter, 

37 framework_filters.SearchFilter, 

38 filters.EventDateFilter, 

39 filters.CategoryFilter, 

40 filters.OrganiserFilter, 

41 ) 

42 ordering_fields = ("start", "end") 

43 search_fields = ("title",) 

44 permission_classes = [IsAuthenticatedOrTokenHasScope] 

45 required_scopes = ["events:read"] 

46 

47 def get_queryset(self): 

48 events = ( 

49 Event.objects.filter(published=True) 

50 .select_related("food_event") 

51 .select_properties("participant_count") 

52 .prefetch_related( 

53 "registrationinformationfield_set", 

54 "documents", 

55 "organisers", 

56 "organisers__board", 

57 "organisers__committee", 

58 "organisers__society", 

59 "organisers__contact_mailinglist", 

60 ) 

61 ) 

62 if self.request.member: 62 ↛ 72line 62 didn't jump to line 72 because the condition on line 62 was always true

63 events = events.prefetch_related( 

64 Prefetch( 

65 "eventregistration_set", 

66 to_attr="member_registration", 

67 queryset=EventRegistration.objects.filter( 

68 member=self.request.member 

69 ).select_properties("queue_position"), 

70 ) 

71 ) 

72 return events 

73 

74 

75class EventDetailView(RetrieveAPIView): 

76 """Returns details of an event.""" 

77 

78 serializer_class = EventSerializer 

79 permission_classes = [IsAuthenticatedOrTokenHasScope] 

80 required_scopes = ["events:read"] 

81 

82 def get_queryset(self): 

83 events = Event.objects.filter(published=True) 

84 if self.request.member: 84 ↛ 94line 84 didn't jump to line 94 because the condition on line 84 was always true

85 events = events.prefetch_related( 

86 Prefetch( 

87 "eventregistration_set", 

88 to_attr="member_registration", 

89 queryset=EventRegistration.objects.filter( 

90 member=self.request.member 

91 ).select_properties("queue_position"), 

92 ) 

93 ) 

94 return events 

95 

96 

97class EventRegistrationsView(ListAPIView): 

98 """Returns a list of registrations.""" 

99 

100 serializer_class = EventRegistrationSerializer 

101 permission_classes = [IsAuthenticatedOrTokenHasScopeForMethod] 

102 required_scopes_per_method = { 

103 "GET": ["events:read"], 

104 "POST": ["events:register"], 

105 "DELETE": ["events:register"], 

106 } 

107 filter_backends = (framework_filters.OrderingFilter,) 

108 ordering_fields = ( 

109 "date", 

110 "member", 

111 ) 

112 

113 def __init__(self): 

114 super().__init__() 

115 self.event = None 

116 

117 def get_serializer_class(self): 

118 if self.request.method.lower() == "post": 

119 return EmptySerializer 

120 return super().get_serializer_class() 

121 

122 def get_queryset(self): 

123 if self.event: 

124 today = timezone.now().date() 

125 return ( 

126 EventRegistration.objects.filter(event=self.event, date_cancelled=None) 

127 .select_related("member__profile") 

128 .prefetch_related( 

129 Prefetch( 

130 "member__membership_set", 

131 queryset=Membership.objects.filter( 

132 Q(until__isnull=True) | Q(until__gt=today), since__lte=today 

133 ).order_by("-since")[:1], 

134 to_attr="_current_membership", 

135 ), 

136 )[: self.event.max_participants] 

137 ) 

138 return EventRegistration.objects.none() 

139 

140 def get_serializer(self, *args, **kwargs): 

141 if len(args) > 0: 

142 registrations = args[0] 

143 fetch_thumbnails( 

144 [r.member.profile.photo for r in registrations if r.member] 

145 ) 

146 return super().get_serializer(*args, **kwargs) 

147 

148 def initial(self, request, *args, **kwargs): 

149 """Run anything that needs to occur prior to calling the method handler.""" 

150 self.format_kwarg = self.get_format_suffix(**kwargs) 

151 

152 # Perform content negotiation and store the accepted info on the request 

153 neg = self.perform_content_negotiation(request) 

154 request.accepted_renderer, request.accepted_media_type = neg 

155 

156 # Determine the API version, if versioning is in use. 

157 version, scheme = self.determine_version(request, *args, **kwargs) 

158 request.version, request.versioning_scheme = version, scheme 

159 

160 # Ensure that the incoming request is permitted 

161 self.perform_authentication(request) 

162 

163 self.event = get_object_or_404(Event, pk=self.kwargs.get("pk"), published=True) 

164 

165 self.check_permissions(request) 

166 self.check_throttles(request) 

167 

168 def post(self, request, *args, **kwargs): 

169 try: 

170 registration = services.create_registration(request.member, self.event) 

171 serializer = EventRegistrationSerializer( 

172 instance=registration, context=self.get_serializer_context() 

173 ) 

174 return Response(serializer.data, status=status.HTTP_201_CREATED) 

175 except RegistrationError as e: 

176 raise PermissionDenied(detail=e) from e 

177 

178 

179class EventRegistrationDetailView(RetrieveAPIView, DestroyAPIView): 

180 """Returns details of an event registration.""" 

181 

182 serializer_class = EventRegistrationSerializer 

183 queryset = EventRegistration.objects.all() 

184 permission_classes = [IsAuthenticatedOrTokenHasScopeForMethod] 

185 required_scopes_per_method = { 

186 "GET": ["events:read"], 

187 "DELETE": ["events:register"], 

188 } 

189 

190 def get_queryset(self): 

191 return ( 

192 super() 

193 .get_queryset() 

194 .filter( 

195 event=self.kwargs["event_id"], 

196 event__published=True, 

197 date_cancelled=None, 

198 ) 

199 ) 

200 

201 def get_serializer(self, *args, **kwargs): 

202 if ( 

203 len(args) > 0 

204 and isinstance(args[0], EventRegistration) 

205 and args[0].member == self.request.member 

206 ): 

207 kwargs.update( 

208 fields=( 

209 "pk", 

210 "member", 

211 "name", 

212 "present", 

213 "queue_position", 

214 "date", 

215 "payment", 

216 ) 

217 ) 

218 return super().get_serializer(*args, **kwargs) 

219 

220 def delete(self, request, *args, **kwargs): 

221 if self.get_object().member != request.member: 

222 raise PermissionDenied() 

223 

224 try: 

225 services.cancel_registration(request.member, self.get_object().event) 

226 return Response(status=status.HTTP_204_NO_CONTENT) 

227 except RegistrationError as e: 

228 raise PermissionDenied(detail=e) from e 

229 

230 

231class EventRegistrationFieldsView(APIView): 

232 """Returns details of an event registration.""" 

233 

234 permission_classes = [IsAuthenticatedOrTokenHasScopeForMethod] 

235 required_scopes_per_method = { 

236 "GET": ["events:read"], 

237 "PUT": ["events:register"], 

238 "PATCH": ["events:register"], 

239 } 

240 

241 def get_object(self): 

242 return get_object_or_404( 

243 EventRegistration, 

244 event=self.kwargs["event_id"], 

245 event__published=True, 

246 pk=self.kwargs["registration_id"], 

247 member=self.request.member, 

248 ) 

249 

250 def get(self, request, *args, **kwargs): 

251 return Response( 

252 data=services.registration_fields(request, registration=self.get_object()), 

253 status=status.HTTP_200_OK, 

254 ) 

255 

256 def put(self, request, *args, **kwargs): 

257 original = services.registration_fields(request, registration=self.get_object()) 

258 required_keys = set(original.keys()) - set(request.data.keys()) 

259 if len(required_keys) > 0: 

260 raise ValidationError( 

261 f"Missing keys '{', '.join(required_keys)}' in request" 

262 ) 

263 

264 try: 

265 services.update_registration( 

266 registration=self.get_object(), field_values=request.data.items() 

267 ) 

268 

269 return Response( 

270 data=services.registration_fields( 

271 request, registration=self.get_object() 

272 ), 

273 status=status.HTTP_200_OK, 

274 ) 

275 except RegistrationError as e: 

276 raise ValidationError(e) from e 

277 

278 def patch(self, request, *args, **kwargs): 

279 try: 

280 services.update_registration( 

281 registration=self.get_object(), field_values=request.data.items() 

282 ) 

283 

284 return Response( 

285 data=services.registration_fields( 

286 request, registration=self.get_object() 

287 ), 

288 status=status.HTTP_200_OK, 

289 ) 

290 except RegistrationError as e: 

291 raise ValidationError(e) from e 

292 

293 

294class ExternalEventListView(ListAPIView): 

295 """Returns an overview of all partner events.""" 

296 

297 serializer_class = ExternalEventSerializer 

298 queryset = ExternalEvent.objects.filter(published=True) 

299 filter_backends = ( 

300 framework_filters.OrderingFilter, 

301 framework_filters.SearchFilter, 

302 filters.EventDateFilter, 

303 ) 

304 ordering_fields = ("start", "end", "title") 

305 search_fields = ("title",) 

306 permission_classes = [IsAuthenticatedOrTokenHasScope] 

307 required_scopes = ["events:read"] 

308 

309 

310class ExternalEventDetailView(RetrieveAPIView): 

311 """Returns a single partner event.""" 

312 

313 serializer_class = ExternalEventSerializer 

314 queryset = ExternalEvent.objects.filter(published=True) 

315 permission_classes = [IsAuthenticatedOrTokenHasScope] 

316 required_scopes = ["events:read"] 

317 

318 

319class MarkPresentAPIView(APIView): 

320 """A view that allows uses to mark their presence at an event using a secret token.""" 

321 

322 permission_classes = [IsAuthenticatedOrTokenHasScope] 

323 required_scopes = ["events:register"] 

324 

325 def patch(self, request, *args, **kwargs): 

326 """Mark a user as present. 

327 

328 Checks if the url is correct, the event has not ended yet, and the user is registered. 

329 """ 

330 event = get_object_or_404(Event, pk=kwargs["pk"]) 

331 if kwargs["token"] != event.mark_present_url_token: 

332 raise PermissionDenied(detail="Invalid url.") 

333 

334 if not request.member or not is_user_registered(request.member, event): 

335 raise PermissionDenied(detail="You are not registered for this event.") 

336 

337 registration = event.registrations.get( 

338 member=request.member, date_cancelled=None 

339 ) 

340 

341 if registration.present: 

342 return Response( 

343 data={"detail": "You were already marked as present."}, 

344 status=status.HTTP_200_OK, 

345 ) 

346 if event.end < timezone.now(): 

347 raise PermissionDenied( 

348 detail="This event has already ended.", 

349 ) 

350 

351 registration.present = True 

352 registration.save() 

353 return Response( 

354 data={"detail": "You have been marked as present."}, 

355 status=status.HTTP_200_OK, 

356 )