import json from dal import autocomplete from django.shortcuts import render, get_object_or_404, redirect from django import forms from django.utils import timezone # 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 ( FieldError, ObjectDoesNotExist, PermissionDenied, ViewDoesNotExist, ) from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin from django.views.generic.detail import DetailView from generic.mixins import SuperuserRequiredMixin from django.views.generic.edit import CreateView, UpdateView, DeleteView from django.views.generic import ListView from django.views.decorators.csrf import csrf_exempt from django.urls import reverse_lazy, reverse from django.http import Http404, JsonResponse from django.http import HttpResponseRedirect, HttpResponse from generic.models import CidUser from .forms import ( CaseCollectionCaseFormSet, CaseCollectionForm, CaseForm, CidReportAnswerForm, CidReportAnswerMarkForm, ConditionForm, FindingForm, SeriesForm, SeriesImageFormSet, SeriesFormSet, SeriesFindingForm, CaseDifferentialFormSet, StructureForm, ) from .models import ( Case, CaseCollection, CaseDetail, CidReportAnswer, Condition, PathologicalProcess, Presentation, Series, Examination, Finding, Structure, Subspecialty, SeriesFinding, SeriesImage, ) from .tables import ( CaseCollectionTable, CaseTable, ConditionTable, FindingTable, PathologicalProcessTable, PresentationTable, SeriesTable, StructureTable, SubspecialtyTable, ) from .filters import ( CaseCollectionFilter, CaseFilter, ConditionFilter, FindingFilter, PathologicalProcessFilter, PresentationFilter, SeriesFilter, StructureFilter, SubspecialtyFilter, ) from django_tables2 import SingleTableView, SingleTableMixin from django_filters.views import FilterView from .decorators import ( user_is_author_or_atlas_editor, user_is_author_or_atlas_editor_or_atlas_marker, user_is_author_or_atlas_series_checker_or_atlas_marker, user_is_atlas_editor, user_is_author_or_atlas_series_checker, user_is_atlas_marker, user_is_author_or_atlas_editor_or_atlas_marker, user_is_collection_author_or_atlas_editor, ) from collections import defaultdict import json import statistics import plotly.express as px from django.core.cache import cache from django.conf import settings from helpers.images import image_as_base64 import logging from copy import deepcopy from django.forms.models import model_to_dict from generic.views import ExamCloneMixin, ExamViews from reversion.views import RevisionMixin import reversion from rest_framework import viewsets from zipview.views import BaseZipView import os logger = logging.getLogger(__name__) class AuthorOrCheckerRequiredMixin(object): def get_object(self, *args, **kwargs): obj = super().get_object(*args, **kwargs) if ( self.request.user.groups.filter(name="atlas_editor").exists() or self.request.user.is_superuser ): return obj if self.request.user not in obj.get_author_objects(): raise PermissionDenied() # or Http404 return obj @login_required @user_is_author_or_atlas_editor_or_atlas_marker def case_detail(request, pk): case = get_object_or_404(Case, pk=pk) # if request.user not in atlas.author.all(): # raise PermissionDenied # logging.debug(atlas.subspecialty.first().name.all()) return render(request, "atlas/case_detail.html", {"case": case}) @login_required @user_is_author_or_atlas_series_checker_or_atlas_marker def series_detail(request, pk, finding_pk=None): series = get_object_or_404(Series, pk=pk) editing_finding = -1 if finding_pk is not None: finding = get_object_or_404(SeriesFinding, pk=finding_pk) series_finding_form = SeriesFindingForm(None, instance=finding) editing_finding = finding.pk else: series_finding_form = SeriesFindingForm(series_id=series.id) # logging.debug(atlas.subspecialty.first().name.all()) return render( request, "atlas/series.html", { "series": series, "series_finding_form": series_finding_form, "editing_finding": editing_finding, }, ) @login_required @user_is_atlas_editor def condition_detail(request, pk): condition = get_object_or_404(Condition, pk=pk) # logging.debug(atlas.subspecialty.first().name.all()) return render( request, "atlas/condition_detail.html", { "condition": condition, }, ) @login_required @user_is_atlas_editor def subspecialty_detail(request, pk): subspecialty = get_object_or_404(Subspecialty, pk=pk) # logging.debug(atlas.subspecialty.first().name.all()) return render( request, "atlas/subspecialty_detail.html", { "subspecialty": subspecialty, }, ) @login_required @user_is_atlas_editor def presentation_detail(request, pk): presentation = get_object_or_404(Presentation, pk=pk) # logging.debug(atlas.subspecialty.first().name.all()) return render( request, "atlas/presentation_detail.html", { "presentation": presentation, }, ) @login_required @user_is_atlas_editor def pathological_process_detail(request, pk): pathological_process = get_object_or_404(PathologicalProcess, pk=pk) # logging.debug(atlas.subspecialty.first().name.all()) return render( request, "atlas/pathological_process_detail.html", { "pathological_process": pathological_process, }, ) @login_required @user_is_atlas_editor def structure_detail(request, pk): structure = get_object_or_404(Structure, pk=pk) # logging.debug(atlas.subspecialty.first().name.all()) return render( request, "atlas/structure_detail.html", { "structure": structure, }, ) @login_required @user_is_atlas_editor def finding_detail(request, pk): finding = get_object_or_404(Finding, pk=pk) # logging.debug(atlas.subspecialty.first().name.all()) return render( request, "atlas/finding_detail.html", { "finding": finding, }, ) @login_required @user_is_author_or_atlas_editor def author_detail(request, pk): # logging.debug(Author.objects.all()) # author = get_object_or_404(Author, pk=pk) author = User.objects.get(pk=pk) atlas = Case.objects.filter(author=pk) return render( request, "atlas/category_detail.html", {"category": author, "atlas": atlas} ) @user_is_author_or_atlas_editor def author_list(request): authors = User.objects.all() return render(request, "atlas/author_list.html", {"authors": authors}) class CaseDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView): model = Case success_url = reverse_lazy("atlas:case_view") template_name = "confirm_delete.html" class SeriesFindingDelete(RevisionMixin, PermissionRequiredMixin, DeleteView): permission_required = "atlas.delete_seriesfinding" model = SeriesFinding template_name = "confirm_delete.html" # success_url = reverse_lazy("atlas:case_view") def get_success_url(self): pk = self.kwargs["pk"] series_id = get_object_or_404(SeriesFinding, pk=pk).series.id return reverse("atlas:series_detail", kwargs={"pk": series_id}) class SeriesDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView): model = Series template_name = "confirm_delete.html" success_url = reverse_lazy("atlas:series_view") class CaseCollectionDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView): model = CaseCollection template_name = "confirm_delete.html" success_url = reverse_lazy("atlas:collection_view") class ConditionDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView): model = Condition template_name = "confirm_delete.html" success_url = reverse_lazy("atlas:condition_view") class FindingDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView): model = Finding template_name = "confirm_delete.html" success_url = reverse_lazy("atlas:finding_view") class StructureDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView): model = Structure template_name = "confirm_delete.html" success_url = reverse_lazy("atlas:structure_view") class CaseCollectionCreate(RevisionMixin, LoginRequiredMixin, CreateView): model = CaseCollection form_class = CaseCollectionForm def get_initial(self): # print(self.request) if "pk" in self.kwargs: initial = super(CaseCollectionCreate, self).get_initial() # case = get_object_or_404(Case, pk=self.kwargs["pk"]) # initial["case"] = case.id return initial def get_form_kwargs(self): kwargs = super(CaseCollectionCreate, self).get_form_kwargs() kwargs.update({"user": self.request.user}) return kwargs def get_context_data(self, **kwargs): context = super(CaseCollectionCreate, self).get_context_data(**kwargs) if self.request.POST: context["case_formset"] = CaseCollectionCaseFormSet( self.request.POST, self.request.FILES, form_kwargs={"user": self.request.user}, ) context["case_formset"].full_clean() else: context["case_formset"] = CaseCollectionCaseFormSet( form_kwargs={"user": self.request.user} ) return context def form_valid(self, form): self.object = form.save(commit=False) self.object.save() form.instance.author.add(self.request.user.id) context = self.get_context_data(form=form) case_formset = context["case_formset"] if case_formset.is_valid(): response = super().form_valid(form) case_formset.instance = self.object case_formset.save() return response else: return super().form_invalid(form) class SeriesCreate(RevisionMixin, LoginRequiredMixin, CreateView): model = Series form_class = SeriesForm def get_initial(self): # print(self.request) if "pk" in self.kwargs: initial = super().get_initial() case = get_object_or_404(Case, pk=self.kwargs["pk"]) initial["case"] = case.id return initial def get_form_kwargs(self): kwargs = super(SeriesCreate, self).get_form_kwargs() kwargs.update({"user": self.request.user}) return kwargs def get_context_data(self, **kwargs): context = super(SeriesCreate, self).get_context_data(**kwargs) if self.request.POST: context["image_formset"] = SeriesImageFormSet( self.request.POST, self.request.FILES ) context["image_formset"].full_clean() else: context["image_formset"] = SeriesImageFormSet() return context def form_valid(self, form): self.object = form.save(commit=False) self.object.save() form.instance.author.add(self.request.user.id) context = self.get_context_data(form=form) series_formset = context["image_formset"] if series_formset.is_valid(): response = super().form_valid(form) series_formset.instance = self.object series_formset.save() return response else: return super().form_invalid(form) class CaseCollectionUpdate(RevisionMixin, AuthorOrCheckerRequiredMixin, UpdateView): model = CaseCollection form_class = CaseCollectionForm # fields = '__all__' # #fields = [ 'condition' ] # #initial = {'date_of_death': '05/01/2018'} # exclude = [ 'created_date', 'published_date' ] def get_form_kwargs(self): kwargs = super(CaseCollectionUpdate, self).get_form_kwargs() kwargs.update({"user": self.request.user}) return kwargs def get_context_data(self, **kwargs): context = super(CaseCollectionUpdate, self).get_context_data(**kwargs) if self.request.POST: context["case_formset"] = CaseCollectionCaseFormSet( self.request.POST, self.request.FILES, instance=self.object, form_kwargs={"user": self.request.user}, ) context["case_formset"].full_clean() else: context["case_formset"] = CaseCollectionCaseFormSet( instance=self.object, form_kwargs={"user": self.request.user} ) return context def form_valid(self, form): # save exam orders (there must be a better way to do this) # exam_orders = {} # for exam in self.object.exams.all(): # exam_orders[exam] = list(exam.exam_questions.all()) self.object = form.save(commit=False) self.object.save() form.instance.author.add(self.request.user.id) context = self.get_context_data(form=form) case_formset = context["case_formset"] # logger.debug(formset.is_valid()) if case_formset.is_valid(): response = super().form_valid(form) case_formset.instance = self.object case_formset.save() # restore exam orders # for exam in self.object.exams.all(): # if exam in exam_orders and self.object in exam_orders[exam]: # print(exam_orders[exam]) # exam.exam_questions.set(exam_orders[exam]) # exam.save() return response else: return super().form_invalid(form) class SeriesUpdate( RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView ): model = Series form_class = SeriesForm # fields = '__all__' # #fields = [ 'condition' ] # #initial = {'date_of_death': '05/01/2018'} # exclude = [ 'created_date', 'published_date' ] def get_context_data(self, **kwargs): context = super(SeriesUpdate, self).get_context_data(**kwargs) if self.request.POST: context["image_formset"] = SeriesImageFormSet( self.request.POST, self.request.FILES, instance=self.object ) context["image_formset"].full_clean() else: context["image_formset"] = SeriesImageFormSet(instance=self.object) return context def get_form_kwargs(self): kwargs = super(SeriesUpdate, self).get_form_kwargs() kwargs.update({"user": self.request.user}) return kwargs def form_valid(self, form): self.object = form.save(commit=False) self.object.save() form.instance.author.add(self.request.user.id) context = self.get_context_data(form=form) image_formset = context["image_formset"] # logger.debug(formset.is_valid()) if image_formset.is_valid(): response = super().form_valid(form) image_formset.instance = self.object image_formset.save() return response else: return super().form_invalid(form) class ConditionUpdate( RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView ): model = Condition form_class = ConditionForm class ConditionCreate(RevisionMixin, LoginRequiredMixin, CreateView): model = Condition form_class = ConditionForm class FindingUpdate( RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView ): model = Finding form_class = FindingForm class FindingCreate(RevisionMixin, LoginRequiredMixin, CreateView): model = Finding form_class = FindingForm class StructureUpdate( RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView ): model = Structure form_class = StructureForm class StructureCreate(RevisionMixin, LoginRequiredMixin, CreateView): model = Structure form_class = StructureForm class AtlasCreateBase(RevisionMixin, LoginRequiredMixin): model = Case form_class = CaseForm def get_form_kwargs(self): kwargs = super(AtlasCreateBase, self).get_form_kwargs() kwargs.update({"user": self.request.user}) return kwargs def get_context_data(self, **kwargs): context = super(AtlasCreateBase, self).get_context_data(**kwargs) if self.request.POST: context["series_formset"] = SeriesFormSet( self.request.POST, self.request.FILES, form_kwargs={"user": self.request.user}, ) context["casedifferential_formset"] = CaseDifferentialFormSet( self.request.POST, self.request.FILES ) context["series_formset"].full_clean() context["casedifferential_formset"].full_clean() else: # context["series_formset"] = SeriesFormSet(instance=Case.objects.filter(author__id=self.request.user.id), queryset=Series.objects.filter(author__id=self.request.user.id)) context["series_formset"] = SeriesFormSet( form_kwargs={"user": self.request.user} ) context["casedifferential_formset"] = CaseDifferentialFormSet() return context def form_valid(self, form): self.object = form.save(commit=False) # Add exam objects self.object.save() form.instance.author.add(self.request.user.id) context = self.get_context_data(form=form) series_formset = context["series_formset"] casedifferential_formset = context["casedifferential_formset"] if series_formset.is_valid() and casedifferential_formset.is_valid(): response = super().form_valid(form) series_formset.instance = self.object series_formset.save() casedifferential_formset.instance = self.object casedifferential_formset.save() # If the normal submit button is pressed we save as normal if "submit" in self.request.POST: return response # else we redirect to the clone url else: return redirect("atlas:case_clone", pk=self.object.pk) else: return super().form_invalid(form) # @login_required class AtlasCreate(AtlasCreateBase, CreateView): def get_initial(self): # There has to be a better way... try: s = (i.pk for i in self.request.user.atlas_default.site.all()) self.initial.update({"site": s}) except AttributeError: pass return self.initial class AtlasUpdate( RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView ): model = Case form_class = CaseForm # fields = '__all__' # #fields = [ 'condition' ] # #initial = {'date_of_death': '05/01/2018'} # exclude = [ 'created_date', 'published_date' ] def get_form_kwargs(self): kwargs = super(AtlasUpdate, self).get_form_kwargs() kwargs.update({"user": self.request.user}) return kwargs def get_context_data(self, **kwargs): context = super(AtlasUpdate, self).get_context_data(**kwargs) if self.request.POST: context["series_formset"] = SeriesFormSet( self.request.POST, self.request.FILES, instance=self.object, form_kwargs={"user": self.request.user}, ) context["casedifferential_formset"] = CaseDifferentialFormSet( self.request.POST, self.request.FILES, instance=self.object ) context["series_formset"].full_clean() context["casedifferential_formset"].full_clean() else: context["series_formset"] = SeriesFormSet( instance=self.object, form_kwargs={"user": self.request.user} ) context["casedifferential_formset"] = CaseDifferentialFormSet( instance=self.object ) return context def form_valid(self, form): self.object = form.save(commit=False) self.object.save() form.instance.author.add(self.request.user.id) context = self.get_context_data(form=form) series_formset = context["series_formset"] casedifferential_formset = context["casedifferential_formset"] # logger.debug(formset.is_valid()) if series_formset.is_valid() and casedifferential_formset.is_valid(): response = super().form_valid(form) series_formset.instance = self.object series_formset.save() casedifferential_formset.instance = self.object casedifferential_formset.save() return response else: return super().form_invalid(form) class AtlasClone(AtlasCreateBase, CreateView): """Clones a existing atlas""" # fields = '__all__' # #fields = [ 'condition' ] # #initial = {'date_of_death': '05/01/2018'} # exclude = [ 'created_date', 'published_date' ] def get_initial(self): # print(self.request) old_object = get_object_or_404(Case, pk=self.kwargs["pk"]) initial_data = model_to_dict(old_object, exclude=["id"]) return initial_data @login_required @user_is_author_or_atlas_editor def atlas_scrap(request, pk): try: atlas = Case.objects.get(pk=pk) except Case.DoesNotExist: raise Http404("Case does not exist") atlas.scrapped = not atlas.scrapped atlas.save() return HttpResponseRedirect(reverse("atlas:case_detail", args=(pk,))) # @login_required # def edit_abnormality_popup(request): # instance = get_object_or_404(Abnormality, pk = pk) # form = AbnormalityForm(request.POST or None) # if form.is_valid(): # instance = form.save() # return HttpResponse('' % (instance.pk, instance)) # return render(request, "atlas/create_simple.html", {'form': form}) @csrf_exempt def get_examination_id(request): if request.is_ajax(): examination_name = request.GET["examination_name"] examination_id = Examination.objects.get(name=examination_name).id data = { "examination_id": examination_id, } return HttpResponse(json.dumps(data), content_type="application/json") return HttpResponse("/") class CaseView(LoginRequiredMixin, SingleTableMixin, FilterView): model = Case table_class = CaseTable template_name = "atlas/view.html" filterset_class = CaseFilter class SeriesView(LoginRequiredMixin, SingleTableMixin, FilterView): model = Series table_class = SeriesTable template_name = "atlas/view.html" filterset_class = SeriesFilter class ConditionView(LoginRequiredMixin, SingleTableMixin, FilterView): model = Condition table_class = ConditionTable template_name = "atlas/view.html" filterset_class = ConditionFilter class SubspecialtyView(LoginRequiredMixin, SingleTableMixin, FilterView): model = Subspecialty table_class = SubspecialtyTable template_name = "atlas/view.html" filterset_class = SubspecialtyFilter class PresentationView(LoginRequiredMixin, SingleTableMixin, FilterView): model = Presentation table_class = PresentationTable template_name = "atlas/view.html" filterset_class = PresentationFilter class PathologicalProcessView(LoginRequiredMixin, SingleTableMixin, FilterView): model = PathologicalProcess table_class = PathologicalProcessTable template_name = "atlas/view.html" filterset_class = PathologicalProcessFilter class StructureView(LoginRequiredMixin, SingleTableMixin, FilterView): model = Structure table_class = StructureTable template_name = "atlas/view.html" filterset_class = StructureFilter class FindingView(LoginRequiredMixin, SingleTableMixin, FilterView): model = Finding table_class = FindingTable template_name = "atlas/view.html" filterset_class = FindingFilter @login_required @user_is_author_or_atlas_series_checker def series_order_dicom(request, pk): series = get_object_or_404(Series, pk=pk) try: series.order_by_dicom() except: return HttpResponse("

Series does not appear to contain dicoms

") return redirect("atlas:series_detail", pk=pk) @login_required @user_is_author_or_atlas_series_checker def series_anonymise_dicom(request, pk): series = get_object_or_404(Series, pk=pk) series.anonymise_images() return redirect("atlas:series_detail", pk=pk) @login_required @user_is_author_or_atlas_series_checker def series_order_dicom_instance(request, pk): series = get_object_or_404(Series, pk=pk) try: series.order_by_dicom("InstanceNumber") except: return HttpResponse( "

Series does not appear to contain dicoms (or field InstanceNumber)

" ) return redirect("atlas:series_detail", pk=pk) @login_required @user_is_author_or_atlas_series_checker def series_order_dicom_SeriesInstanceUID(request, pk): series = get_object_or_404(Series, pk=pk) try: series.order_by_dicom("SeriesInstanceUID") except: return HttpResponse( "

Series does not appear to contain dicoms (or field SeriesInstanceUID)

" ) return redirect("atlas:series_detail", pk=pk) @login_required @user_is_author_or_atlas_series_checker def series_order_upload_filename(request, pk): series = get_object_or_404(Series, pk=pk) series.order_by_upload_filename() return redirect("atlas:series_detail", pk=pk) class SeriesImagesZipView(SuperuserRequiredMixin, BaseZipView): """Download all images from an image series""" def get_files(self): series = Series.objects.get(pk=self.kwargs["pk"]) return [i.image.file for i in series.images.all()] def create_series_findings(request): # posts = Post.objects.all() response_data = {} if request.POST.get("action") == "post": series_finding_id = int(request.POST.get("series_finding_id")) series_id = request.POST.get("series") findings_ids = json.loads(request.POST.get("findings")) structure_ids = json.loads(request.POST.get("structures")) description = request.POST.get("description") annotation_json = request.POST.get("annotation_json") viewport_json = request.POST.get("viewport_json") series = Series.objects.get(pk=series_id) findings = Finding.objects.filter(pk__in=findings_ids) structures = Structure.objects.filter(pk__in=structure_ids) if series_finding_id > 0: sf = get_object_or_404(SeriesFinding, pk=series_finding_id) sf.series = series sf.description = description sf.annotation_json = annotation_json sf.viewport_json = viewport_json else: sf = SeriesFinding.objects.create( series=series, description=description, annotation_json=annotation_json, viewport_json=viewport_json, ) sf.findings.set(findings) sf.structures.set(structures) sf.save() return JsonResponse({"success": True}) return JsonResponse({"success": False}) return render(request, "create_post.html", {"posts": posts}) class ConditionAutocomplete(autocomplete.Select2QuerySetView): def get_queryset(self): # Don't forget to filter out results depending on the visitor ! if not self.request.user.is_authenticated: return Condition.objects.none() qs = Condition.objects.all() if self.q: # This raises a fielderror which breaks creating a new item if not caught try: qs = qs.filter(name__icontains=self.q) except FieldError: return Condition.objects.none() return qs class PresentationAutocomplete(autocomplete.Select2QuerySetView): def get_queryset(self): # Don't forget to filter out results depending on the visitor ! if not self.request.user.is_authenticated: return Presentation.objects.none() qs = Presentation.objects.all() if self.q: # This raises a fielderror which breaks creating a new item if not caught try: qs = qs.filter(name__icontains=self.q) except FieldError: return Presentation.objects.none() return qs class FindingAutocomplete(autocomplete.Select2QuerySetView): def get_queryset(self): # Don't forget to filter out results depending on the visitor ! if not self.request.user.is_authenticated: return Finding.objects.none() qs = Finding.objects.all() if self.q: # This raises a fielderror which breaks creating a new item if not caught try: qs = qs.filter(name__icontains=self.q) except FieldError: return Finding.objects.none() return qs class StructureAutocomplete(autocomplete.Select2QuerySetView): def get_queryset(self): # Don't forget to filter out results depending on the visitor ! if not self.request.user.is_authenticated: return Structure.objects.none() qs = Structure.objects.all() if self.q: # This raises a fielderror which breaks creating a new item if not caught try: qs = qs.filter(name__icontains=self.q) except FieldError: return Structure.objects.none() return qs @login_required @user_is_atlas_editor def categories_list(request): # condition = get_object_or_404(Condition, pk=pk) # logging.debug(atlas.subspecialty.first().name.all()) return render( request, "atlas/categories_list.html", # { # "condition": condition, # }, ) # def collection_view(request): # collections = CaseCollection.objects.all() # # return render( # request, "atlas/collection_view.html", {"collections": collections} # ) class CollectionView(LoginRequiredMixin, SingleTableMixin, FilterView): model = CaseCollection table_class = CaseCollectionTable template_name = "atlas/view.html" filterset_class = CaseCollectionFilter @user_is_collection_author_or_atlas_editor def collection_detail(request, pk): collection = get_object_or_404(CaseCollection, pk=pk) cases = collection.cases.all().order_by("casedetail__sort_order") return render(request, "atlas/collection_detail.html", {"collection": collection, "cases": cases}) def collection_take(request, pk): collection = get_object_or_404(CaseCollection, pk=pk) return render(request, "atlas/collection_take.html", {"collection": collection}) @user_is_collection_author_or_atlas_editor def collection_mark_overview(request, pk): collection = get_object_or_404(CaseCollection, pk=pk) if collection.collection_type == "REV": raise Http404("Collection in review mode") cases = collection.cases.all().order_by("casedetail__sort_order").prefetch_related() case_details = CaseDetail.objects.filter( case__in=cases, collection=collection ).prefetch_related("case") answers = CidReportAnswer.objects.filter(question__in=case_details) case_map = {} for cd in case_details: case_map[cd] = [] for answer in answers: case_map[answer.question].append(answer) return render( request, "atlas/collection_mark_overview.html", { "collection": collection, "answers": answers, "case_details": case_details, "case_map": case_map, }, ) @user_is_collection_author_or_atlas_editor def collection_mark_question(request, pk, case_number): collection = get_object_or_404(CaseCollection, pk=pk) if collection.collection_type == "REV": raise Http404("Collection in review mode") cases = collection.cases.all().order_by("casedetail__sort_order").prefetch_related() case = cases[case_number] previous = case_number > 0 next = case_number < (len(cases) - 1) case_detail = CaseDetail.objects.filter(case=case, collection=collection) answers = CidReportAnswer.objects.filter(question__in=case_detail) answer_forms = [] if request.method == "POST": for answer in answers: if answer: form = CidReportAnswerMarkForm( request.POST, instance=answer, prefix=answer.id ) else: form = CidReportAnswerMarkForm(request.POST, prefix=answer.id) if form.is_valid(): answer = form.save(commit=False) # answer.cid = cid # answer.question = case_detail # answer.published_date = timezone.now() answer.save() kwargs = {"pk": pk} if "save" in request.POST: return redirect( "atlas:collection_mark_question", case_number=case_number, **kwargs ) if "next" in request.POST: return redirect( "atlas:collection_mark_question", case_number=case_number + 1, **kwargs ) elif "previous" in request.POST: return redirect( "atlas:collection_mark_question", case_number=case_number - 1, **kwargs ) elif "goto" in request.POST: return redirect( "atlas:collection_mark_question", case_number=int(request.POST.get("goto")), **kwargs, ) else: for answer in answers: form = CidReportAnswerMarkForm(instance=answer, prefix=answer.pk) answer_forms.append((answer, form)) series_list = case.series.all().prefetch_related("images", "examination", "plane") return render( request, "atlas/collection_mark_question.html", { "collection": collection, "series_list": series_list, "answers": answers, "case_detail": case_detail, "case": case, "case_number": case_number, "answer_forms": answer_forms, "previous": previous, "next": next, }, ) @user_is_collection_author_or_atlas_editor def collection_candidates(request, pk): collection = get_object_or_404(CaseCollection, pk=pk) if ( request.user in collection.author.all() or request.user.groups.filter(name="cid_user_manager").exists() ): cid_users = collection.valid_users.all() cid_user_count = cid_users.count() return render( request, "atlas/collection_candidates.html", { "collection": collection, "cid_users": cid_users, "cid_user_count": cid_user_count, }, ) raise PermissionDenied def collection_take_overview(request, pk, cid, passcode): collection = get_object_or_404(CaseCollection, pk=pk) if not collection.active: raise Http404("Exam not found") if not collection.check_cid_user(cid, passcode): raise Http404("Error accessing exam") cases = collection.cases.all().order_by("casedetail__sort_order").prefetch_related() case_details = CaseDetail.objects.filter(case__in=cases, collection=collection) answers = CidReportAnswer.objects.filter(cid=cid, question__in=case_details) answer_question_map = {} for ans in answers: answer_question_map[ans.question] = ans question_answer_tuples = [] answer_count = 0 for q in case_details: if q in answer_question_map and answer_question_map[q].answer: question_answer_tuples.append((q, answer_question_map[q])) answer_count += 1 else: question_answer_tuples.append((q, None)) c = CidUser.objects.filter(cid=cid).first() cid_user_exam = collection.get_or_create_cid_user_exam(cid_user=c) return render( request, "atlas/collection_take_overview.html", { "collection": collection, "cid": cid, "question_answer_tuples": question_answer_tuples, "answer_count": answer_count, "collection_length": len(cases), "passcode": passcode, "cid_user_exam": cid_user_exam, }, ) def collection_case_view_take(request, pk, case_number, cid, passcode): collection = get_object_or_404(CaseCollection, pk=pk) form = None answer = None if collection.collection_type != collection.COLLECTION_TYPE_CHOICES.REVIEW: if not collection.active: raise Http404("Exam not found") if not collection.check_cid_user(cid, passcode, request): raise Http404("Error accessing exam") cases = collection.cases.all().order_by("casedetail__sort_order").prefetch_related() case = cases[case_number] c = CidUser.objects.filter(cid=cid).first() cid_user_exam = collection.get_or_create_cid_user_exam(cid_user=c) case_detail = CaseDetail.objects.get(case=case, collection=collection) answer = case_detail.cidreportanswer_set.filter(cid=cid).first() if request.method == "POST": if not collection.publish_results: if answer: form = CidReportAnswerForm(request.POST, instance=answer) else: form = CidReportAnswerForm(request.POST) if form.is_valid(): answer = form.save(commit=False) answer.cid = cid answer.question = case_detail # answer.published_date = timezone.now() answer.save() cid_user_exam.end_time = timezone.now() cid_user_exam.save() kwargs = {"pk": pk, "cid": cid, "passcode": passcode} if "next" in request.POST: return redirect( "atlas:collection_case_view_take", case_number=case_number + 1, **kwargs ) elif "previous" in request.POST: return redirect( "atlas:collection_case_view_take", case_number=case_number - 1, **kwargs ) elif "finish" in request.POST: return redirect("atlas:collection_take_overview", **kwargs) elif "goto" in request.POST: return redirect( "atlas:collection_case_view_take", case_number=int(request.POST.get("goto")), **kwargs, ) else: form = CidReportAnswerForm(instance=answer) series_list = case.series.all().prefetch_related("images", "examination", "plane") previous = case_number > 0 next = case_number < (len(cases) - 1) if collection.publish_results: show_title = collection.show_title_post show_history = collection.show_history_post show_description = collection.show_description_post show_discussion = collection.show_discussion_post show_report = collection.show_report_post else: show_title = collection.show_title_pre show_history = collection.show_history_pre show_description = collection.show_description_pre show_discussion = collection.show_discussion_pre show_report = collection.show_report_pre return render( request, "atlas/collection_case_view_take.html", { "form": form, "collection": collection, "case": case, "series_list": series_list, "case_number": case_number, "previous": previous, "next": next, "cid": cid, "passcode": passcode, "answer": answer, "show_title": show_title, "show_history": show_history, "show_description": show_description, "show_discussion": show_discussion, "show_report": show_report, }, ) @user_is_collection_author_or_atlas_editor def collection_case_view(request, pk, case_number): collection = get_object_or_404(CaseCollection, pk=pk) form = None answer = None if collection.collection_type != collection.COLLECTION_TYPE_CHOICES.REVIEW: if not collection.active: raise Http404("Exam not found") cases = collection.cases.all().order_by("casedetail__sort_order").prefetch_related() case = cases[case_number] series_list = case.series.all().prefetch_related("images", "examination", "plane") previous = case_number > 0 next = case_number < (len(cases) - 1) return render( request, "atlas/case_detail.html", { "form": form, "collection": collection, "collection_length": len(cases), "case": case, "series_list": series_list, "case_number": case_number, "previous": previous, "next": next, "answer": answer, }, ) # TODO: this needs a major cleanup @user_is_collection_author_or_atlas_editor def collection_scores_cid(request, pk): collection = get_object_or_404(CaseCollection, pk=pk) if not collection.exam_mode: raise Http404("Collection not in exam mode") user_answers_and_marks = defaultdict(list) user_answers_marks = defaultdict(list) user_answers = defaultdict(list) user_names = {} # cid_passcodes = {} by_question = defaultdict(dict) unmarked = set() cases = collection.cases.all().order_by("casedetail__sort_order").prefetch_related() case_details = CaseDetail.objects.filter( case__in=cases, collection=collection ).prefetch_related("case") cid_user_answers = CidReportAnswer.objects.filter(question__in=case_details) # questions = exam.exam_questions.all().prefetch_related("answers") # cid_user_answers = CidReportAnswer.objects.select_related("question").filter( # question__in=questions, exam__id=pk # ) cids = set() # Loop through all candidates for cid_user_answer in cid_user_answers: # Convoluted (probably...) cid = cid_user_answer.cid # cid_passcodes[cid] = cid_user_answer.passcode cids.add(cid) s = cid_user_answer user_names[cid] = cid q = cid_user_answer.question.case # if not s: # # skip if no answer # user_answers_marks[cid].append(0) # user_answers[cid].append("") # by_question[q].append(("", 0)) # continue ans = s.answer answer_score = s.get_answer_score() if answer_score == "unmarked": index = cases.index(q) unmarked.add(index) user_answers[cid].append(ans) user_answers_marks[cid].append(answer_score) user_answers_and_marks[cid].append((ans, answer_score)) by_question[q][cid] = (ans, answer_score, s.feedback) 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 != "unmarked"] ) user_scores_normalised[user] = user_scores[user] # ignore scores of 0 for stats user_scores_list = [i for i in user_scores.values() if i > 0] max_score = len(cases) if len(user_scores_list) < 1: mean = 0 median = 0 mode = 0 fig_html = "" else: mean = statistics.mean(user_scores_list) median = statistics.median(user_scores_list) try: mode = statistics.mode(user_scores_list) except statistics.StatisticsError: mode = "No unique mode" df = user_scores_list fig = px.histogram( df, x=0, title="{}: distribution of scores".format(collection), labels={"0": "Score"}, height=400, width=600, ) fig_html = fig.to_html() collection.stats_mean = mean collection.stats_median = median collection.stats_mode = mode collection.stats_candidates = len(user_scores_list) collection.stats_max_possible = max_score collection.stats_min = min(user_scores_list) collection.stats_max = max(user_scores_list) collection.stats_graph = fig_html collection.user_scores = user_scores collection.save() return render( request, f"atlas/collection_scores.html", { "cids": sorted(cids), # "cid_passcodes": cid_passcodes, "exam": collection, "unmarked": unmarked, "questions": cases, "by_question": by_question, "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, "max_score": max_score, "mean": mean, "median": median, "mode": mode, "plot": fig_html, }, )