diff --git a/anatomy/views.py b/anatomy/views.py
index d57f25c5..a7f106a1 100644
--- a/anatomy/views.py
+++ b/anatomy/views.py
@@ -572,21 +572,6 @@ def active_exams(request):
return JsonResponse(active_exams)
-def question_save_annotation(request, pk):
- if request.is_ajax() and request.method == "POST":
- question = get_object_or_404(AnatomyQuestion, pk=pk)
-
- question.image_annotations = request.POST.get("annotation")
- print(question.image_annotations)
-
- question.save()
- data = {"status": "success"}
- return JsonResponse(data, status=200)
- else:
- data = {"status": "error"}
- return JsonResponse(data, status=400)
-
-
def exam_json(request, pk):
exam = get_object_or_404(Exam, pk=pk)
@@ -813,13 +798,6 @@ def exam_scores_cid_user(request, pk, sk):
)
-def cid_selector(request):
- return render(
- request,
- "anatomy/cid_selector.html",
- )
-
-
# @login_required
# def exam_scores(request, pk):
# exam = get_object_or_404(Exam, pk=pk)
@@ -1155,4 +1133,20 @@ class AnatomyQuestionView(SingleTableMixin, FilterView):
table_class = AnatomyQuestionTable
template_name = "anatomy/anatomy_question_view.html"
- filterset_class = AnatomyQuestionFilter
\ No newline at end of file
+ filterset_class = AnatomyQuestionFilter
+
+
+def question_save_annotation(request, pk):
+ if request.is_ajax() and request.method == "POST":
+ question = get_object_or_404(AnatomyQuestion, pk=pk)
+
+ question.image_annotations = request.POST.get("annotation")
+ print(question.image_annotations)
+
+ question.save()
+ data = {"status": "success"}
+ return JsonResponse(data, status=200)
+ else:
+ data = {"status": "error"}
+ return JsonResponse(data, status=400)
+
diff --git a/rapids/forms.py b/rapids/forms.py
index 73f15940..a2869a1c 100755
--- a/rapids/forms.py
+++ b/rapids/forms.py
@@ -1,40 +1,69 @@
-from django.forms import ModelForm, ModelMultipleChoiceField, ModelChoiceField, ChoiceField
+from django.forms import (
+ ModelForm,
+ ModelMultipleChoiceField,
+ ModelChoiceField,
+ ChoiceField,
+)
from django.forms import inlineformset_factory
-from rapids.models import Abnormality, Examination, Note, Rapid, RapidCreationDefault, RapidImage, Region, Answer
+from rapids.models import (
+ Abnormality,
+ Examination,
+ Note,
+ Rapid,
+ RapidCreationDefault,
+ RapidImage,
+ Region,
+ Answer,
+ CidUserAnswer,
+ Examm,
+)
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.forms.widgets import RadioSelect, TextInput, Textarea
+class RapidAnswerForm(ModelForm):
+ class Meta:
+ model = CidUserAnswer
+ fields = ("answer",)
+
+
+class MarkRapidQuestionForm(Form):
+ # correct = forms.CharField(required=False)
+ # half_correct = forms.CharField(required=False)
+ # incorrect = forms.CharField(required=False)
+ marked_answers = CharField(required=False)
+
+
class ExaminationForm(ModelForm):
class Meta:
model = Examination
- fields = ['examination']
+ fields = ["examination"]
class NoteForm(ModelForm):
class Meta:
model = Note
- fields = ['note']
+ fields = ["note"]
class RegionForm(ModelForm):
class Meta:
model = Region
- fields = ['name']
+ fields = ["name"]
class AbnormalityForm(ModelForm):
class Meta:
model = Abnormality
- fields = ['name']
+ fields = ["name"]
class RapidCreationDefaultForm(ModelForm):
class Meta:
model = RapidCreationDefault
- fields = ['site']
+ fields = ["site"]
exclude = ["author"]
@@ -45,46 +74,50 @@ class RapidForm(ModelForm):
# include
# in the template.
css = {
- 'all': ['css/widgets.css'],
+ "all": ["css/widgets.css"],
}
# Adding this javascript is crucial
- js = ['jsi18n.js', "tesseract.min.js"]
+ js = ["jsi18n.js", "tesseract.min.js"]
def __init__(self, *args, **kwargs):
super(RapidForm, self).__init__(*args, **kwargs)
- #self.fields['question'].widget.attrs = {'class': 'question-form', 'rows': 10, 'cols': 100}
- #self.fields['feedback'].widget.attrs = {'class': 'feedback-form', 'rows': 10, 'cols': 100}
- self.fields['abnormality'] = ModelMultipleChoiceField(
+ # self.fields['question'].widget.attrs = {'class': 'question-form', 'rows': 10, 'cols': 100}
+ # self.fields['feedback'].widget.attrs = {'class': 'feedback-form', 'rows': 10, 'cols': 100}
+ self.fields["abnormality"] = ModelMultipleChoiceField(
required=False,
queryset=Abnormality.objects.all(),
- widget=FilteredSelectMultiple(verbose_name="Abnormality",
- is_stacked=False))
+ widget=FilteredSelectMultiple(verbose_name="Abnormality", is_stacked=False),
+ )
- self.fields['region'] = ModelMultipleChoiceField(
+ self.fields["region"] = ModelMultipleChoiceField(
required=False,
queryset=Region.objects.all(),
- widget=FilteredSelectMultiple(verbose_name="Region",
- is_stacked=False))
+ widget=FilteredSelectMultiple(verbose_name="Region", is_stacked=False),
+ )
- self.fields['examination'] = ModelMultipleChoiceField(
+ self.fields["examination"] = ModelMultipleChoiceField(
queryset=Examination.objects.all(),
- widget=FilteredSelectMultiple(verbose_name="Examination",
- is_stacked=False))
+ widget=FilteredSelectMultiple(verbose_name="Examination", is_stacked=False),
+ )
- self.fields['laterality'] = ChoiceField(
- choices=Rapid.LATERALITY_CHOICES,
- required=True,
- widget=RadioSelect())
+ self.fields["laterality"] = ChoiceField(
+ choices=Rapid.LATERALITY_CHOICES, required=True, widget=RadioSelect()
+ )
class Meta:
model = Rapid
- #fields = ['due_back']
- #fields = '__all__'
+ # fields = ['due_back']
+ # fields = '__all__'
fields = [
- "normal", "abnormality", "region", "laterality", "examination",
- "site", "feedback"
+ "normal",
+ "abnormality",
+ "region",
+ "laterality",
+ "examination",
+ "site",
+ "feedback",
]
- #fields = ['question', 'feedback', 'subspecialty', 'references']
+ # fields = ['question', 'feedback', 'subspecialty', 'references']
widgets = {
# "normal": RadioSelect(
# choices=[(True, 'Yes'),
@@ -92,14 +125,16 @@ class RapidForm(ModelForm):
}
-ImageFormSet = inlineformset_factory(Rapid,
- RapidImage,
- fields=['image', 'feedback_image'],
- exclude=[],
- can_delete=True,
- extra=4,
- max_num=10,
- field_classes="testing")
+ImageFormSet = inlineformset_factory(
+ Rapid,
+ RapidImage,
+ fields=["image", "feedback_image"],
+ exclude=[],
+ can_delete=True,
+ extra=4,
+ max_num=10,
+ field_classes="testing",
+)
AnswerFormSet = inlineformset_factory(
Rapid,
diff --git a/rapids/urls.py b/rapids/urls.py
index ea47966a..f701c8f1 100755
--- a/rapids/urls.py
+++ b/rapids/urls.py
@@ -8,19 +8,36 @@ urlpatterns = [
path("", views.index, name="index"),
path("author//", views.author_detail, name="author_detail"),
path("author/", views.author_list, name="author_list"),
+ path("question/", views.RapidView.as_view(), name="rapid_view"),
#path("unchecked/", views.unchecked_list, name="unchecked_list"),
#path("verified//", views.verified_detail, name="verified_detail"),
- path("/", views.rapid_detail, name="rapid_detail"),
- path("/split", views.rapid_split, name="rapid_split"),
- path("/clone", views.RapidClone.as_view(), name="rapid_clone"),
+ path("question//", views.rapid_detail, name="rapid_detail"),
+ path("question//split", views.rapid_split, name="rapid_split"),
+ path("question//clone", views.RapidClone.as_view(), name="rapid_clone"),
#path("verified/", views.verified, name="verified"),
#path("all_questions/", views.all_questions, name="all_questions"),
- path("/scrap", views.rapid_scrap, name="rapid_scrap"),
+ path("question//scrap", views.rapid_scrap, name="rapid_scrap"),
+ path("exam///mark", views.mark, name="mark"),
+ path("exam//mark", views.mark_overview, name="mark_overview"),
+ path("exam///", views.exam_take, name="exam_take"),
+ path("exam//question//", views.exam_question_detail, name="exam_question_detail"),
+ path("exam//", views.exam_overview, name="exam_overview"),
+ path("exam//json_edit", views.exam_json_edit, name="exam_json_edit"),
+ path("exam//scores", views.exam_scores_cid,
+ name="exam_scores_cid"),
+ path("exam//scores//", views.exam_scores_cid_user,
+ name="exam_scores_cid_user"),
+ path("exam//toggle_active", views.exam_toggle_active, name="exam_toggle_active"),
+ path("exam//toggle_results_published", views.exam_toggle_results_published, name="exam_toggle_results_published"),
+ path("exam/submit", views.postExamAnswers, name="exam_answers_submit"),
+ path("exam/", views.exam_list, name="exam_list"),
+ path("exam/json/", views.active_exams, name="active_exams"),
+ path("exam/json/", views.exam_json, name="exam_json"),
+ path("exam/json//recreate", views.exam_json_recreate, name="exam_json_recreate"),
path("create/", views.RapidCreate.as_view(), name="rapid_create"),
path("create/defaults",
views.RapidCreationDefaultView.as_view(),
name="rapid_create_defaults"),
- path("view/", views.RapidView.as_view(), name="rapid_view"),
path("region/create/", views.create_region, name="create_region"),
path("region/ajax/get_region_id",
views.get_region_id,
diff --git a/rapids/views.py b/rapids/views.py
index 75914dc1..e04f1b5a 100755
--- a/rapids/views.py
+++ b/rapids/views.py
@@ -481,4 +481,523 @@ class RapidView(SingleTableMixin, FilterView):
table_class = RapidTable
template_name = "rapids/view.html"
- filterset_class = RapidFilter
\ No newline at end of file
+ filterset_class = RapidFilter
+
+
+def loadJsonAnswer(answer):
+ # As access is not restricted make sure the data appears valid
+ if (not isinstance(answer["cid"], int)) or (
+ not isinstance(answer["eid"], int) or (not isinstance(answer["ans"], str))
+ ):
+ return JsonResponse({"success": False, "error": "cid or eid or answers not defined"})
+
+ # The model should catch invalid data but this should be less intensive
+ max_int = 999999999999999999999
+ if answer["cid"] > max_int or answer["eid"] > max_int:
+ return JsonResponse({"success": False, "error": "invalid cid"})
+
+ exam = get_object_or_404(Exam, pk=answer["eid"])
+
+ if not exam.active:
+ return JsonResponse(
+ {"success": False, "error": "No active exam: {}".format(answer["eid"])}
+ )
+
+ exiting_answers = CidUserAnswer.objects.filter(
+ question__id=answer["qid"], exam__id=answer["eid"], cid=answer["cid"]
+ )
+
+ if not exiting_answers:
+ ans = CidUserAnswer(answer=answer["ans"], cid=answer["cid"])
+ ans.question_id = answer["qid"]
+ ans.exam_id = answer["eid"]
+
+ ans.full_clean()
+
+ ans.save()
+ else:
+ # Update an existing answer
+ # should never be more than one (famous last words)
+ ans = exiting_answers[0]
+ ans.answer = answer["ans"]
+ ans.full_clean()
+ ans.save()
+
+ return True
+
+
+def exam_json_edit(request, pk):
+ if request.is_ajax() and request.method == "POST":
+
+ exam = get_object_or_404(Exam, pk=pk)
+
+ if request.POST.get("set_open_access") == "true":
+ state = True
+ else:
+ state = False
+
+ questions_changed = []
+ for q in exam.exam_questions.all():
+ q.open_access = state
+ q.save()
+ questions_changed.append(q.pk)
+
+ data = {"status": "success", "questions": questions_changed, "state": state}
+ return JsonResponse(data, status=200)
+ else:
+ data = {"status": "error"}
+ return JsonResponse(data, status=400)
+
+
+@csrf_exempt
+def postExamAnswers(request):
+ if request.is_ajax and request.method == "POST":
+
+ n = 0
+ # horrible but it works
+ for k in request.POST.dict():
+ print(k)
+ for answer in json.loads(k):
+ ret = loadJsonAnswer(answer)
+
+ if ret is not True:
+ return ret
+ n = n + 1
+
+ # print(UserAnswer.objects.filter(exam__id=q["eid"]))
+ # print(request.urlencode())
+
+ return JsonResponse({"success": True, "question_count": n})
+ return JsonResponse({"success": False, "error": "Invalid data"})
+
+
+# @user_passes_test(user_is_admin, login_url="/accounts/login")
+@login_required
+def mark_overview(request, pk):
+ exam = get_object_or_404(Exam, pk=pk)
+
+ questions = exam.exam_questions.all()
+
+ return render(
+ request, "rapids/mark_overview.html", {"exam": exam, "questions": questions}
+ )
+
+
+# @user_passes_test(user_is_admin, login_url="/accounts/login")
+@login_required
+def mark(request, pk, sk):
+ exam = get_object_or_404(Exam, pk=pk)
+
+ questions = exam.exam_questions.all()
+
+ n = sk
+
+ question_details = {
+ "total": len(questions),
+ "current": n + 1,
+ }
+
+ try:
+ question = questions[sk]
+ except IndexError:
+ raise Http404("Exam question does not exist")
+
+ answers_dict = {}
+
+ for ans in question.answers.all():
+ answers_dict[ans.answer_compare] = ans
+
+ marked_answers_set = set(answers_dict.keys())
+
+ # correct_answers = [i.answer.lower() for i in question.answers.all()]
+ # half_correct_answers = [
+ # i.answer.lower() for i in question.half_mark_answers.all()
+ # ]
+ # incorrect_answers = [
+ # i.answer.lower() for i in question.incorrect_answers.all()
+ # ]
+
+ unmarked_user_answers = question.GetUnmarkedAnswers()
+
+ if request.method == "POST":
+
+ form = MarkRapidQuestionForm(request.POST)
+ # *******************************
+ # TODO: convert to JSON request
+ # *******************************
+ if form.is_valid():
+ # If skip button is pressed skip the question without marking
+ if "skip" in request.POST:
+ return redirect("rapids:mark", pk=pk, sk=n + 1)
+
+ cd = form.cleaned_data
+ # correct = cd.get("correct")
+ # half_correct = cd.get("half_correct")
+ # incorrect = cd.get("incorrect")
+ marked_answers = json.loads(cd.get("marked_answers"))
+
+ ## This is mildly dangerous as we delete all answers
+ # question.answers.all().delete()
+ # question.half_mark_answers.all().delete()
+ # question.incorrect_answers.all().delete()
+
+ # This should probably use json (or something else...)
+ # ajax.....
+ for ans in marked_answers["correct"]:
+ ans = ans.strip()
+ if ans == "":
+ continue
+
+ a = Answer.objects.filter(
+ answer__iexact=ans, question_id=question.pk
+ ).first()
+
+ if a is None:
+ a = Answer()
+ a.question_id = question.pk
+ a.answer = ans
+
+ a.status = Answer.MarkOptions.CORRECT
+ a.save()
+
+ # for ans in half_correct.split("--//--"):
+ for ans in marked_answers["half-correct"]:
+ ans = ans.strip()
+ if ans == "":
+ continue
+
+ a = Answer.objects.filter(
+ answer__iexact=ans, question_id=question.pk
+ ).first()
+
+ if a is None:
+ a = Answer()
+ a.question_id = question.pk
+ a.answer = ans
+
+ a.status = Answer.MarkOptions.HALF_MARK
+ a.save()
+
+ for ans in marked_answers["incorrect"]:
+ ans = ans.strip()
+ if ans == "":
+ continue
+
+ a = Answer.objects.filter(
+ answer__iexact=ans, question_id=question.pk
+ ).first()
+
+ if a is None:
+ a = Answer()
+ a.question_id = question.pk
+ a.answer = ans
+
+ a.status = Answer.MarkOptions.INCORRECT
+ a.save()
+ # answer = form.save(commit=False)
+ # answer.user = request.user
+ # answer.question = question
+ # answer.published_date = timezone.now()
+ # answer.save()
+ if "next" in request.POST:
+ return redirect("rapids:mark", pk=pk, sk=n + 1)
+ elif "previous" in request.POST:
+ return redirect("rapids:mark", pk=pk, sk=n - 1)
+
+ # Reset user answers (relies on the functional javascript)
+ unmarked_user_answers = set()
+ else:
+ form = MarkRapidQuestionForm()
+
+ correct_answers = question.answers.filter(status=Answer.MarkOptions.CORRECT)
+ half_mark_answers = question.answers.filter(status=Answer.MarkOptions.HALF_MARK)
+ incorrect_answers = question.answers.filter(status=Answer.MarkOptions.INCORRECT)
+
+ return render(
+ request,
+ "rapids/mark.html",
+ {
+ "exam": exam,
+ "form": form,
+ "question": question,
+ "question_details": question_details,
+ "user_answers": unmarked_user_answers,
+ "correct_answers": correct_answers,
+ "half_mark_answers": half_mark_answers,
+ "incorrect_answers": incorrect_answers,
+ },
+ )
+
+
+def exam_toggle_results_published(request, pk):
+ if request.is_ajax() and request.method == "POST":
+
+ exam = get_object_or_404(Exam, pk=pk)
+
+ exam.publish_results = (
+ True if request.POST.get("publish_results") == "true" else False
+ )
+ exam.save()
+ data = {"status": "success", "publish_results": exam.publish_results}
+ return JsonResponse(data, status=200)
+ else:
+ data = {"status": "error"}
+ return JsonResponse(data, status=400)
+
+
+def exam_toggle_active(request, pk):
+ if request.is_ajax() and request.method == "POST":
+
+ exam = get_object_or_404(Exam, pk=pk)
+
+ exam.active = True if request.POST.get("active") == "true" else False
+ exam.save()
+ data = {"status": "success", "active": exam.active}
+ return JsonResponse(data, status=200)
+ else:
+ data = {"status": "error"}
+ return JsonResponse(data, status=400)
+
+
+def active_exams(request):
+ exams = Exam.objects.all()
+ print(exams)
+
+ active_exams = {"exams": []}
+
+ for exam in exams:
+ if exam.active:
+ active_exams["exams"].append(
+ {
+ "name": exam.get_exam_name(),
+ "url": request.build_absolute_uri(exam.get_json_url()),
+ }
+ )
+
+ return JsonResponse(active_exams)
+
+
+def exam_json(request, pk):
+
+ exam = get_object_or_404(Exam, pk=pk)
+
+ if not exam.active:
+ raise Http404("No available exam")
+
+ exam_json_cache = cache.get("exam_json_{}".format(pk))
+
+ if exam_json_cache is not None and not exam.recreate_json:
+ exam_json_cache["cached"] = True
+ return JsonResponse(exam_json_cache)
+
+ questions = exam.exam_questions.all()
+
+ exam_questions = defaultdict(dict)
+
+ exam_order = []
+
+ for q in questions:
+ exam_order.append(q.id)
+
+ exam_questions[q.id] = {
+ "title": "{}".format(q.description),
+ "question": str(q.question_type),
+ "images": [image_as_base64(q.image)],
+ "annotations": [str(q.image_annotations)],
+ "type": "rapid",
+ }
+
+ exam_json = {
+ "eid": exam.id,
+ "cached": False,
+ "exam_type": "rapid",
+ "exam_name": exam.name,
+ "exam_mode": True,
+ "exam_order": exam_order,
+ "questions": exam_questions,
+ }
+
+ if exam.time_limit:
+ exam_json["exam_time"] = exam.time_limit
+
+ exam.recreate_json = False
+ exam.save()
+
+ cache.set("exam_json_{}".format(pk), exam_json, 3600)
+
+ return JsonResponse(exam_json)
+
+
+@login_required
+def exam_json_recreate(request, pk):
+ exam = get_object_or_404(Exam, pk=pk)
+
+ exam.recreate_json = True
+ exam.save()
+
+ return redirect("rapids:exam_overview", pk=pk)
+
+
+@login_required
+def exam_scores_cid(request, pk):
+ exam = get_object_or_404(Exam, pk=pk)
+
+ questions = exam.exam_questions.all()
+
+ cids = (
+ CidUserAnswer.objects.filter(question__in=questions)
+ .order_by("cid")
+ .values_list("cid", flat=True)
+ .distinct()
+ )
+
+ user_answers_and_marks = defaultdict(list)
+ user_answers_marks = defaultdict(list)
+ user_answers = defaultdict(list)
+ user_names = {}
+
+ by_question = defaultdict(list)
+ unmarked = set()
+
+ # Loop through all candidates
+ for cid in cids:
+ # Convoluted (probably...)
+ user_names[cid] = cid
+ for q in questions:
+ # Get user answer
+ s = q.cid_user_answers.filter(cid=cid).first()
+ if not s:
+ # skip if no answer
+ user_answers_marks[cid].append(0)
+ user_answers[cid].append("")
+ by_question[q].append(("", 0))
+ continue
+
+ ans = s.answer
+ answer_score = s.get_answer_score()
+ if answer_score == "unmarked":
+ index = exam.get_question_index(q)
+ unmarked.add(index)
+ user_answers[cid].append(ans)
+ user_answers_marks[cid].append(answer_score)
+ user_answers_and_marks[cid].append((ans, answer_score))
+
+ by_question[q].append((ans, answer_score))
+
+ user_scores = {}
+ for user in user_answers_marks:
+ user_scores[user] = sum(
+ [i for i in user_answers_marks[user] if i != "unmarked"]
+ )
+
+ user_scores_list = list(user_scores.values())
+
+ if len(user_scores_list) < 1:
+ mean = 0
+ median = 0
+ mode = 0
+ fig_html = ""
+ else:
+ mean = statistics.mean(user_scores_list)
+ median = statistics.median(user_scores_list)
+ try:
+ mode = statistics.mode(user_scores_list)
+ except statistics.StatisticsError:
+ mode = "No unique mode"
+
+ df = user_scores_list
+ fig = px.histogram(
+ df,
+ x=0,
+ title="{}: distribution of scores".format(exam),
+ labels={"0": "Score"},
+ height=400,
+ width=600,
+ )
+ fig_html = fig.to_html()
+
+ max_score = len(questions) * 2
+
+ return render(
+ request,
+ "rapids/exam_scores.html",
+ {
+ "cids": cids,
+ "exam": exam,
+ "unmarked": unmarked,
+ "questions": questions,
+ "by_question": by_question,
+ "user_answers": dict(user_answers),
+ "user_answers_marks": dict(user_answers_marks),
+ "user_scores": user_scores,
+ "user_scores_list": user_scores_list,
+ "user_names": user_names,
+ "user_answers_and_marks": user_answers_and_marks,
+ "max_score": max_score,
+ "mean": mean,
+ "median": median,
+ "mode": mode,
+ "plot": fig_html,
+ },
+ )
+
+
+def exam_scores_cid_user(request, pk, sk):
+ exam = get_object_or_404(Exam, pk=pk)
+
+ # TODO:Need some kind of test for cid
+ cid = sk
+
+ questions = exam.exam_questions.all()
+
+ answers_and_marks = []
+ answers_marks = []
+ answers = []
+
+ for q in questions:
+ # Get user answer
+ user_answer = q.cid_user_answers.filter(cid=cid).first()
+ if not user_answer:
+ # skip if no answer
+ answers_marks.append(0)
+ answers.append("")
+ answer_score = 0
+ ans = "Not answered"
+ else:
+ ans = user_answer.answer
+
+ answer_score = user_answer.get_answer_score()
+
+ correct_answer = q.GetPrimaryAnswer()
+
+ if not exam.publish_results:
+ correct_answer = "*****"
+ answer_score = 0
+ answers.append(ans)
+ answers_marks.append(answer_score)
+ answers_and_marks.append((ans, answer_score, correct_answer))
+
+ if "unmarked" in answers_marks:
+ answered = [i for i in answers_marks if type(i) == int]
+ total_score = sum(answered)
+ unmarked_number = len(answers_marks) - len(answered)
+ total_score = "{} ({} unmarked)".format(total_score, unmarked_number)
+ else:
+ total_score = sum(answers_marks)
+
+ max_score = len(questions) * 2
+
+ return render(
+ request,
+ "rapids/exam_scores_user.html",
+ {
+ "exam": exam,
+ "cid": cid,
+ "questions": questions,
+ "answers": answers,
+ "answers_marks": answers_marks,
+ "total_score": total_score,
+ "max_score": max_score,
+ "answers_and_marks": answers_and_marks,
+ },
+ )
\ No newline at end of file