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