11from django .http import JsonResponse
2+ from django .conf import settings
23from django .views import View
34from django .urls import reverse
45from django .utils import timezone
56from django_email_learning .personalised .api .serializers import (
67 QuizSubmissionRequest ,
78 QuestionResponse ,
89)
10+ from django .utils .decorators import method_decorator
11+ from django .views .decorators .csrf import csrf_exempt
912from django_email_learning .services .metrics_service import metric_service
1013from django_email_learning .services import jwt_service
1114from django .utils .translation import gettext as _
@@ -204,15 +207,28 @@ def post(self, request, *args, **kwargs): # type: ignore[no-untyped-def]
204207 token = serializer .token
205208 answers = serializer .answers
206209
207- submited_question_ids = {response .id for response in answers }
210+ response_payload , error_response = self .process_quiz_submission (
211+ token = token ,
212+ answers = answers ,
213+ )
214+ if error_response :
215+ return error_response
216+
217+ return JsonResponse (response_payload , status = 200 )
218+
219+ @classmethod
220+ def process_quiz_submission (
221+ cls , token : str , answers : list [QuestionResponse ]
222+ ) -> tuple [dict | None , JsonResponse | None ]:
223+ submitted_question_ids = {response .id for response in answers }
208224 response_map = {response .id : response .answers for response in answers }
209225
210226 try :
211227 decoded = jwt_service .decode_jwt (token = token )
212228 except jwt_service .InvalidTokenException as jde :
213- return JsonResponse ({"error" : str (jde )}, status = 400 )
229+ return None , JsonResponse ({"error" : str (jde )}, status = 400 )
214230 except jwt_service .ExpiredTokenException as ete :
215- return JsonResponse ({"error" : str (ete )}, status = 410 )
231+ return None , JsonResponse ({"error" : str (ete )}, status = 410 )
216232
217233 delivery_id = decoded ["delivery_id" ]
218234
@@ -221,7 +237,7 @@ def post(self, request, *args, **kwargs): # type: ignore[no-untyped-def]
221237 id = delivery_id , hash_value = decoded ["delivery_hash" ]
222238 )
223239 except ContentDelivery .DoesNotExist :
224- return JsonResponse (
240+ return None , JsonResponse (
225241 {
226242 "error" : "The content delivery associated with this token does not exist."
227243 },
@@ -230,23 +246,25 @@ def post(self, request, *args, **kwargs): # type: ignore[no-untyped-def]
230246
231247 enrollment = delivery .enrollment
232248 if enrollment .status != EnrollmentStatus .ACTIVE :
233- return JsonResponse ({"error" : "Quiz is not valid anymore" }, status = 400 )
249+ return None , JsonResponse (
250+ {"error" : "Quiz is not valid anymore" }, status = 400
251+ )
234252
235253 quiz = delivery .course_content .quiz
236254 if not quiz :
237- return JsonResponse (
255+ return None , JsonResponse (
238256 {"error" : "No quiz associated with this link" }, status = 500
239257 )
240258
241259 try :
242- score , passed = self .calculate_score_and_passed (
260+ score , passed = cls .calculate_score_and_passed (
243261 quiz , answers , decoded .get ("question_ids" )
244262 )
245263 logger .info (
246264 f"Learner ID { enrollment .learner .id } submitted quiz for Course { enrollment .course .title } with score { score } . Passed: { passed } "
247265 )
248266 except ValueError as ve :
249- return JsonResponse ({"error" : str (ve )}, status = 500 )
267+ return None , JsonResponse ({"error" : str (ve )}, status = 500 )
250268
251269 QuizSubmission .objects .create (
252270 delivery = delivery ,
@@ -271,9 +289,9 @@ def post(self, request, *args, **kwargs): # type: ignore[no-untyped-def]
271289 if (quiz .is_blocking and passed ) or QuizSubmission .objects .filter (
272290 delivery = delivery
273291 ).count () == 1 :
274- delivery = delivery .schedule_next_delivery ()
292+ new_delivery = delivery .schedule_next_delivery ()
275293
276- if not delivery :
294+ if not new_delivery :
277295 enrollment .graduate ()
278296 else :
279297 failed_submissions_count = QuizSubmission .objects .filter (
@@ -328,7 +346,7 @@ def post(self, request, *args, **kwargs): # type: ignore[no-untyped-def]
328346 is_passed = passed ,
329347 is_blocking = quiz .is_blocking ,
330348 )
331- return JsonResponse (
349+ return (
332350 {
333351 "score" : score ,
334352 "passed" : passed ,
@@ -353,14 +371,14 @@ def post(self, request, *args, **kwargs): # type: ignore[no-untyped-def]
353371 ],
354372 }
355373 for question in quiz .questions .filter (
356- id__in = submited_question_ids
374+ id__in = submitted_question_ids
357375 )
358376 ],
359377 }
360378 if not quiz .is_blocking
361379 else None ,
362380 },
363- status = 200 ,
381+ None ,
364382 )
365383
366384 @staticmethod
@@ -420,6 +438,101 @@ def calculate_score_and_passed(
420438 return score , passed
421439
422440
441+ @method_decorator (csrf_exempt , name = "dispatch" )
442+ class AmpQuizSubmissionView (View ):
443+ @staticmethod
444+ def _set_amp_headers (response : JsonResponse , source_origin : str ) -> JsonResponse :
445+ response ["Access-Control-Allow-Origin" ] = source_origin
446+ response ["AMP-Access-Control-Allow-Source-Origin" ] = source_origin
447+ response ["Access-Control-Allow-Credentials" ] = "true"
448+ return response
449+
450+ def _amp_json_response (
451+ self , payload : dict , status : int , source_origin : str
452+ ) -> JsonResponse :
453+ response = JsonResponse (payload , status = status )
454+ return self ._set_amp_headers (response = response , source_origin = source_origin )
455+
456+ def post (self , request , * args , ** kwargs ): # type: ignore[no-untyped-def]
457+ source_origin = request .GET .get ("__amp_source_origin" )
458+ if not source_origin :
459+ return JsonResponse (
460+ {"error" : _ ("Missing __amp_source_origin query parameter." )},
461+ status = 400 ,
462+ )
463+
464+ trusted_origins = {
465+ trusted_origin .rstrip ("/" )
466+ for trusted_origin in settings .CSRF_TRUSTED_ORIGINS
467+ }
468+ normalized_source_origin = source_origin .rstrip ("/" )
469+ if normalized_source_origin not in trusted_origins :
470+ return JsonResponse (
471+ {"error" : _ ("Invalid __amp_source_origin. Origin is not trusted." )},
472+ status = 400 ,
473+ )
474+
475+ token = request .POST .get ("token" )
476+ if not token :
477+ return self ._amp_json_response (
478+ {"error" : _ ("Token is required." )},
479+ status = 400 ,
480+ source_origin = normalized_source_origin ,
481+ )
482+
483+ answers_map : dict [int , set [int ]] = {}
484+
485+ for key , values in request .POST .lists ():
486+ if key == "token" :
487+ continue
488+
489+ try :
490+ question_id = int (key )
491+ except (TypeError , ValueError ):
492+ return self ._amp_json_response (
493+ {"error" : _ ("Invalid question ID in form payload." )},
494+ status = 400 ,
495+ source_origin = normalized_source_origin ,
496+ )
497+
498+ parsed_answers : set [int ] = set ()
499+ for value in values :
500+ if value in (None , "" ):
501+ continue
502+
503+ try :
504+ parsed_answers .add (int (value ))
505+ except (TypeError , ValueError ):
506+ return self ._amp_json_response (
507+ {"error" : _ ("Invalid answer ID in form payload." )},
508+ status = 400 ,
509+ source_origin = normalized_source_origin ,
510+ )
511+
512+ answers_map [question_id ] = parsed_answers
513+
514+ answers = [
515+ QuestionResponse (id = question_id , answers = answer_ids )
516+ for question_id , answer_ids in answers_map .items ()
517+ ]
518+
519+ response_payload , error_response = QuizSubmissionView .process_quiz_submission (
520+ token = token ,
521+ answers = answers ,
522+ )
523+ if error_response :
524+ return self ._set_amp_headers (
525+ response = error_response ,
526+ source_origin = normalized_source_origin ,
527+ )
528+
529+ return self ._amp_json_response (
530+ response_payload ,
531+ status = 200 ,
532+ source_origin = normalized_source_origin ,
533+ )
534+
535+
423536class SubmitCertificateFormView (View ):
424537 def post (self , request , * args , ** kwargs ): # type: ignore[no-untyped-def]
425538 payload = json .loads (request .body )
0 commit comments