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
+11 -6
View File
@@ -17,6 +17,7 @@ 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 django.contrib.admin.widgets import FilteredSelectMultiple
@@ -24,11 +25,13 @@ 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
@@ -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,7 +94,6 @@ class AtlasForm(ModelForm):
super(AtlasForm, self).__init__(*args, **kwargs)
class Meta:
model = Case
# fields = ['due_back']
@@ -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):
@@ -140,6 +144,7 @@ class AtlasForm(ModelForm):
return instance
SeriesFormSet = inlineformset_factory(
Case,
Series.case.through,
+9 -1
View File
@@ -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,
)
+147 -46
View File
@@ -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
@@ -98,7 +115,10 @@ def normaliseScore(score):
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
@@ -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
@@ -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
@@ -565,6 +602,7 @@ class LongView(LoginRequiredMixin, SingleTableMixin, FilterView):
filterset_class = LongFilter
class LongSeriesView(LoginRequiredMixin, SingleTableMixin, FilterView):
model = LongSeries
table_class = LongSeriesTable
@@ -605,14 +643,12 @@ def loadJsonAnswer(answer):
# 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
@@ -684,7 +723,9 @@ def mark_answer(request, exam_id, question_number, cid, override=False):
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,7 +736,9 @@ 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)
@@ -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)
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":
@@ -729,8 +782,9 @@ def mark_answer(request, exam_id, question_number, cid, override=False):
# 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)
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",
@@ -893,13 +969,10 @@ def exam_scores_cid(request, pk):
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())
@@ -967,7 +1040,6 @@ def exam_scores_cid_user(request, pk, sk):
# answers = []
answer_text = []
view_all_results = False
if request.user.groups.filter(name="view_all_results").exists():
view_all_results = True
@@ -976,20 +1048,33 @@ 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("")
answer_score = 4
# 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:
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
@@ -1025,6 +1110,7 @@ def exam_scores_cid_user(request, pk, sk):
},
)
@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,6 +1213,7 @@ 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)
@@ -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):
@@ -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