diff --git a/atlas/forms.py b/atlas/forms.py index f95a9a18..0f2a9c14 100755 --- a/atlas/forms.py +++ b/atlas/forms.py @@ -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", -) \ No newline at end of file +) diff --git a/atlas/templates/atlas/series.html b/atlas/templates/atlas/series.html index b248170f..25bdbad2 100755 --- a/atlas/templates/atlas/series.html +++ b/atlas/templates/atlas/series.html @@ -2,10 +2,10 @@ {% extends "atlas/base.html" %} {% block content %} -Edit -{% if request.user.is_superuser %} -Admin Edit -{% endif %} + Edit + {% if request.user.is_superuser %} + Admin Edit + {% endif %} -{% include 'atlas/series_viewer.html' %} -{% endblock %} \ No newline at end of file + {% include 'atlas/series_viewer.html' %} +{% endblock %} diff --git a/longs/forms.py b/longs/forms.py index 1d928a20..51438920 100755 --- a/longs/forms.py +++ b/longs/forms.py @@ -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, ) diff --git a/longs/views.py b/longs/views.py index e8102633..0d1621de 100755 --- a/longs/views.py +++ b/longs/views.py @@ -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('