This commit is contained in:
Ross
2021-11-24 22:24:58 +00:00
parent aae7a103c1
commit 1d1319429f
4 changed files with 219 additions and 105 deletions
+19 -14
View File
@@ -17,18 +17,21 @@ from atlas.models import (
from anatomy.models import Modality from anatomy.models import Modality
from generic.models import Examination, Condition, Sign from generic.models import Examination, Condition, Sign
#from generic.models import Examination, Site, Condition, Sign
# from generic.models import Examination, Site, Condition, Sign
from django.contrib.admin.widgets import FilteredSelectMultiple from django.contrib.admin.widgets import FilteredSelectMultiple
from django.forms.widgets import RadioSelect, TextInput, Textarea from django.forms.widgets import RadioSelect, TextInput, Textarea
from tinymce.widgets import TinyMCE from tinymce.widgets import TinyMCE
class ExaminationForm(ModelForm): class ExaminationForm(ModelForm):
class Meta: class Meta:
model = Examination model = Examination
fields = ["examination"] fields = ["examination"]
class SeriesForm(ModelForm): class SeriesForm(ModelForm):
class Media: class Media:
# Django also includes a few javascript files necessary # Django also includes a few javascript files necessary
@@ -43,10 +46,10 @@ class SeriesForm(ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(SeriesForm, self).__init__(*args, **kwargs) super(SeriesForm, self).__init__(*args, **kwargs)
#self.fields["examination"] = ModelChoiceField( # self.fields["examination"] = ModelChoiceField(
# queryset=Examination.objects.all(), # queryset=Examination.objects.all(),
# widget=FilteredSelectMultiple(verbose_name="Examination", is_stacked=False), # widget=FilteredSelectMultiple(verbose_name="Examination", is_stacked=False),
#) # )
self.fields["modality"] = ModelChoiceField( self.fields["modality"] = ModelChoiceField(
required=True, required=True,
@@ -62,7 +65,7 @@ class SeriesForm(ModelForm):
class AtlasForm(ModelForm): class AtlasForm(ModelForm):
#exams = ModelMultipleChoiceField(required=False, queryset=Exam.objects.all(),widget=FilteredSelectMultiple(verbose_name="Exams", is_stacked=False)) # exams = ModelMultipleChoiceField(required=False, queryset=Exam.objects.all(),widget=FilteredSelectMultiple(verbose_name="Exams", is_stacked=False))
class Media: class Media:
# Django also includes a few javascript files necessary # Django also includes a few javascript files necessary
@@ -76,7 +79,9 @@ class AtlasForm(ModelForm):
js = ["jsi18n.js", "tesseract.min.js"] js = ["jsi18n.js", "tesseract.min.js"]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user') # To get request.user. Do not use kwargs.pop('user', None) due to potential security hole self.user = kwargs.pop(
"user"
) # To get request.user. Do not use kwargs.pop('user', None) due to potential security hole
if kwargs.get("instance"): if kwargs.get("instance"):
# We get the 'initial' keyword argument or initialize it # We get the 'initial' keyword argument or initialize it
# as a dict if it didn't exist. # as a dict if it didn't exist.
@@ -89,14 +94,13 @@ class AtlasForm(ModelForm):
super(AtlasForm, self).__init__(*args, **kwargs) super(AtlasForm, self).__init__(*args, **kwargs)
class Meta: class Meta:
model = Case model = Case
# fields = ['due_back'] # fields = ['due_back']
# fields = '__all__' # fields = '__all__'
fields = [ fields = [
#"examination", # "examination",
#"site", # "site",
"title", "title",
"subspecialty", "subspecialty",
"description", "description",
@@ -111,10 +115,10 @@ class AtlasForm(ModelForm):
# "normal": RadioSelect( # "normal": RadioSelect(
# choices=[(True, 'Yes'), # choices=[(True, 'Yes'),
# (False, 'No')]) # (False, 'No')])
"findings" : TinyMCE(attrs={'cols': 80, 'rows': 20}), "findings": TinyMCE(attrs={"cols": 80, "rows": 20}),
"mark_scheme" : TinyMCE(attrs={'cols': 80, 'rows': 30}), "mark_scheme": TinyMCE(attrs={"cols": 80, "rows": 30}),
"description" : Textarea(attrs={'cols': 80, 'rows': 5}), "description": Textarea(attrs={"cols": 80, "rows": 5}),
"history" : Textarea(attrs={'cols': 80, 'rows': 5}), "history": Textarea(attrs={"cols": 80, "rows": 5}),
} }
def save(self, commit=True): def save(self, commit=True):
@@ -126,8 +130,8 @@ class AtlasForm(ModelForm):
def save_m2m(): def save_m2m():
old_save_m2m() old_save_m2m()
#instance.exams.clear() # instance.exams.clear()
#for exam in self.cleaned_data["exams"]: # for exam in self.cleaned_data["exams"]:
# # We need the id here # # We need the id here
# instance.exams.add(exam.pk) # instance.exams.add(exam.pk)
@@ -140,6 +144,7 @@ class AtlasForm(ModelForm):
return instance return instance
SeriesFormSet = inlineformset_factory( SeriesFormSet = inlineformset_factory(
Case, Case,
Series.case.through, Series.case.through,
+5 -5
View File
@@ -2,10 +2,10 @@
{% extends "atlas/base.html" %} {% extends "atlas/base.html" %}
{% block content %} {% block content %}
<a href="{% url 'atlas:series_update' pk=series.pk %}" title="Edit the Series">Edit</a> <a href="{% url 'atlas:series_update' pk=series.pk %}" title="Edit the Series">Edit</a>
{% if request.user.is_superuser %} {% if request.user.is_superuser %}
<a href="{% url 'admin:atlas_series_change' series.id %}" title="Edit the Series using the admin interface">Admin Edit</a> <a href="{% url 'admin:atlas_series_change' series.id %}" title="Edit the Series using the admin interface">Admin Edit</a>
{% endif %} {% endif %}
{% include 'atlas/series_viewer.html' %} {% include 'atlas/series_viewer.html' %}
{% endblock %} {% endblock %}
+9 -1
View File
@@ -197,7 +197,15 @@ SeriesFormSet = inlineformset_factory(
can_delete=True, can_delete=True,
extra=0, extra=0,
max_num=10, max_num=10,
field_classes="testing", )
LongFormSet = inlineformset_factory(
LongSeries,
Long,
exclude=[],
can_delete=True,
extra=0,
max_num=10,
) )
+182 -81
View File
@@ -5,7 +5,11 @@ from django import forms
# from django.contrib.auth.models import User # from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required, user_passes_test from django.contrib.auth.decorators import login_required, user_passes_test
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied, ViewDoesNotExist from django.core.exceptions import (
ObjectDoesNotExist,
PermissionDenied,
ViewDoesNotExist,
)
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic.detail import DetailView from django.views.generic.detail import DetailView
@@ -22,7 +26,9 @@ from django.http import HttpResponseRedirect, HttpResponse
from .forms import ( from .forms import (
LongForm, LongForm,
LongSeriesForm, LongSeriesImageFormSet, LongFormSet,
LongSeriesForm,
LongSeriesImageFormSet,
MarkLongQuestionDoubleForm, MarkLongQuestionDoubleForm,
MarkLongQuestionSingleForm, MarkLongQuestionSingleForm,
SeriesFormSet, SeriesFormSet,
@@ -30,7 +36,9 @@ from .forms import (
ExamForm, ExamForm,
) )
from .models import ( from .models import (
AnswerMarks, Long, LongSeries, AnswerMarks,
Long,
LongSeries,
Examination, Examination,
Exam, Exam,
CidUserAnswer, CidUserAnswer,
@@ -41,7 +49,15 @@ from .filters import LongFilter, LongSeriesFilter, UserAnswerFilter
from django_tables2 import SingleTableView, SingleTableMixin from django_tables2 import SingleTableView, SingleTableMixin
from django_filters.views import FilterView from django_filters.views import FilterView
from .decorators import user_is_author_or_long_checker, user_is_author_or_long_checker_or_long_marker, user_is_author_or_long_series_checker_or_long_marker, user_is_long_checker, user_is_author_or_long_series_checker, user_is_long_marker, user_is_author_or_long_checker_or_long_marker from .decorators import (
user_is_author_or_long_checker,
user_is_author_or_long_checker_or_long_marker,
user_is_author_or_long_series_checker_or_long_marker,
user_is_long_checker,
user_is_author_or_long_series_checker,
user_is_long_marker,
user_is_author_or_long_checker_or_long_marker,
)
from collections import defaultdict from collections import defaultdict
import json import json
@@ -74,6 +90,7 @@ import os
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def normaliseScore(score): def normaliseScore(score):
if score >= 25.5 and score <= 28: if score >= 25.5 and score <= 28:
return 4.5 return 4.5
@@ -98,7 +115,10 @@ def normaliseScore(score):
class AuthorOrCheckerRequiredMixin(object): class AuthorOrCheckerRequiredMixin(object):
def get_object(self, *args, **kwargs): def get_object(self, *args, **kwargs):
obj = super().get_object(*args, **kwargs) obj = super().get_object(*args, **kwargs)
if self.request.user.groups.filter(name="long_checker").exists() or self.request.user.is_superuser: if (
self.request.user.groups.filter(name="long_checker").exists()
or self.request.user.is_superuser
):
return obj return obj
if self.request.user not in obj.get_author_objects(): if self.request.user not in obj.get_author_objects():
raise PermissionDenied() # or Http404 raise PermissionDenied() # or Http404
@@ -126,6 +146,7 @@ def question_detail(request, pk):
# logging.debug(long.subspecialty.first().name.all()) # logging.debug(long.subspecialty.first().name.all())
return render(request, "longs/question_detail.html", {"question": long}) return render(request, "longs/question_detail.html", {"question": long})
@login_required @login_required
@user_is_author_or_long_series_checker_or_long_marker @user_is_author_or_long_series_checker_or_long_marker
def long_series_detail(request, pk): def long_series_detail(request, pk):
@@ -137,6 +158,7 @@ def long_series_detail(request, pk):
# logging.debug(long.subspecialty.first().name.all()) # logging.debug(long.subspecialty.first().name.all())
return render(request, "longs/long_series.html", {"series": series}) return render(request, "longs/long_series.html", {"series": series})
@login_required @login_required
@user_is_author_or_long_checker @user_is_author_or_long_checker
def long_split(request, pk): def long_split(request, pk):
@@ -236,10 +258,12 @@ class LongDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView):
model = Long model = Long
success_url = reverse_lazy("longs:long_view") success_url = reverse_lazy("longs:long_view")
class LongSeriesDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView): class LongSeriesDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView):
model = LongSeries model = LongSeries
success_url = reverse_lazy("longs:long_series_view") success_url = reverse_lazy("longs:long_series_view")
@login_required @login_required
@user_is_author_or_long_checker @user_is_author_or_long_checker
def long_clone(request, pk): def long_clone(request, pk):
@@ -272,6 +296,7 @@ def long_clone(request, pk):
return render(request, "longs/long_form.html", context) return render(request, "longs/long_form.html", context)
class LongSeriesCreate(RevisionMixin, LoginRequiredMixin, CreateView): class LongSeriesCreate(RevisionMixin, LoginRequiredMixin, CreateView):
model = LongSeries model = LongSeries
form_class = LongSeriesForm form_class = LongSeriesForm
@@ -282,7 +307,7 @@ class LongSeriesCreate(RevisionMixin, LoginRequiredMixin, CreateView):
initial = super().get_initial() initial = super().get_initial()
long = get_object_or_404(Long, pk=self.kwargs["pk"]) long = get_object_or_404(Long, pk=self.kwargs["pk"])
initial['long'] = long.id initial["long"] = long.id
return initial return initial
@@ -294,8 +319,11 @@ class LongSeriesCreate(RevisionMixin, LoginRequiredMixin, CreateView):
self.request.POST, self.request.FILES self.request.POST, self.request.FILES
) )
context["image_formset"].full_clean() context["image_formset"].full_clean()
context["long_formset"] = LongFormSet(self.request.POST, self.request.FILES)
context["long_formset"].full_clean()
else: else:
context["image_formset"] = LongSeriesImageFormSet() context["image_formset"] = LongSeriesImageFormSet()
context["leng_formset"] = LongFormSet()
return context return context
def form_valid(self, form): def form_valid(self, form):
@@ -307,16 +335,22 @@ class LongSeriesCreate(RevisionMixin, LoginRequiredMixin, CreateView):
context = self.get_context_data(form=form) context = self.get_context_data(form=form)
series_formset = context["image_formset"] series_formset = context["image_formset"]
if series_formset.is_valid(): long_formset = context["long_formset"]
if series_formset.is_valid() and long_formset.is_valid():
response = super().form_valid(form) response = super().form_valid(form)
series_formset.instance = self.object series_formset.instance = self.object
series_formset.save() series_formset.save()
long_formset.instance = self.object
long_formset.save()
return response return response
else: else:
return super().form_invalid(form) return super().form_invalid(form)
class LongSeriesUpdate(RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView):
class LongSeriesUpdate(
RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView
):
model = LongSeries model = LongSeries
form_class = LongSeriesForm form_class = LongSeriesForm
@@ -341,9 +375,9 @@ class LongSeriesUpdate(RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequire
self.object.save() self.object.save()
# Do this in the form.save() # Do this in the form.save()
#new_exams = list(form.cleaned_data['exams'].values_list("pk", flat=True)) # new_exams = list(form.cleaned_data['exams'].values_list("pk", flat=True))
#self.object.exams.clear() # self.object.exams.clear()
#self.object.exams.add(*new_exams) # self.object.exams.add(*new_exams)
form.instance.author.add(self.request.user.id) form.instance.author.add(self.request.user.id)
@@ -358,6 +392,7 @@ class LongSeriesUpdate(RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequire
else: else:
return super().form_invalid(form) return super().form_invalid(form)
class LongCreateBase(RevisionMixin, LoginRequiredMixin, CreateView): class LongCreateBase(RevisionMixin, LoginRequiredMixin, CreateView):
model = Long model = Long
form_class = LongForm form_class = LongForm
@@ -387,9 +422,9 @@ class LongCreateBase(RevisionMixin, LoginRequiredMixin, CreateView):
self.object.save() self.object.save()
# Do this in the form.save() # Do this in the form.save()
#new_exams = list(form.cleaned_data['exams'].values_list("pk", flat=True)) # new_exams = list(form.cleaned_data['exams'].values_list("pk", flat=True))
#self.object.exams.clear() # self.object.exams.clear()
#self.object.exams.add(*new_exams) # self.object.exams.add(*new_exams)
form.instance.author.add(self.request.user.id) form.instance.author.add(self.request.user.id)
@@ -433,7 +468,9 @@ class LongCreate(LongCreateBase):
# return super().form_valid(form) # return super().form_valid(form)
class LongUpdate(RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView): class LongUpdate(
RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView
):
model = Long model = Long
form_class = LongForm form_class = LongForm
@@ -468,9 +505,9 @@ class LongUpdate(RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin
self.object.save() self.object.save()
# Do this in the form.save() # Do this in the form.save()
#new_exams = list(form.cleaned_data['exams'].values_list("pk", flat=True)) # new_exams = list(form.cleaned_data['exams'].values_list("pk", flat=True))
#self.object.exams.clear() # self.object.exams.clear()
#self.object.exams.add(*new_exams) # self.object.exams.add(*new_exams)
form.instance.author.add(self.request.user.id) form.instance.author.add(self.request.user.id)
@@ -565,6 +602,7 @@ class LongView(LoginRequiredMixin, SingleTableMixin, FilterView):
filterset_class = LongFilter filterset_class = LongFilter
class LongSeriesView(LoginRequiredMixin, SingleTableMixin, FilterView): class LongSeriesView(LoginRequiredMixin, SingleTableMixin, FilterView):
model = LongSeries model = LongSeries
table_class = LongSeriesTable table_class = LongSeriesTable
@@ -591,7 +629,7 @@ def loadJsonAnswer(answer):
exam = get_object_or_404(Exam, pk=eid) exam = get_object_or_404(Exam, pk=eid)
#if not exam.active: # if not exam.active:
# return False, JsonResponse( # return False, JsonResponse(
# {"success": False, "error": "No active exam: {}".format(eid)} # {"success": False, "error": "No active exam: {}".format(eid)}
# ) # )
@@ -605,14 +643,12 @@ def loadJsonAnswer(answer):
# Long cases seperate sections by qidn # Long cases seperate sections by qidn
qidn = answer["qidn"] qidn = answer["qidn"]
# If the user answer does not exist # If the user answer does not exist
if not exiting_answers: if not exiting_answers:
ans = CidUserAnswer(cid=answer["cid"]) ans = CidUserAnswer(cid=answer["cid"])
ans.question_id = answer["qid"] ans.question_id = answer["qid"]
ans.exam_id = eid ans.exam_id = eid
# If the answer already exists or we have started populating it # If the answer already exists or we have started populating it
else: else:
# Update an existing answer # Update an existing answer
@@ -638,6 +674,7 @@ def loadJsonAnswer(answer):
return True, None return True, None
@user_is_long_marker @user_is_long_marker
@reversion.create_revision() @reversion.create_revision()
def mark_answer_override(request, exam_id, question_number, cid): def mark_answer_override(request, exam_id, question_number, cid):
@@ -668,7 +705,9 @@ def mark_answer(request, exam_id, question_number, cid, override=False):
except ObjectDoesNotExist: except ObjectDoesNotExist:
raise Http404("User answer does not exist") raise Http404("User answer does not exist")
cid_list = list(question.cid_user_answers.filter(exam__id=exam_id).values_list("cid", flat=True)) cid_list = list(
question.cid_user_answers.filter(exam__id=exam_id).values_list("cid", flat=True)
)
previous_cid = False previous_cid = False
next_cid = False next_cid = False
@@ -679,12 +718,14 @@ def mark_answer(request, exam_id, question_number, cid, override=False):
elif cid_list[-1] == cid: elif cid_list[-1] == cid:
previous_cid = cid_list[-2] previous_cid = cid_list[-2]
else: else:
next_cid = cid_list[cid_list.index(cid)+1] next_cid = cid_list[cid_list.index(cid) + 1]
previous_cid = cid_list[cid_list.index(cid)-1] previous_cid = cid_list[cid_list.index(cid) - 1]
try: try:
if exam.double_mark: if exam.double_mark:
unmarked = question.get_unmarked_user_answers(exam_pk=exam.id, marker=request.user) unmarked = question.get_unmarked_user_answers(
exam_pk=exam.id, marker=request.user
)
else: else:
unmarked = question.get_unmarked_user_answers(exam_pk=exam.id) unmarked = question.get_unmarked_user_answers(exam_pk=exam.id)
next_unmarked_id = unmarked[0].cid next_unmarked_id = unmarked[0].cid
@@ -695,14 +736,16 @@ def mark_answer(request, exam_id, question_number, cid, override=False):
# We use different forms if the exam should be single or double marked # We use different forms if the exam should be single or double marked
if not exam.double_mark or (request.method == "POST" and request.POST['form_id'] == 'discrepancy_form'): if not exam.double_mark or (
request.method == "POST" and request.POST["form_id"] == "discrepancy_form"
):
if request.method == "POST": if request.method == "POST":
form = MarkLongQuestionSingleForm(request.POST) form = MarkLongQuestionSingleForm(request.POST)
if form.is_valid(): if form.is_valid():
# If skip button is pressed skip the question without marking # If skip button is pressed skip the question without marking
# Skip is problematic if we skip to the next unmarked answer # Skip is problematic if we skip to the next unmarked answer
#if "skip" in request.POST: # if "skip" in request.POST:
# return redirect("longs:mark_answer", pk=exam_id, sk=question_number, cid=next_unmarked_id) # return redirect("longs:mark_answer", pk=exam_id, sk=question_number, cid=next_unmarked_id)
# Extract score from form and save it to the object # Extract score from form and save it to the object
@@ -710,14 +753,24 @@ def mark_answer(request, exam_id, question_number, cid, override=False):
answer.save() answer.save()
if "next" in request.POST: if "next" in request.POST:
return redirect("longs:mark_answer", exam_id=exam_id, question_number=question_number, cid=next_unmarked_id) return redirect(
"longs:mark_answer",
exam_id=exam_id,
question_number=question_number,
cid=next_unmarked_id,
)
if "save" in request.POST: if "save" in request.POST:
return redirect("longs:mark_answer", exam_id=exam_id, question_number=question_number, cid=cid) return redirect(
#elif "previous" in request.POST: "longs:mark_answer",
exam_id=exam_id,
question_number=question_number,
cid=cid,
)
# elif "previous" in request.POST:
# return redirect("longs:mark_question_overview", pk=exam_id, sk=n - 1) # return redirect("longs:mark_question_overview", pk=exam_id, sk=n - 1)
else: else:
form = MarkLongQuestionSingleForm(initial={'score': answer.score}) form = MarkLongQuestionSingleForm(initial={"score": answer.score})
else: else:
if request.method == "POST": if request.method == "POST":
@@ -726,11 +779,12 @@ def mark_answer(request, exam_id, question_number, cid, override=False):
if form.is_valid(): if form.is_valid():
# If skip button is pressed skip the question without marking # If skip button is pressed skip the question without marking
# Skip is problematic if we skip to the next unmarked answer # Skip is problematic if we skip to the next unmarked answer
#if "skip" in request.POST: # if "skip" in request.POST:
# return redirect("longs:mark_answer", pk=exam_id, sk=question_number, cid=next_unmarked_id) # return redirect("longs:mark_answer", pk=exam_id, sk=question_number, cid=next_unmarked_id)
mark_object = AnswerMarks.objects.get_or_create(
mark_object = AnswerMarks.objects.get_or_create(user_answer=answer, marker=request.user)[0] user_answer=answer, marker=request.user
)[0]
# Extract score from form and save it to the object # Extract score from form and save it to the object
mark_object.score = form.cleaned_data["score"] mark_object.score = form.cleaned_data["score"]
mark_object.mark_reason = form.cleaned_data["mark_reason"] mark_object.mark_reason = form.cleaned_data["mark_reason"]
@@ -748,19 +802,35 @@ def mark_answer(request, exam_id, question_number, cid, override=False):
answer.score = "" answer.score = ""
answer.save() answer.save()
if "next" in request.POST: if "next" in request.POST:
return redirect("longs:mark_answer", exam_id=exam_id, question_number=question_number, cid=next_unmarked_id) return redirect(
"longs:mark_answer",
exam_id=exam_id,
question_number=question_number,
cid=next_unmarked_id,
)
if "save" in request.POST: if "save" in request.POST:
return redirect("longs:mark_answer", exam_id=exam_id, question_number=question_number, cid=cid) return redirect(
#elif "previous" in request.POST: "longs:mark_answer",
exam_id=exam_id,
question_number=question_number,
cid=cid,
)
# elif "previous" in request.POST:
# return redirect("longs:mark_question_overview", pk=exam_id, sk=n - 1) # return redirect("longs:mark_question_overview", pk=exam_id, sk=n - 1)
else: else:
try: try:
mark_object = AnswerMarks.objects.get(user_answer=answer, marker=request.user) mark_object = AnswerMarks.objects.get(
form = MarkLongQuestionDoubleForm(initial={'score': mark_object.score,'mark_reason': mark_object.mark_reason,'candidate_feedback': mark_object.candidate_feedback, }) user_answer=answer, marker=request.user
)
form = MarkLongQuestionDoubleForm(
initial={
"score": mark_object.score,
"mark_reason": mark_object.mark_reason,
"candidate_feedback": mark_object.candidate_feedback,
}
)
except AnswerMarks.DoesNotExist: except AnswerMarks.DoesNotExist:
form = MarkLongQuestionDoubleForm() form = MarkLongQuestionDoubleForm()
@@ -769,7 +839,9 @@ def mark_answer(request, exam_id, question_number, cid, override=False):
# if they do automatically update teh scoer # if they do automatically update teh scoer
marks = set(answer.mark.values_list("score", flat=True)) marks = set(answer.mark.values_list("score", flat=True))
if len(marks) > 1 or override: if len(marks) > 1 or override:
discrepancy_form = MarkLongQuestionSingleForm(initial={'score': answer.score }) discrepancy_form = MarkLongQuestionSingleForm(
initial={"score": answer.score}
)
return render( return render(
request, request,
@@ -789,6 +861,7 @@ def mark_answer(request, exam_id, question_number, cid, override=False):
}, },
) )
# @user_passes_test(user_is_admin, login_url="/accounts/login") # @user_passes_test(user_is_admin, login_url="/accounts/login")
@login_required @login_required
@user_is_long_marker @user_is_long_marker
@@ -809,13 +882,16 @@ def mark_question_overview(request, exam_id, sk):
except IndexError: except IndexError:
raise Http404("Exam question does not exist") raise Http404("Exam question does not exist")
user_answers = question.cid_user_answers.filter(exam__id=exam_id) user_answers = question.cid_user_answers.filter(exam__id=exam_id)
unmarked_count = user_answers.filter(score=CidUserAnswer.ScoreOptions.UNMARKED).count() unmarked_count = user_answers.filter(
score=CidUserAnswer.ScoreOptions.UNMARKED
).count()
if exam.double_mark: if exam.double_mark:
marker_unmarked_count = question.get_unmarked_user_answer_count(exam.pk, marker=request.user) marker_unmarked_count = question.get_unmarked_user_answer_count(
exam.pk, marker=request.user
)
return render( return render(
request, request,
"longs/mark_question_double_overview.html", "longs/mark_question_double_overview.html",
@@ -875,7 +951,7 @@ def exam_scores_cid(request, pk):
if not s: if not s:
# skip if no answer, (score 4) # skip if no answer, (score 4)
user_answers_marks[cid].append(4) user_answers_marks[cid].append(4)
#user_answers[cid].append("") # user_answers[cid].append("")
by_question[q].append(("", 4)) by_question[q].append(("", 4))
continue continue
else: else:
@@ -884,22 +960,19 @@ def exam_scores_cid(request, pk):
if answer_score == "": if answer_score == "":
index = exam.get_question_index(q) index = exam.get_question_index(q)
unmarked.add(index) unmarked.add(index)
#user_answers[cid].append(ans) # user_answers[cid].append(ans)
user_answers_marks[cid].append(answer_score) user_answers_marks[cid].append(answer_score)
#user_answers_and_marks[cid].append((ans, answer_score)) # user_answers_and_marks[cid].append((ans, answer_score))
by_question[q].append((ans, answer_score)) by_question[q].append((ans, answer_score))
user_scores = {} user_scores = {}
user_scores_normalised = {} user_scores_normalised = {}
for user in user_answers_marks: for user in user_answers_marks:
user_scores[user] = sum( user_scores[user] = sum([i for i in user_answers_marks[user] if i != ""])
[i for i in user_answers_marks[user] if i != ""] user_scores_normalised[user] = normaliseScore(
sum([i for i in user_answers_marks[user] if i != ""])
) )
user_scores_normalised[user] = normaliseScore(sum(
[i for i in user_answers_marks[user] if i != ""]
))
user_scores_list = list(user_scores.values()) user_scores_list = list(user_scores.values())
@@ -938,13 +1011,13 @@ def exam_scores_cid(request, pk):
"unmarked": unmarked, "unmarked": unmarked,
"questions": questions, "questions": questions,
"by_question": by_question, "by_question": by_question,
#"user_answers": dict(user_answers), # "user_answers": dict(user_answers),
"user_answers_marks": dict(user_answers_marks), "user_answers_marks": dict(user_answers_marks),
"user_scores": user_scores, "user_scores": user_scores,
"user_scores_normalised": user_scores_normalised, "user_scores_normalised": user_scores_normalised,
"user_scores_list": user_scores_list, "user_scores_list": user_scores_list,
"user_names": user_names, "user_names": user_names,
#"user_answers_and_marks": user_answers_and_marks, # "user_answers_and_marks": user_answers_and_marks,
"max_score": max_score, "max_score": max_score,
"mean": mean, "mean": mean,
"median": median, "median": median,
@@ -962,12 +1035,11 @@ def exam_scores_cid_user(request, pk, sk):
questions = exam.exam_questions.all() questions = exam.exam_questions.all()
#answers_and_marks = [] # answers_and_marks = []
answers_marks = [] answers_marks = []
#answers = [] # answers = []
answer_text = [] answer_text = []
view_all_results = False view_all_results = False
if request.user.groups.filter(name="view_all_results").exists(): if request.user.groups.filter(name="view_all_results").exists():
view_all_results = True view_all_results = True
@@ -976,26 +1048,39 @@ def exam_scores_cid_user(request, pk, sk):
# Get user answer # Get user answer
user_answer = q.cid_user_answers.filter(cid=cid, exam__id=pk).first() user_answer = q.cid_user_answers.filter(cid=cid, exam__id=pk).first()
if not user_answer or user_answer is None: if not user_answer or user_answer is None:
# skip if no answer # skip if no answer
#answers_marks.append("") # answers_marks.append("")
#answers.append("") # answers.append("")
answer_score = 4 answer_score = 4
#ans = "Not answered" # ans = "Not answered"
answer_text.append((("Not answered"),("Not answered"),("Not answered"),("Not answered"),("Not answered"))) answer_text.append(
(
("Not answered"),
("Not answered"),
("Not answered"),
("Not answered"),
("Not answered"),
)
)
else: else:
answer_score = user_answer.get_answer_score() answer_score = user_answer.get_answer_score()
answer_text.append((user_answer.answer_observations, user_answer.answer_interpretation, user_answer.answer_principle_diagnosis, user_answer.answer_differential_diagnosis, user_answer.answer_management)) answer_text.append(
(
user_answer.answer_observations,
user_answer.answer_interpretation,
user_answer.answer_principle_diagnosis,
user_answer.answer_differential_diagnosis,
user_answer.answer_management,
)
)
if not exam.publish_results and not view_all_results: if not exam.publish_results and not view_all_results:
answer_score = 0 answer_score = 0
#answers.append(ans) # answers.append(ans)
answers_marks.append(answer_score) answers_marks.append(answer_score)
#answers_and_marks.append((ans, answer_score, correct_answer)) # answers_and_marks.append((ans, answer_score, correct_answer))
answered = [i for i in answers_marks if i != ""] answered = [i for i in answers_marks if i != ""]
if "" in answers_marks: if "" in answers_marks:
@@ -1015,16 +1100,17 @@ def exam_scores_cid_user(request, pk, sk):
"exam": exam, "exam": exam,
"cid": cid, "cid": cid,
"questions": questions, "questions": questions,
#"answers": answers, # "answers": answers,
"answers_marks": answers_marks, "answers_marks": answers_marks,
"total_score": total_score, "total_score": total_score,
"normalised_score": normalised_score, "normalised_score": normalised_score,
"max_score": max_score, "max_score": max_score,
"answer_text": answer_text "answer_text": answer_text
#"answers_and_marks": answers_and_marks, # "answers_and_marks": answers_and_marks,
}, },
) )
@login_required @login_required
@user_is_author_or_long_series_checker @user_is_author_or_long_series_checker
def long_series_order_dicom(request, pk): def long_series_order_dicom(request, pk):
@@ -1032,10 +1118,11 @@ def long_series_order_dicom(request, pk):
try: try:
series.order_by_dicom() series.order_by_dicom()
except: except:
return HttpResponse('<h1>Series does not appear to contain dicoms</h1>') return HttpResponse("<h1>Series does not appear to contain dicoms</h1>")
return redirect("longs:long_series_detail", pk=pk) return redirect("longs:long_series_detail", pk=pk)
@login_required @login_required
@user_is_author_or_long_series_checker @user_is_author_or_long_series_checker
def long_series_order_dicom_instance(request, pk): def long_series_order_dicom_instance(request, pk):
@@ -1043,10 +1130,13 @@ def long_series_order_dicom_instance(request, pk):
try: try:
series.order_by_dicom("InstanceNumber") series.order_by_dicom("InstanceNumber")
except: except:
return HttpResponse('<h1>Series does not appear to contain dicoms (or field InstanceNumber)</h1>') return HttpResponse(
"<h1>Series does not appear to contain dicoms (or field InstanceNumber)</h1>"
)
return redirect("longs:long_series_detail", pk=pk) return redirect("longs:long_series_detail", pk=pk)
@login_required @login_required
@user_is_author_or_long_series_checker @user_is_author_or_long_series_checker
def long_series_order_dicom_SeriesInstanceUID(request, pk): def long_series_order_dicom_SeriesInstanceUID(request, pk):
@@ -1054,10 +1144,13 @@ def long_series_order_dicom_SeriesInstanceUID(request, pk):
try: try:
series.order_by_dicom("SeriesInstanceUID") series.order_by_dicom("SeriesInstanceUID")
except: except:
return HttpResponse('<h1>Series does not appear to contain dicoms (or field SeriesInstanceUID)</h1>') return HttpResponse(
"<h1>Series does not appear to contain dicoms (or field SeriesInstanceUID)</h1>"
)
return redirect("longs:long_series_detail", pk=pk) return redirect("longs:long_series_detail", pk=pk)
@login_required @login_required
@user_is_author_or_long_series_checker @user_is_author_or_long_series_checker
def long_series_order_upload_filename(request, pk): def long_series_order_upload_filename(request, pk):
@@ -1066,6 +1159,7 @@ def long_series_order_upload_filename(request, pk):
return redirect("longs:long_series_detail", pk=pk) return redirect("longs:long_series_detail", pk=pk)
LongExamViews = ExamViews(Exam, Long, "longs", "long", loadJsonAnswer) LongExamViews = ExamViews(Exam, Long, "longs", "long", loadJsonAnswer)
@@ -1080,6 +1174,7 @@ class ExamCreate(RevisionMixin, LoginRequiredMixin, CreateView):
form.instance.author.add(self.request.user.id) form.instance.author.add(self.request.user.id)
return super().form_valid(form) return super().form_valid(form)
class ExamClone(ExamCloneMixin, ExamCreate): class ExamClone(ExamCloneMixin, ExamCreate):
"""Clone exam view""" """Clone exam view"""
@@ -1105,9 +1200,10 @@ class ExamDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView):
class ExamViewSet(RevisionMixin, viewsets.ModelViewSet): class ExamViewSet(RevisionMixin, viewsets.ModelViewSet):
queryset = Exam.objects.all().order_by('name') queryset = Exam.objects.all().order_by("name")
serializer_class = ExamSerializer serializer_class = ExamSerializer
def question_json_unbased(request, pk): def question_json_unbased(request, pk):
""" """
No (file based) caching is enabled for unbased quesitons No (file based) caching is enabled for unbased quesitons
@@ -1117,15 +1213,16 @@ def question_json_unbased(request, pk):
question_json = question.get_question_json(based=False) question_json = question.get_question_json(based=False)
return JsonResponse(question_json) return JsonResponse(question_json)
def question_json(request, pk): def question_json(request, pk):
question = get_object_or_404(Long, pk=pk) question = get_object_or_404(Long, pk=pk)
#exam = get_object_or_404(self.Exam, pk=pk) # exam = get_object_or_404(self.Exam, pk=pk)
#if not exam.active: # if not exam.active:
# raise Http404("No available exam") # raise Http404("No available exam")
path= "{0}longs/questions/{1}.json".format(settings.MEDIA_ROOT, pk) path = "{0}longs/questions/{1}.json".format(settings.MEDIA_ROOT, pk)
url= "{0}longs/questions/{1}.json".format(settings.MEDIA_URL, pk) url = "{0}longs/questions/{1}.json".format(settings.MEDIA_URL, pk)
if os.path.isfile(path) and not question.recreate_json: if os.path.isfile(path) and not question.recreate_json:
return redirect(url) return redirect(url)
@@ -1137,6 +1234,7 @@ def question_json(request, pk):
return redirect(question_json) return redirect(question_json)
@login_required @login_required
@user_is_author_or_long_checker @user_is_author_or_long_checker
def question_json_recreate(request, pk): def question_json_recreate(request, pk):
@@ -1151,7 +1249,7 @@ def question_json_recreate(request, pk):
exam.recreate_json = True exam.recreate_json = True
exam.save() exam.save()
#question.get_question_json() # question.get_question_json()
return redirect("longs:question_detail", pk=pk) return redirect("longs:question_detail", pk=pk)
@@ -1173,6 +1271,7 @@ class UserAnswerDelete(SuperuserRequiredMixin, DeleteView):
template_name = "user_answer_delete.html" template_name = "user_answer_delete.html"
success_url = reverse_lazy("longs:user_answer_table_view") success_url = reverse_lazy("longs:user_answer_table_view")
@user_passes_test(lambda u: u.is_superuser) @user_passes_test(lambda u: u.is_superuser)
def user_answer_delete_multiple(request): def user_answer_delete_multiple(request):
if request.is_ajax(): if request.is_ajax():
@@ -1195,14 +1294,16 @@ def refresh_exam_question_json(request, pk):
return redirect("longs:exam_overview", pk=pk) return redirect("longs:exam_overview", pk=pk)
class SeriesImagesZipView(SuperuserRequiredMixin, BaseZipView): class SeriesImagesZipView(SuperuserRequiredMixin, BaseZipView):
"""Download all images from an image series""" """Download all images from an image series"""
def get_files(self): def get_files(self):
series = LongSeries.objects.get(pk=self.kwargs['pk']) series = LongSeries.objects.get(pk=self.kwargs["pk"])
return [i.image.file for i in series.images.all()] return [i.image.file for i in series.images.all()]
def question_review(request, pk): def question_review(request, pk):
""" """
Return a json representation of the question when the exam is active Return a json representation of the question when the exam is active