', image_object)
+
+
+class RapidTable(tables.Table):
+ edit = tables.LinkColumn('rapids:rapid_update',
+ text='Edit',
+ args=[A('pk')],
+ orderable=False)
+ view = tables.LinkColumn('rapids:rapid_detail',
+ text='View',
+ args=[A('pk')],
+ orderable=False)
+ clone = tables.LinkColumn('rapids:rapid_clone',
+ text='Clone',
+ args=[A('pk')],
+ orderable=False)
+ images = ImageColumn("images", orderable=False)
+
+ class Meta:
+ model = Rapid
+ template_name = "django_tables2/bootstrap4.html"
+ fields = ("normal", "abnormality", "region", "examination",
+ "laterality", "site", "created_date", "author")
+ sequence = ("view", "images")
\ No newline at end of file
diff --git a/longs/templates/admin/base_site.html b/longs/templates/admin/base_site.html
new file mode 100755
index 00000000..792249f6
--- /dev/null
+++ b/longs/templates/admin/base_site.html
@@ -0,0 +1,5 @@
+{% extends "admin/base_site.html" %}
+{% load static %}
+{% block extrahead %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/longs/templates/rapids/author_list.html b/longs/templates/rapids/author_list.html
new file mode 100755
index 00000000..82564136
--- /dev/null
+++ b/longs/templates/rapids/author_list.html
@@ -0,0 +1,8 @@
+{% extends 'rapids/base.html' %}
+
+{% block content %}
+
+{% for author in authors %}
+Author: {{author.username}} ,
+{% endfor %}
+{% endblock %}
\ No newline at end of file
diff --git a/longs/templates/rapids/base.html b/longs/templates/rapids/base.html
new file mode 100755
index 00000000..9b974595
--- /dev/null
+++ b/longs/templates/rapids/base.html
@@ -0,0 +1,27 @@
+{% extends 'base.html' %}
+
+{% block title %}
+Rapids
+{% endblock %}
+
+{% block css %}
+{% endblock %}
+
+{% block js %}
+{% endblock %}
+
+
+
+{% block content %}
+{% endblock %}
+{% block navigation %}
+Rapids:
+{% if request.user.is_authenticated %}
+Exams /
+Questions /
+Create Question
+{% endif %}
+{% comment %}
+Questions by:
+author {% endcomment %}
+{% endblock %}
\ No newline at end of file
diff --git a/longs/templates/rapids/category_detail.html b/longs/templates/rapids/category_detail.html
new file mode 100755
index 00000000..0d8e840e
--- /dev/null
+++ b/longs/templates/rapids/category_detail.html
@@ -0,0 +1,13 @@
+{% extends 'rapids/base.html' %}
+
+{% block content %}
+{{category}}:
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/longs/templates/rapids/create_simple.html b/longs/templates/rapids/create_simple.html
new file mode 100755
index 00000000..14e906af
--- /dev/null
+++ b/longs/templates/rapids/create_simple.html
@@ -0,0 +1,5 @@
+Add new {{name}}
+
\ No newline at end of file
diff --git a/longs/templates/rapids/exam.html b/longs/templates/rapids/exam.html
new file mode 100644
index 00000000..5636dc22
--- /dev/null
+++ b/longs/templates/rapids/exam.html
@@ -0,0 +1,79 @@
+{% extends 'rapids/base.html' %}
+
+{% block content %}
+
+
+Question {{question_details.current}} of {{question_details.total}}
+{{ question.question_type.first }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/longs/templates/rapids/exam_list.html b/longs/templates/rapids/exam_list.html
new file mode 100644
index 00000000..ff2cefdf
--- /dev/null
+++ b/longs/templates/rapids/exam_list.html
@@ -0,0 +1,30 @@
+{% extends 'rapids/base.html' %}
+
+{% block content %}
+Examinations
+
+ Active exams:
+
+{% for exam in exams %}
+ {% if exam.active %}
+
+ {{exam.name}} (mark) (scores) [Results are {% if not exam.publish_results %} not {% endif %}published]
+
+ {% endif %}
+{% endfor %}
+
+
+ Inactive exams:
+
+{% for exam in exams %}
+ {% if not exam.active %}
+
+ {{exam.name}} (mark) (scores) [Results are {% if not exam.publish_results %} not {% endif %}published]
+
+ {% endif %}
+{% endfor %}
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/longs/templates/rapids/exam_overview.html b/longs/templates/rapids/exam_overview.html
new file mode 100644
index 00000000..5c3fa9ce
--- /dev/null
+++ b/longs/templates/rapids/exam_overview.html
@@ -0,0 +1,141 @@
+{% extends 'rapids/exams.html' %}
+
+{% block content %}
+
+{% load thumbnail %}
+
+
Admin Edit
+
Exam: {{ exam.name }}
+ This exam has {{question_number}} questions. Time limit: {{exam.time_limit}} seconds.
+
+
+ Exam active: [When checked the exam will be available to take in the test system]
+
+
+ Publish results: [When checked the exam results will be available on this site]
+
+
Mark exam
+
+
+ {% for question in questions.all %}
+
+
+ {% for image in question.GetImages %}
+
+ {% endfor %}
+ {% if not question.normal %}
+ Abnormality: {{ question.get_abnormalities }} Region: {{ question.get_regions }}
+
+ {{ question.GetPrimaryAnswer }}
+ {% else %}
+ Normal
+ {% endif %}
+
+ Examination: {{ question.get_examinations }}, View , Mark
+
+ {% endfor %}
+
+
JSON
+
Refresh JSON cache
+
Make questions open access
+
Make questions closed access
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/longs/templates/rapids/exam_scores.html b/longs/templates/rapids/exam_scores.html
new file mode 100644
index 00000000..918eba05
--- /dev/null
+++ b/longs/templates/rapids/exam_scores.html
@@ -0,0 +1,65 @@
+{% extends 'rapids/exams.html' %}
+
+{% block content %}
+
+
{{ exam.name }}
+
+ {% if unmarked %}
+ The following questions need marking
+ {% for exam_index in unmarked %}
+
{{ exam_index|add:1 }}
+ {% endfor %}
+
+ {% endif %}
+
+
+
+
Stats
+ Candidates: {{cids|length}}
+ Max score: {{max_score}}
+ Mean: {{mean}}, Median {{median}}, Mode {{mode}}
+
+
{{plot|safe}}
+
+
+
+
+ Candidate ID
+ Score
+
+ {% for user, value in user_answers_marks.items %}
+
+ {{user}}
+ {{user_scores|get_item:user}}
+
+ {% endfor %}
+
+
+
+
Answers as a table
+
+
+ Candidate
+ {% for cid in cids %}
+ {{cid}}
+ {% endfor %}
+
+
+ {% for question in questions %}
+
+ Question {{forloop.counter}}
+ {% for ans, score in by_question|get_item:question %}
+ {{ans}}
+ {% endfor %}
+
+ {% endfor %}
+
+ Score:
+ {% for score in user_scores_list %}
+ {{score}}
+ {% endfor %}
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/longs/templates/rapids/exam_scores_user.html b/longs/templates/rapids/exam_scores_user.html
new file mode 100644
index 00000000..2860266f
--- /dev/null
+++ b/longs/templates/rapids/exam_scores_user.html
@@ -0,0 +1,18 @@
+{% extends 'rapids/base.html' %}
+
+{% block content %}
+
+
Exam: {{ exam.name }}
+
Candidate: {{ cid }}
+ Answers:
+
{% for ans, score, correct_answer in answers_and_marks %}
+ Question {{forloop.counter}} - Correct answer: {{ correct_answer }}
+ {{ans}} ({{score}})
+ {% endfor %}
+
+
Total mark: {{ total_score }} / {{max_score}}
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/longs/templates/rapids/exams.html b/longs/templates/rapids/exams.html
new file mode 100644
index 00000000..117e6f8a
--- /dev/null
+++ b/longs/templates/rapids/exams.html
@@ -0,0 +1,7 @@
+{% extends 'rapids/base.html' %}
+
+{% block navigation %}
+{{block.super}}
+
+Exams: {{exam.name}}-> Overview / Mark / Scores
+{% endblock %}
\ No newline at end of file
diff --git a/longs/templates/rapids/index.html b/longs/templates/rapids/index.html
new file mode 100644
index 00000000..a69a1c57
--- /dev/null
+++ b/longs/templates/rapids/index.html
@@ -0,0 +1,11 @@
+{% extends 'rapids/base.html' %}
+
+{% block content %}
+{% for exam in exams %}
+
+
+ {% if request.user.is_staff %}
Mark answers {% endif %}
+ {% if request.user.is_staff %}
Scores {% endif %}
+
+{% endfor %}
+{% endblock %}
\ No newline at end of file
diff --git a/longs/templates/rapids/index_old.html b/longs/templates/rapids/index_old.html
new file mode 100755
index 00000000..8bc76cd7
--- /dev/null
+++ b/longs/templates/rapids/index_old.html
@@ -0,0 +1,23 @@
+{% extends 'rapids/base.html' %}
+
+{% block content %}
+My Questions:
+
+
+Other Questions:
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/longs/templates/rapids/mark.html b/longs/templates/rapids/mark.html
new file mode 100644
index 00000000..d0c7a519
--- /dev/null
+++ b/longs/templates/rapids/mark.html
@@ -0,0 +1,67 @@
+{% extends 'rapids/exams.html' %}
+
+{% block content %}
+Marking question {{question_details.current}} of {{question_details.total}}
+Edit Admin
+ Edit
+{% if question.normal %}
+This question is normal
+Answers will be automatically marked.
+{% else %}
+This question is abnormal
+Answers marked as normal will be automatically marked.
+{% endif %}
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/longs/templates/rapids/mark_overview.html b/longs/templates/rapids/mark_overview.html
new file mode 100644
index 00000000..37849233
--- /dev/null
+++ b/longs/templates/rapids/mark_overview.html
@@ -0,0 +1,20 @@
+{% extends 'rapids/exams.html' %}
+
+{% block content %}
+
+
Marking exam: {{ exam.name }}
+ You can start marking from a particular question by clicking on it below.
+
+
+ Show all Show unmarked
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/longs/templates/rapids/note_form.html b/longs/templates/rapids/note_form.html
new file mode 100755
index 00000000..89dcd1ae
--- /dev/null
+++ b/longs/templates/rapids/note_form.html
@@ -0,0 +1,12 @@
+{% extends "rapids/base.html" %}
+
+{% block content %}
+Add Note
+
+{% endblock %}
diff --git a/longs/templates/rapids/rapid_detail.html b/longs/templates/rapids/rapid_detail.html
new file mode 100755
index 00000000..3768b778
--- /dev/null
+++ b/longs/templates/rapids/rapid_detail.html
@@ -0,0 +1,26 @@
+{% extends 'rapids/base.html' %}
+
+{% block content %}
+
+
+{% if exam %}
+
+
+{% if previous > -1 %}
+
Previous question
+{% endif %}
+ This question is part of exam: {{exam.name}} [{{pos}}/{{exam_length}}]
+{% if next %}
+
Next question
+{% endif %}
+
+{% endif %}
+
+Edit
+Clone
+ Add Note
+{% if request.user.is_superuser %}
+Admin Edit
+{% endif %}
+{% include 'rapids/rapid_display_block.html' %}
+{% endblock %}
\ No newline at end of file
diff --git a/longs/templates/rapids/rapid_display_block.html b/longs/templates/rapids/rapid_display_block.html
new file mode 100755
index 00000000..03dde6a1
--- /dev/null
+++ b/longs/templates/rapids/rapid_display_block.html
@@ -0,0 +1,38 @@
+
+
+ {{ question.created_date }}
+
+
+
Rapid: {{ question }}
+
Region: {{ question.get_regions }}
+
Examination: {{ question.get_examinations }}
+
Laterality: {{ question.laterality }}
+
Abnormality: {{ question.get_abnormalities }}
+
Images:
+ {% for image in question.images.all %}
+
+ Image {{ forloop.counter }}{% if image.feedback_image %} [feedback image]{% endif %}:
+
+
+ {% endfor %}
+
+
Feedback: {{ question.feedback }}
+
Author(s): {% for author in question.author.all %} {{author}} , {% endfor %}
+
Checked by: {% for verified in question.verified.all %} {{verified}} , {% endfor %}
+
Scrapped: {{ question.scrapped }} (toggle)
+
Answers: {{ question.GetMarkedAnswers }}
+
+
+
+ Notes:
+
+ {% for note in question.rapid_notes.all %}
+
+ {{ note.created_on }} by {{ note.author }}: {{ note.note }}
+
+ {% endfor %}
+
+
+
\ No newline at end of file
diff --git a/longs/templates/rapids/rapid_form.html b/longs/templates/rapids/rapid_form.html
new file mode 100755
index 00000000..24964892
--- /dev/null
+++ b/longs/templates/rapids/rapid_form.html
@@ -0,0 +1,292 @@
+{% extends "rapids/base.html" %}
+{% load static from static %}
+
+{% block js %}
+
+
+
+
+{{ form.media }}
+{% endblock %}
+{% block content %}
+Submit Rapid
+Edit defaults
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/longs/templates/rapids/rapidcreationdefault_form.html b/longs/templates/rapids/rapidcreationdefault_form.html
new file mode 100755
index 00000000..d5ec635d
--- /dev/null
+++ b/longs/templates/rapids/rapidcreationdefault_form.html
@@ -0,0 +1,10 @@
+{% extends "rapids/base.html" %}
+
+
+{% block content %}
+Defaults for creating new rapids
+
+{% endblock %}
\ No newline at end of file
diff --git a/longs/templates/rapids/view.html b/longs/templates/rapids/view.html
new file mode 100755
index 00000000..2d6dddd3
--- /dev/null
+++ b/longs/templates/rapids/view.html
@@ -0,0 +1,18 @@
+{% extends 'rapids/base.html' %}
+
+{% load render_table from django_tables2 %}
+{% block css %}
+{% endblock %}
+
+{% block content %}
+
+
+
Filter Rapids
+
+
+{% render_table table %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/longs/tests.py b/longs/tests.py
new file mode 100755
index 00000000..7ce503c2
--- /dev/null
+++ b/longs/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/longs/urls.py b/longs/urls.py
new file mode 100755
index 00000000..65c4cc51
--- /dev/null
+++ b/longs/urls.py
@@ -0,0 +1,63 @@
+from django.urls import path, include
+from . import views
+
+app_name = "rapids"
+
+urlpatterns = [
+ # path('', views.question_list, name='question_list'),
+ 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("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("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("region/create/", views.create_region, name="create_region"),
+ path("region/ajax/get_region_id",
+ views.get_region_id,
+ name="get_region_id"),
+ path("abnormality/create/",
+ views.create_abnormality,
+ name="create_abnormality"),
+ path("abnormality/ajax/get_abnormality_id",
+ views.get_abnormality_id,
+ name="get_abnormality_id"),
+ path("examination/create/",
+ views.create_examination,
+ name="create_examination"),
+ path("examination/ajax/get_examination_id",
+ views.get_examination_id,
+ name="get_examination_id"),
+ path(
+ "question//update",
+ views.RapidUpdate.as_view(),
+ name="rapid_update",
+ ),
+ path("/add_note", views.AddNote.as_view(), name="rapid_add_note"),
+]
diff --git a/longs/views.py b/longs/views.py
new file mode 100755
index 00000000..ce4b5c05
--- /dev/null
+++ b/longs/views.py
@@ -0,0 +1,1126 @@
+import json
+from django.shortcuts import render, get_object_or_404, redirect
+from django import forms
+
+# from django.contrib.auth.models import User
+from django.contrib.auth.decorators import login_required, user_passes_test
+from django.contrib.auth.models import User
+from django.core.exceptions import PermissionDenied
+
+from django.contrib.auth.mixins import LoginRequiredMixin
+
+from django.views.generic.edit import CreateView, UpdateView, DeleteView
+from django.views.generic import ListView
+from django.views.decorators.csrf import csrf_exempt
+
+from django.urls import reverse_lazy, reverse
+
+from django.http import Http404, JsonResponse
+from django.http import HttpResponseRedirect, HttpResponse
+
+from .forms import (
+ RapidForm,
+ MarkRapidQuestionForm,
+ ImageFormSet,
+ NoteForm,
+ RegionForm,
+ AbnormalityForm,
+ ExaminationForm,
+ AnswerFormSet,
+ AnswerUpdateFormSet,
+)
+from .models import (
+ Rapid,
+ Note,
+ Abnormality,
+ Region,
+ Examination,
+ Exam,
+ Answer,
+ CidUserAnswer,
+)
+from .tables import RapidTable
+from .filters import RapidFilter
+
+from django_tables2 import SingleTableView, SingleTableMixin
+from django_filters.views import FilterView
+
+from .decorators import user_is_author_or_rapid_checker
+
+from collections import defaultdict
+import json
+import statistics
+import plotly.express as px
+
+from django.core.cache import cache
+
+from helpers.images import image_as_base64
+
+import logging
+from copy import deepcopy
+from django.forms.models import model_to_dict
+from rapids.forms import RapidCreationDefaultForm
+from rapids.models import RapidCreationDefault
+
+logger = logging.getLogger(__name__)
+
+
+class AuthorOrCheckerRequiredMixin(object):
+ def get_object(self, *args, **kwargs):
+ obj = super(UpdateView, self).get_object(*args, **kwargs)
+ if self.request.user.groups.filter(name="rapid_checker").exists():
+ return obj
+ if self.request.user not in obj.author.all():
+ raise PermissionDenied() # or Http404
+ return obj
+
+
+@login_required
+def index(request):
+ exams = Exam.objects.all()
+ return render(request, "rapids/index.html", {"exams": exams})
+
+
+# def index(request):
+# other_rapids = Rapid.objects.exclude(author=request.user.pk)
+# user_rapids = Rapid.objects.filter(author=request.user.pk)
+# return render(
+# request,
+# "rapids/index.html",
+# {"other_rapids": other_rapids, "user_rapids": user_rapids},
+# )
+
+
+@login_required
+def rapid_detail(request, pk):
+ rapid = get_object_or_404(Rapid, pk=pk)
+
+ # if request.user not in rapid.author.all():
+ # raise PermissionDenied
+
+ # logging.debug(rapid.rapid_notes.first())
+ # logging.debug(rapid.subspecialty.first().name.all())
+ return render(request, "rapids/rapid_detail.html", {"question": rapid})
+
+
+@login_required
+def rapid_split(request, pk):
+ rapid = get_object_or_404(Rapid, pk=pk)
+
+ images = rapid.images.all()
+
+ old_abnormality = rapid.abnormality.all()
+ old_region = rapid.region.all()
+ old_examination = rapid.examination.all()
+ old_site = rapid.site.all()
+ old_author = rapid.author.all()
+
+ if not images:
+ raise Http404
+
+ for n in range(len(images) - 1):
+ rapid.pk = None
+
+ rapid.save()
+
+ rapid.abnormality.set(old_abnormality)
+ rapid.region.set(old_region)
+ rapid.examination.set(old_examination)
+ rapid.site.set(old_site)
+ rapid.author.set(old_author)
+ images[n].rapid = rapid
+ images[n].save()
+
+ # images[-1].rapid
+
+ # if request.user not in rapid.author.all():
+ # raise PermissionDenied
+
+ # logging.debug(rapid.rapid_notes.first())
+ # logging.debug(rapid.subspecialty.first().name.all())
+ return render(request, "rapids/rapid_detail.html", {"question": rapid})
+
+
+@login_required
+def author_detail(request, pk):
+ # logging.debug(Author.objects.all())
+ # author = get_object_or_404(Author, pk=pk)
+ author = User.objects.get(pk=pk)
+
+ rapids = Rapid.objects.filter(author=pk)
+
+ return render(
+ request, "rapids/category_detail.html", {"category": author, "rapids": rapids}
+ )
+
+
+def author_list(request):
+ authors = User.objects.all()
+
+ return render(request, "rapids/author_list.html", {"authors": authors})
+
+
+class RapidCreationDefaultView(LoginRequiredMixin, UpdateView):
+ model = RapidCreationDefault
+ form_class = RapidCreationDefaultForm
+
+ # fields = '__all__'
+ # #fields = [ 'condition' ]
+ # #initial = {'date_of_death': '05/01/2018'}
+ # exclude = [ 'created_date', 'published_date' ]
+ # def dispatch(self, request, *args, **kwargs):
+ # """
+ # Overridden so we can make sure the `Rapid` instance exists
+ # before going any further.
+ # """
+ # self.pk = get_object_or_404(Rapid, pk=kwargs['pk'])
+ # return super().dispatch(request, *args, **kwargs)
+ def get_object(self, queryset=None):
+ obj, create = RapidCreationDefault.objects.get_or_create(
+ author=self.request.user
+ )
+
+ return obj
+
+ def form_valid(self, form):
+
+ model = form.save(commit=False)
+
+ model.author = self.request.user
+ model.save()
+
+ response = super().form_valid(form)
+
+ return response
+
+ # form.instance.author.add(self.request.user.id)
+
+
+class AddNote(LoginRequiredMixin, CreateView):
+ model = Note
+ form_class = NoteForm
+
+ # fields = '__all__'
+ # #fields = [ 'condition' ]
+ # #initial = {'date_of_death': '05/01/2018'}
+ # exclude = [ 'created_date', 'published_date' ]
+ def dispatch(self, request, *args, **kwargs):
+ """
+ Overridden so we can make sure the `Rapid` instance exists
+ before going any further.
+ """
+ self.pk = get_object_or_404(Rapid, pk=kwargs["pk"])
+ return super().dispatch(request, *args, **kwargs)
+
+ def form_valid(self, form):
+
+ note = form.save(commit=False)
+
+ note.rapid = self.pk
+
+ note.author = self.request.user
+ note.save()
+
+ response = super().form_valid(form)
+
+ return response
+
+ # form.instance.author.add(self.request.user.id)
+
+
+@login_required
+def rapid_clone(request, pk):
+ new_item = get_object_or_404(Rapid, pk=pk)
+ new_item.pk = None # autogen a new pk (item_id)
+ # new_item.name = "Copy of " + new_item.name #need to change uniques
+
+ form = RapidForm(request.POST or None, instance=new_item)
+
+ image_formset = ImageFormSet()
+ answer_formset = AnswerFormSet()
+
+ if form.is_valid():
+ form.instance.author.add(request.user.id)
+
+ # logger.debug(formset.is_valid())
+ if image_formset.is_valid() and answer_formset.is_valid():
+ response = super().form_valid(form)
+ image_formset.instance = obj
+ image_formset.save()
+
+ answer_formset.instance = obj
+ answer_formset.save()
+ return response
+ else:
+ return super().form_invalid(form)
+
+ context = {
+ "form": form,
+ "image_formset": image_formset,
+ "answer_formset": answer_formset
+ # other context
+ }
+
+ return render(request, "rapids/rapid_form.html", context)
+
+
+class RapidCreateBase(LoginRequiredMixin, CreateView):
+ model = Rapid
+ form_class = RapidForm
+
+ def get_context_data(self, **kwargs):
+ context = super(RapidCreateBase, self).get_context_data(**kwargs)
+ if self.request.POST:
+ context["image_formset"] = ImageFormSet(
+ self.request.POST, self.request.FILES
+ )
+ context["image_formset"].full_clean()
+ context["answer_formset"] = AnswerFormSet(self.request.POST)
+ context["answer_formset"].full_clean()
+ else:
+ context["image_formset"] = ImageFormSet()
+ context["answer_formset"] = AnswerFormSet()
+ return context
+
+ def form_valid(self, form):
+
+ self.object = form.save(commit=False)
+ self.object.save()
+
+ form.instance.author.add(self.request.user.id)
+
+ context = self.get_context_data(form=form)
+ image_formset = context["image_formset"]
+ answer_formset = context["answer_formset"]
+ if image_formset.is_valid() and answer_formset.is_valid():
+ response = super().form_valid(form)
+ image_formset.instance = self.object
+ image_formset.save()
+ answer_formset.instance = self.object
+ answer_formset.save()
+ # If the normal submit button is pressed we save as normal
+ if "submit" in self.request.POST:
+ return response
+ # else we redirect to the clone url
+ else:
+ return redirect("rapids:rapid_clone", pk=self.object.pk)
+
+ else:
+ return super().form_invalid(form)
+
+
+# @login_required
+class RapidCreate(RapidCreateBase):
+
+ initial = {"laterality": Rapid.NONE}
+
+ def get_initial(self):
+ # There has to be a better way...
+ try:
+ s = (i.pk for i in self.request.user.rapid_default.site.all())
+ self.initial.update({"site": s})
+ except AttributeError:
+ pass
+ return self.initial
+
+ # fields = '__all__'
+ # #fields = [ 'condition' ]
+ # #initial = {'date_of_death': '05/01/2018'}
+ # exclude = [ 'created_date', 'published_date' ]
+
+ # self.object = form.save(commit=False)
+ # self.object.save()
+
+ # form.instance.author.add(self.request.user.id)
+ # return super().form_valid(form)
+
+
+class RapidUpdate(LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView):
+ model = Rapid
+ form_class = RapidForm
+
+ # fields = '__all__'
+ # #fields = [ 'condition' ]
+ # #initial = {'date_of_death': '05/01/2018'}
+ # exclude = [ 'created_date', 'published_date' ]
+ def get_context_data(self, **kwargs):
+ context = super(RapidUpdate, self).get_context_data(**kwargs)
+ if self.request.POST:
+ context["image_formset"] = ImageFormSet(
+ self.request.POST, self.request.FILES, instance=self.object
+ )
+ context["image_formset"].full_clean()
+ context["answer_formset"] = AnswerFormSet(
+ self.request.POST, instance=self.object
+ )
+ context["answer_formset"].full_clean()
+ else:
+ context["image_formset"] = ImageFormSet(instance=self.object)
+ context["answer_formset"] = AnswerFormSet(instance=self.object)
+ return context
+
+ def form_valid(self, form):
+
+ self.object = form.save(commit=False)
+ self.object.save()
+
+ form.instance.author.add(self.request.user.id)
+
+ context = self.get_context_data(form=form)
+ image_formset = context["image_formset"]
+ answer_formset = context["answer_formset"]
+ # logger.debug(formset.is_valid())
+ if image_formset.is_valid() and answer_formset.is_valid():
+ response = super().form_valid(form)
+ image_formset.instance = self.object
+ image_formset.save()
+ answer_formset.instance = self.object
+ answer_formset.save()
+ return response
+ else:
+ return super().form_invalid(form)
+
+
+class RapidClone(RapidCreateBase):
+ """Clones a existing rapid"""
+
+ # fields = '__all__'
+ # #fields = [ 'condition' ]
+ # #initial = {'date_of_death': '05/01/2018'}
+ # exclude = [ 'created_date', 'published_date' ]
+ def get_initial(self):
+ # print(self.request)
+ old_object = get_object_or_404(Rapid, pk=self.kwargs["pk"])
+ initial_data = model_to_dict(old_object, exclude=["id"])
+
+ return initial_data
+
+
+@login_required
+@user_is_author_or_rapid_checker
+def rapid_scrap(request, pk):
+ try:
+ rapid = Rapid.objects.get(pk=pk)
+ except Rapid.DoesNotExist:
+ raise Http404("Rapid does not exist")
+
+ rapid.scrapped = not rapid.scrapped
+ rapid.save()
+ return HttpResponseRedirect(reverse("rapids:rapid_detail", args=(pk,)))
+
+
+# @login_required
+# def edit_abnormality_popup(request):
+# instance = get_object_or_404(Abnormality, pk = pk)
+# form = AbnormalityForm(request.POST or None)
+# if form.is_valid():
+# instance = form.save()
+# return HttpResponse('' % (instance.pk, instance))
+# return render(request, "rapids/create_simple.html", {'form': form})
+
+
+@login_required
+def create_abnormality(request):
+ form = AbnormalityForm(request.POST or None)
+ if form.is_valid():
+ instance = form.save()
+ return HttpResponse(
+ ''
+ % (instance.pk, instance)
+ )
+ return render(
+ request, "rapids/create_simple.html", {"form": form, "name": "Abnormality"}
+ )
+
+
+@csrf_exempt
+def get_abnormality_id(request):
+ if request.is_ajax():
+ abnormality_name = request.GET["abnormality_name"]
+ abnormality_id = Abnormality.objects.get(name=abnormality_name).id
+ data = {
+ "abnormality_id": abnormality_id,
+ }
+ return HttpResponse(json.dumps(data), content_type="application/json")
+ return HttpResponse("/")
+
+
+@login_required
+def create_examination(request):
+ form = ExaminationForm(request.POST or None)
+ if form.is_valid():
+ instance = form.save()
+ return HttpResponse(
+ ''
+ % (instance.pk, instance)
+ )
+ return render(
+ request, "rapids/create_simple.html", {"form": form, "name": "Examination"}
+ )
+
+
+@csrf_exempt
+def get_examination_id(request):
+ if request.is_ajax():
+ examination_name = request.GET["examination_name"]
+ examination_id = Examination.objects.get(name=examination_name).id
+ data = {
+ "examination_id": examination_id,
+ }
+ return HttpResponse(json.dumps(data), content_type="application/json")
+ return HttpResponse("/")
+
+
+@login_required
+def create_region(request):
+ form = RegionForm(request.POST or None)
+ if form.is_valid():
+ instance = form.save()
+ return HttpResponse(
+ ''
+ % (instance.pk, instance)
+ )
+ return render(
+ request, "rapids/create_simple.html", {"form": form, "name": "Region"}
+ )
+
+
+@csrf_exempt
+def get_region_id(request):
+ if request.is_ajax():
+ region_name = request.GET["region_name"]
+ region_id = Region.objects.get(name=region_name).id
+ data = {
+ "region_id": region_id,
+ }
+ return HttpResponse(json.dumps(data), content_type="application/json")
+ return HttpResponse("/")
+
+
+class RapidView(SingleTableMixin, FilterView):
+ model = Rapid
+ table_class = RapidTable
+ template_name = "rapids/view.html"
+
+ filterset_class = RapidFilter
+
+
+def loadJsonAnswer(answer):
+ eid = int(answer["eid"].split("/")[1])
+
+ # As access is not restricted make sure the data appears valid
+ if (not isinstance(answer["cid"], int)) or (
+ not isinstance(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 eid > max_int:
+ return JsonResponse({"success": False, "error": "invalid cid"})
+
+ exam = get_object_or_404(Exam, pk=eid)
+
+ if not exam.active:
+ return JsonResponse(
+ {"success": False, "error": "No active exam: {}".format(eid)}
+ )
+
+ exiting_answers = CidUserAnswer.objects.filter(
+ question__id=answer["qid"], exam__id=eid, cid=answer["cid"]
+ )
+
+ posted_answer = answer["ans"]
+
+ # Normal answers are just posted with the answer "Normal"
+ normal = False
+ if posted_answer == "Normal":
+ normal = True
+ posted_answer = ""
+ # We can ignore the first ab
+ elif answer["qidn"] == "1" and posted_answer == "Abnormal":
+ return False
+
+
+ if not exiting_answers:
+ ans = CidUserAnswer(answer=posted_answer, normal=normal, cid=answer["cid"])
+ ans.question_id = answer["qid"]
+ ans.exam_id = 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.normal = normal
+ ans.answer = posted_answer
+ 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
+
+ for answer in json.loads(request.POST.get("answers")):
+ ret = loadJsonAnswer(answer)
+
+ #if ret is not True:
+ # return ret
+
+ if 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, json=True):
+ exams = Exam.objects.all()
+
+ 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()),
+ "type": "rapid",
+ }
+ )
+
+ if json == False:
+ return active_exams["exams"]
+
+ 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("rapids_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)
+
+ # Loop through rapidimage associations
+ images = []
+ feedback_images = []
+ for i in q.images.all():
+ if i.feedback_image == True:
+ feedback_images.append(image_as_base64(i.image))
+ # feedback_images.append(i.image.url)
+ else:
+ images.append(image_as_base64(i.image))
+ #images.append(i.image.url)
+
+
+
+ exam_questions[q.id] = {
+ "images": images,
+ #"feedback_image": [],
+ #"annotations": [str(q.image_annotations)],
+ "type": "rapid",
+ }
+
+ #if feedback_images:
+ # exam_questions[q.id]["feedback_image"] = feedback_images
+
+
+ exam_json = {
+ "eid": "rapid/{}".format(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("rapids_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
+ elif s.normal:
+ ans = "Normal"
+ else:
+ 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 or user_answer is None:
+ # skip if no answer
+ answers_marks.append(0)
+ #answers.append("")
+ answer_score = 0
+ ans = "Not answered"
+ else:
+ if user_answer.normal:
+ ans = "Normal"
+ 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,
+ },
+ )
+
+
+@login_required
+def exam_question_detail(request, pk, sk):
+ exam = get_object_or_404(Exam, pk=pk)
+
+ question = exam.exam_questions.all()[sk]
+
+ exam_length = len(exam.exam_questions.all())
+
+ pos = exam.get_question_index(question) + 1
+
+ print(question.exams.through.sort_value)
+
+ previous = -1
+ if sk > 0:
+ previous = sk - 1
+ next = sk + 1
+ if sk == exam_length - 1:
+ next = False
+
+ return render(
+ request,
+ "rapids/rapid_detail.html",
+ {
+ "question": question,
+ "exam": exam,
+ "next": next,
+ "previous": previous,
+ "exam_length": exam_length,
+ "pos": pos,
+ },
+ )
+
+
+@login_required
+def exam_list(request):
+ exams = Exam.objects.all()
+ return render(request, "rapids/exam_list.html", {"exams": exams})
+
+
+@login_required
+def exam_overview(request, pk):
+ print(type(pk))
+ #print(Exam.objects.all())
+ #exams = Exam.objects.all()
+ #print("test", Exam.objects.all().get(id=pk))
+ exam = get_object_or_404(Exam, pk=pk)
+ print("exam", exam)
+
+ questions = exam.exam_questions.all()
+
+ question_number = len(questions)
+
+ return render(
+ request,
+ "rapids/exam_overview.html",
+ {"exam": exam, "questions": questions, "question_number": question_number},
+ )
\ No newline at end of file