import json import pprint from typing import Any from dal import autocomplete from django.db.models.base import Model as Model from django.db.models.query import QuerySet 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 import View from django.views.generic.detail import DetailView import pydicom from generic.mixins import SuperuserRequiredMixin from django.views.generic.edit import CreateView, UpdateView, DeleteView, FormView 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, CidUserExam from atlas.helpers import get_cases_available_to_user from .forms import ( AddCollectionToCaseForm, CaseAuthorForm, CaseCollectionAuthorForm, CaseCollectionCaseFormSet, CaseCollectionForm, CaseDetailForm, CaseForm, CaseResourceFormSet, CidQuestionAnswerForm, CidReportAnswerForm, CidReportAnswerMarkForm, ConditionAutocompleteForm, ConditionForm, ExamGroupsForm, FindingForm, JsonAnswerForm, PriorCaseForm, QuestionSchemaForm, ResourceForm, SelfReviewForm, SeriesAuthorForm, SeriesForm, SeriesImageFormSet, SeriesFormSet, SeriesFindingForm, CaseDifferentialFormSet, StructureForm, UserQuestionAnswerForm, UserReportAnswerForm, ) from .models import ( Case, CaseCollection, CaseDetail, CasePrior, CidReportAnswer, Condition, Differential, PathologicalProcess, Presentation, QuestionSchema, Resource, SelfReview, Series, Examination, Finding, Structure, Subspecialty, SeriesFinding, SeriesImage, UncategorisedDicom, UserReportAnswer, ) from .tables import ( CaseCollectionTable, CaseTable, ConditionTable, FindingTable, PathologicalProcessTable, PresentationTable, QuestionSchemaTable, SeriesTable, StructureTable, SubspecialtyTable, ) from .filters import ( CaseCollectionFilter, CaseFilter, ConditionFilter, FindingFilter, PathologicalProcessFilter, PresentationFilter, QuestionSchemaFilter, SeriesFilter, StructureFilter, SubspecialtyFilter, ) from django_tables2 import SingleTableView, SingleTableMixin from django_filters.views import FilterView from autocomplete import HTMXAutoComplete from .decorators import ( user_is_author_or_atlas_editor, user_has_case_view_access, user_is_author_or_atlas_series_checker_or_atlas_marker_or_open_access, user_is_atlas_editor, user_is_author_or_atlas_series_checker, user_is_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 compare_dicom_datasets, image_as_base64, print_dicom import logging from copy import deepcopy from django.forms.models import model_to_dict from generic.views import ( AuthorRequiredMixin, ExamCloneMixin, ExamGroupsUpdateBase, ExamViews, SeriesImagesZipViewBase, ) from reversion.views import RevisionMixin import reversion from rest_framework import viewsets import os import difflib 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 class AtlasEditorRequiredMixin(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 raise PermissionDenied("You must be an atlas editor to do this.") # or Http404 @login_required @user_has_case_view_access 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_or_open_access def series_thumbnail(request, pk, finding_pk=None): series = get_object_or_404(Series, pk=pk) thumbnail, _ = series.get_thumbnail(recreate=True) return HttpResponse(thumbnail) @login_required @user_is_author_or_atlas_series_checker_or_atlas_marker_or_open_access def series_detail(request, pk, finding_pk=None): series = get_object_or_404(Series, pk=pk) can_edit = series.check_user_can_edit(request.user) 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, "can_edit": can_edit, }, ) @login_required # @user_is_atlas_editor def question_schema_detail(request, pk: int): question_schema = get_object_or_404(QuestionSchema, pk=pk) example_form = question_schema.get_example_form() # logging.debug(atlas.subspecialty.first().name.all()) return render( request, "atlas/question_schema_detail.html", {"question_schema": question_schema, "example_form": example_form}, ) @login_required # @user_is_atlas_editor def question_schema_schemas(request): question_schemas = QuestionSchema.objects.all() return render( request, "atlas/question_schemas_preset.html", { "question_schemas": question_schemas, }, ) @login_required # @user_is_atlas_editor def condition_detail(request, pk): condition = get_object_or_404(Condition, pk=pk) form = ConditionAutocompleteForm() can_merge = False if ( request.user.is_superuser or request.user.groups.filter(name="atlas_editor").exists() ): can_merge = True # logging.debug(atlas.subspecialty.first().name.all()) return render( request, "atlas/condition_detail.html", {"condition": condition, "form": form, "can_merge": can_merge}, ) # class GetConditionAutocomplete(HTMXAutoComplete): # name = "conditions" # multiselect = False # # class Meta: # model = Condition @login_required @user_is_atlas_editor def condition_merge(request, pk_to_merge): if not request.htmx: raise Http404 condition_to_merge = get_object_or_404(Condition, pk=pk_to_merge) # condition_to_merge_into = get_object_or_404(Condition, pk=pk_to_merge_into) print(condition_to_merge.case_set) if condition_to_merge.rcr_curriculum: return HttpResponse("You cannot merge rcr curriculum items") condition_to_merge_into = request.POST.get("conditions") if not condition_to_merge_into: return HttpResponse("You need to select a condition to merge into.") condition_to_merge_into = get_object_or_404(Condition, pk=condition_to_merge_into) if condition_to_merge == condition_to_merge_into: return HttpResponse("You cannot merge into the same condition.") # move all relevant relationships cases = condition_to_merge.case_set.all().values_list("id", flat=True) condition_to_merge_into.case_set.add(cases) # cases = Case.objects.filter(condition=condition_to_merge) # cases.update(condition=condition_to_merge_into) differentials = Differential.objects.filter(condition=condition_to_merge) differentials.update(condition=condition_to_merge_into) return HttpResponse("Merged") # conditions = Condition.objects.all() # return render(request, "atlas/condition_options.html", {"conditions": conditions}) @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 def resource_detail(request, pk): resource = get_object_or_404(Resource, pk=pk) return render(request, "atlas/resource_detail.html", {"resource": resource}) @login_required def resource_view(request, pk): resource = get_object_or_404(Resource, pk=pk) return HttpResponse(resource.get_display()) @login_required def author_detail(request, pk): # logging.debug(Author.objects.all()) # author = get_object_or_404(Author, pk=pk) author = User.objects.get(pk=pk) if request.user.pk == pk: atlas = Case.objects.filter(author=pk) else: atlas = Case.objects.filter(author=pk, open_access=True) 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}) def index(request): return render(request, "atlas/index.html", {}) def user_collections(request): collections = request.user.user_casecollection_exams.all() return render(request, "atlas/user_collections.html", {"collections": collections}) def add_case_to_collection(request, collection_id): if not request.htmx: return Http404 collection = get_object_or_404(CaseCollection, pk=collection_id) if request.method == "POST": case = get_object_or_404(Case, pk=request.POST["case"]) if case in collection.cases.all(): return HttpResponse("Case already in collection") print(case) collection.add_case(case) return HttpResponse("Case added to collection (refresh to see)") # if request.method == "POST": cases = get_cases_available_to_user(request.user) return render( request, "atlas/add_case_to_collection.html", {"cases": cases, "collection": collection}, ) @login_required def user_uploads_series(request, series_instance_uid: str): # We don't restrict to teh uploading user here. Should we? dicoms = UncategorisedDicom.objects.filter(series_instance_uid=series_instance_uid) if not dicoms: raise Http404("Series not found") return render( request, "atlas/user_upload_viewer.html", {"dicoms": dicoms, "series_id": series_instance_uid}, ) @login_required def new_uploads(request): return render(request, "atlas/new_uploads.html", {}) @login_required @user_passes_test(lambda u: u.is_superuser) def all_uploads(request, case_id: int | None = None): return user_uploads(request, case_id, user_only=False) @user_passes_test(lambda u: u.is_superuser) def other_user_uploads(request, user_pk: int, case_id: int | None = None): return user_uploads(request, case_id, user_only=False, user_pk=user_pk) @login_required def user_uploads( request, case_id: int | None = None, user_only: bool = True, user_pk: int | None = None, ): user = request.user if user_only: dicoms = UncategorisedDicom.objects.filter(user=request.user) elif user_pk is not None: user = get_object_or_404(User, pk=user_pk) dicoms = UncategorisedDicom.objects.filter(user=user) else: dicoms = UncategorisedDicom.objects.filter() data = defaultdict(list) for d in dicoms: tags = d.basic_dicom_tags if tags is None: d.save() tags = d.basic_dicom_tags data[tags["SeriesInstanceUID"]].append((tags, d.created_date)) series_list = [] for series in data: tags, date = data[series][0] series_list.append((series, len(data[series]), tags, date)) series_list.sort(key=lambda l: l[-1]) case = False if case_id is not None: case = get_object_or_404(Case, pk=case_id) return render( request, "atlas/user_uploads.html", {"series_list": series_list, "case": case, "user": user}, ) class CaseDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView): model = Case success_url = reverse_lazy("atlas:case_view") template_name = "confirm_delete.html" # TODO fix permissions 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_object(self): series_finding = super().get_object() series = series_finding.series if series.check_user_can_edit(self.request.user): return series_finding else: raise PermissionDenied 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, AtlasEditorRequiredMixin, DeleteView): model = Series template_name = "confirm_delete.html" success_url = reverse_lazy("atlas:series_view") class CaseCollectionDelete(RevisionMixin, AtlasEditorRequiredMixin, DeleteView): model = CaseCollection template_name = "confirm_delete.html" success_url = reverse_lazy("atlas:collection_view") class ConditionDelete(RevisionMixin, AtlasEditorRequiredMixin, DeleteView): model = Condition template_name = "confirm_delete.html" success_url = reverse_lazy("atlas:condition_view") class FindingDelete(RevisionMixin, AtlasEditorRequiredMixin, DeleteView): model = Finding template_name = "confirm_delete.html" success_url = reverse_lazy("atlas:finding_view") class StructureDelete(RevisionMixin, AtlasEditorRequiredMixin, DeleteView): model = Structure template_name = "confirm_delete.html" success_url = reverse_lazy("atlas:structure_view") class ResourceDelete(LoginRequiredMixin, DeleteView): model = Resource template_name = "confirm_delete.html" success_url = reverse_lazy("atlas:resource_view") class ResourceCreate(LoginRequiredMixin, CreateView): model = Resource form_class = ResourceForm def form_valid(self, form): self.object = form.save(commit=False) self.object.save() form.instance.author.add(self.request.user.id) return super().form_valid(form) class ResourceView(LoginRequiredMixin, ListView): model = Resource # form_class = ResourceForm class CaseCollectionCreate(RevisionMixin, LoginRequiredMixin, CreateView): model = CaseCollection form_class = CaseCollectionForm template_name = "atlas/casecollection_form.html" 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 template_name = "atlas/casecollection_form.html" # 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, "collection": self.object}, ) context["case_formset"].full_clean() else: context["case_formset"] = CaseCollectionCaseFormSet( instance=self.object, form_kwargs={"user": self.request.user, "collection": self.object}, ) context["collection"] = context["casecollection"] 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"] # 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 ResourceUpdate(LoginRequiredMixin, UpdateView): model = Resource form_class = ResourceForm def form_valid(self, form): self.object = form.save(commit=False) self.object.save() form.instance.author.add(self.request.user.id) return super().form_valid(form) class ConditionUpdate(RevisionMixin, LoginRequiredMixin, UpdateView): model = Condition form_class = ConditionForm class ConditionCreate(RevisionMixin, LoginRequiredMixin, CreateView): model = Condition form_class = ConditionForm class QuestionSchemaCreate(LoginRequiredMixin, CreateView): model = QuestionSchema form_class = QuestionSchemaForm class QuestionSchemaUpdate(LoginRequiredMixin, UpdateView): model = QuestionSchema form_class = QuestionSchemaForm class QuestionSchemaDelete(AuthorOrCheckerRequiredMixin, DeleteView): model = QuestionSchema template_name = "confirm_delete.html" class FindingUpdate(RevisionMixin, LoginRequiredMixin, UpdateView): model = Finding form_class = FindingForm class FindingCreate(RevisionMixin, LoginRequiredMixin, CreateView): model = Finding form_class = FindingForm class StructureUpdate(RevisionMixin, LoginRequiredMixin, 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["caseresource_formset"] = CaseResourceFormSet( self.request.POST, self.request.FILES, form_kwargs={"user": self.request.user}, ) context["series_formset"].full_clean() context["casedifferential_formset"].full_clean() context["caseresource_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() context["caseresource_formset"] = CaseResourceFormSet( form_kwargs={"user": self.request.user} ) 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"] caseresource_formset = context["caseresource_formset"] if ( series_formset.is_valid() and casedifferential_formset.is_valid() and caseresource_formset.is_valid() ): response = super().form_valid(form) series_formset.instance = self.object series_formset.save() casedifferential_formset.instance = self.object casedifferential_formset.save() caseresource_formset.instance = self.object caseresource_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): if "pk" in self.kwargs: initial = super().get_initial() exam = get_object_or_404(CaseCollection, pk=self.kwargs["pk"]) initial["exams"] = [exam.id] return initial ## 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["caseresource_formset"] = CaseResourceFormSet( self.request.POST, self.request.FILES, instance=self.object, form_kwargs={"user": self.request.user}, ) context["series_formset"].full_clean() context["casedifferential_formset"].full_clean() context["caseresource_formset"].full_clean() else: context["series_formset"] = SeriesFormSet( instance=self.object, form_kwargs={"user": self.request.user} ) context["casedifferential_formset"] = CaseDifferentialFormSet( instance=self.object ) context["caseresource_formset"] = CaseResourceFormSet( instance=self.object, 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) series_formset = context["series_formset"] casedifferential_formset = context["casedifferential_formset"] caseresource_formset = context["caseresource_formset"] # logger.debug(formset.is_valid()) if ( series_formset.is_valid() and casedifferential_formset.is_valid() and caseresource_formset.is_valid() ): response = super().form_valid(form) series_formset.instance = self.object series_formset.save() casedifferential_formset.instance = self.object casedifferential_formset.save() caseresource_formset.instance = self.object caseresource_formset.save() return response else: return super().form_invalid(form) def add_collection_to_case_form(request, case_id): if request.method == "POST": form = AddCollectionToCaseForm(request.POST, user=request.user) if form.is_valid(): case = get_object_or_404(Case, pk=case_id) case.casecollection_set.add(*form.cleaned_data["casecollection"]) # do_something_with_form_data(form.cleaned_data) return HttpResponse("Done") return render(request, "web/htmx_contact_form_confirm.html") else: form = AddCollectionToCaseForm(user=request.user) return render( request, "generic_form.html", { "form": form, "target_url": reverse("atlas:case_collection_form", args=(case_id,)), }, ) # class AddCollectionToCaseView(FormView): # form_class = AddCollectionToCaseForm # template_name = "generic_form.html" # # def form_valid(self, form): # form.save() # return super(AddCollectionToCaseView, self).form_valid(form) # # def get_form_kwargs(self): # kwargs = super(AddCollectionToCaseView, self).get_form_kwargs() # kwargs.update({"user": self.request.user}) # return kwargs # # def get_success_url(self) -> str: # pk = self.kwargs["pk"] # return reverse("atlas:case_detail", kwargs={"pk": pk}) class AtlasClone(AtlasCreateBase, AuthorOrCheckerRequiredMixin, 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"]) if ( self.request.user.groups.filter(name="atlas_editor").exists() or self.request.user.is_superuser ): pass else: if self.request.user not in old_object.get_author_objects(): raise PermissionDenied() # or Http404 else: pass 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.accepts("application/json")(): 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 QuestionSchemaView(LoginRequiredMixin, SingleTableMixin, FilterView): model = QuestionSchema table_class = QuestionSchemaTable template_name = "atlas/view.html" filterset_class = QuestionSchemaFilter 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 def case_order_dicom(request, pk): if request.htmx: case = get_object_or_404(Case, pk=pk) fail = [] # TODO: don't try to order non stack series for series in case.series.all(): try: if request.user in series.get_author_objects(): series.order_by_dicom() else: fail.append(series.pk) except: fail.append(series.pk) if fail: return HttpResponse( f"Unable to order series {','.join([str(i) for i in fail])}" ) else: return HttpResponse("Series reordered") raise Http404 @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 def image_diff(request): if request.htmx: image_ids = request.POST.getlist("image-id") if len(image_ids) == 2: first: SeriesImage = get_object_or_404(SeriesImage, pk=image_ids[0]) second: SeriesImage = get_object_or_404(SeriesImage, pk=image_ids[1]) first_ds = pydicom.read_file(first.image.file) # del first_ds.PixelData # j1 = first_ds.to_json_dict() f = print_dicom(first_ds, join=False) second_ds = pydicom.read_file(second.image.file) # del second_ds.PixelData # j2 = second_ds.to_json_dict() s = print_dicom(second_ds, join=False) diff = difflib.HtmlDiff().make_table(f, s) dict_diff = pprint.pformat(compare_dicom_datasets(first_ds, second_ds)) # return HttpResponse(diff) return HttpResponse(f"
{dict_diff}

{diff}") else: ds_set = [] for pk in image_ids: series_image = get_object_or_404(SeriesImage, pk=pk) ds_set.append(pydicom.read_file(series_image.image.file)) dict_diff = pprint.pformat(compare_dicom_datasets(*ds_set)) return HttpResponse(f"
{dict_diff}
") return HttpResponse("Fail") @login_required def series_image_dicom(request, pk): image = get_object_or_404(SeriesImage, pk=pk) return render(request, "atlas/series_image_dicom.html", {"image": image}) @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) # Vendored in from BaseZipView class SeriesImagesZipView(SeriesImagesZipViewBase, SuperuserRequiredMixin): series_object = Series # At the moment only the series owner / author can delete findings but anyone can create 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")) condition_ids = json.loads(request.POST.get("conditions")) description = request.POST.get("description") annotation_json = request.POST.get("annotation_json") viewport_json = request.POST.get("viewport_json") current_image_id_index = request.POST.get("current_image_id_index") series = Series.objects.get(pk=series_id) findings = Finding.objects.filter(pk__in=findings_ids) structures = Structure.objects.filter(pk__in=structure_ids) conditions = Condition.objects.filter(pk__in=condition_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.conditions.set(conditions) sf.current_image_id_index = current_image_id_index 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 ConditionAutocompleteRCRCurriculum(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.filter(rcr_curriculum=True) if self.q: # This raises a fielderror which breaks creating a new item if not caught try: qs = qs.filter(name__icontains=self.q, rcr_curriculum=True) 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_viva(request, pk): collection = get_object_or_404(CaseCollection, pk=pk) cases = ( collection.cases.all() .order_by("casedetail__sort_order") .prefetch_related( "series", "series__images", "series__contrast", "series__plane", "series__examination", ) ) return render( request, "atlas/collection_viva.html", {"collection": collection, "cases": cases, "can_edit": True}, ) @user_is_collection_author_or_atlas_editor def collection_viva_case(request, pk, case_number): collection = get_object_or_404(CaseCollection, pk=pk) # cases = collection.cases.all().order_by("casedetail__sort_order") current_case = collection.get_case_by_index(case_number) return render( request, "atlas/collection_viva_case.html", {"collection": collection, "current_case": current_case}, ) @user_is_collection_author_or_atlas_editor def collection_viva_series(request, pk, series_id): collection = get_object_or_404(CaseCollection, pk=pk) series = get_object_or_404(Series, pk=series_id) return render( request, "atlas/collection_viva_series.html", {"collection": collection, "current_series": series}, ) @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") casedetails = CaseDetail.objects.filter(collection=collection).order_by( "sort_order" ) return render( request, "atlas/collection_detail.html", {"collection": collection, "casesdetails": casedetails, "can_edit": True}, ) @user_is_collection_author_or_atlas_editor def collection_history(request, exam_id: int): """An overview page that shows who has attempted / finised the collection""" collection = get_object_or_404(CaseCollection, pk=exam_id) casedetails = CaseDetail.objects.filter(collection=collection).order_by( "sort_order" ) userexams = collection.get_cid_user_exams() print(userexams) return render( request, "atlas/collection_history.html", { "collection": collection, "casesdetails": casedetails, "can_edit": True, "userexams": userexams, }, ) @user_is_collection_author_or_atlas_editor def collection_history_user(request, exam_id: int, user_pk: int): collection = get_object_or_404(CaseCollection, pk=exam_id) user = get_object_or_404(User, pk=user_pk) casedetails = CaseDetail.objects.filter( collection=collection, ).order_by("sort_order") print(user) print(casedetails) user_answers = [(casedetail, casedetail.get_user_answers(user)) for casedetail in casedetails] return render( request, "atlas/collection_history_user.html", { "collection": collection, "user": user, "casedetails": casedetails, "user_answers": user_answers, }, ) def collection_take_start(request, pk): """The starting page for taking / reviewing a case collection Args: request (_type_): _description_ pk (_type_): _description_ Returns: _type_: _description_ """ collection = get_object_or_404(CaseCollection, pk=pk) valid_user = (collection.check_logged_in_user(request),) cid_exam = None if collection.collection_type == "REV" or valid_user: cid_exam = collection.get_cid_user_exams(user_user=request.user).first() template_variables = { "collection": collection, "valid_user": valid_user, "cid_exam": cid_exam, } match collection.collection_type: case x if x in ("REP", "QUE"): return render( request, "atlas/collection_take_start.html", template_variables ) case "REV": return render( request, "atlas/collection_review_start.html", template_variables ) @user_is_collection_author_or_atlas_editor def collection_case_priors(request, exam_id, case_id): case_detail = CaseDetail.objects.get(case=case_id, collection=exam_id) if request.htmx: if "remove" in request.POST: p = CasePrior.objects.get( case_detail=case_detail, prior_case=request.POST["remove"] ) p.delete() return HttpResponse(f"Case removed") elif "prior_case_id" in request.POST: prior_case = Case.objects.get(pk=request.POST["prior_case_id"]) p, created = CasePrior.objects.get_or_create( case_detail=case_detail, prior_case=prior_case ) p.relation_text = request.POST["relation"] if not p.relation_text: return HttpResponse( "You need to enter text to describe the relationship between the cases" ) p.save() return HttpResponse(f"Case added") else: return HttpResponse("False") raise Http404() collection = case_detail.collection added_priors = CasePrior.objects.filter(case_detail=case_detail) added_relation_map = {} for prior in added_priors: added_relation_map[prior.prior_case] = prior.relation_text case = case_detail.case available_cases = case.get_all_prior_cases() available_priors = [] for case in available_cases: if case in added_relation_map: available_priors.append((case, True, added_relation_map[case])) else: available_priors.append((case, False, "")) form = PriorCaseForm() case_number, case_count = collection.get_index_of_case( case_detail.case, case_count=True ) previous = collection.get_previous_case(case_detail.case) next = collection.get_next_case(case_detail.case) return render( request, "atlas/collection_case_priors.html", { "case_detail": case_detail, "form": form, # "example_form": example_form, "collection": collection, "case": case_detail.case, "previous": previous, "next": next, "collection_length": case_count, "case_number": case_number, "added_priors": added_priors, "available_priors": available_priors, }, ) @user_is_collection_author_or_atlas_editor def collection_case_details(request, exam_id, case_id): case_detail = CaseDetail.objects.get(case=case_id, collection=exam_id) collection = case_detail.collection if not collection.collection_type == "QUE": raise Http404("Collection not in question mode") if request.method == "POST": # Called if the user saves the correct answer form if request.POST.get("submit") == "answer": # answers = request.POST case_detail.question_answers = json.loads(request.POST.get("json_answer")) case_detail.save() example_form = JsonAnswerForm( request.POST, question_schema=case_detail.question_schema ) form = CaseDetailForm(instance=case_detail) # Called if the user saves the main form elif request.POST.get("submit") == "save": form = CaseDetailForm(request.POST, instance=case_detail) if form.is_valid(): form.save() # Add any additional logic or redirection here # example_form = JsonAnswerForm(request.POST, question_schema=case_detail.question_schema) post_data = request.POST.copy() if ( case_detail.question_schema is not None and case_detail.question_answers is not None ): question_keys = set(case_detail.question_schema["properties"].keys()) answer_keys = set(case_detail.question_answers.keys()) answers = {} for key, value in case_detail.question_answers.items(): if key in question_keys: answers[key] = value diff = question_keys - answer_keys for key in diff: answers[key] = "" post_data["json_answer"] = json.dumps(answers) case_detail.question_answers = answers case_detail.save() example_form = JsonAnswerForm( post_data, question_schema=case_detail.question_schema ) form = CaseDetailForm(instance=case_detail) # This shouldn't happen else: assert False form = CaseDetailForm(request.POST, instance=case_detail) else: post_data = request.POST.copy() post_data["json_answer"] = json.dumps(case_detail.question_answers) example_form = JsonAnswerForm( post_data, question_schema=case_detail.question_schema ) form = CaseDetailForm(instance=case_detail) pass # post_data = None # if case_detail.question_schema is not None and case_detail.question_answers is not None: # question_keys = set(case_detail.question_schema["properties"].keys()) # answer_keys = set(case_detail.question_answers.keys()) # answers = {} # for key, value in case_detail.question_answers.items(): # if key in question_keys: # answers[key] = value # diff = question_keys - answer_keys # for key in diff: # answers[key] = "" # post_data = request.POST.copy() # post_data["json_answer"]= json.dumps(answers) # case_detail.question_answers = answers # example_form = JsonAnswerForm(post_data, question_schema=case_detail.question_schema) # form = CaseDetailForm(instance=case_detail) # blank_form = JsonAnswerForm(question_schema=case_detail.question_schema) case_number, case_count = collection.get_index_of_case( case_detail.case, case_count=True ) previous = collection.get_previous_case(case_detail.case) next = collection.get_next_case(case_detail.case) return render( request, "atlas/collection_case_detail.html", { "case_detail": case_detail, "form": form, "example_form": example_form, "collection": collection, "case": case_detail.case, "previous": previous, "next": next, "collection_length": case_count, "case_number": case_number, }, ) @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") case, case_count = collection.get_case_by_index(case_number, case_count=True) previous = case_number > 0 next = case_number < (case_count - 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 exam_cids(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_cid_users.all() cid_user_count = cid_users.count() return render( request, "atlas/exam_cids.html", { "collection": collection, "cid_users": cid_users, "cid_user_count": cid_user_count, }, ) raise PermissionDenied def collection_take_overview_user(request, pk): return collection_take_overview(request, pk) def collection_take_overview( request, pk: int, cid: int | None = None, passcode: str | None = None ): """ Summary page that shows which questions a user has attempted (with / without answers) Args: request (_type_): _description_ pk (int): _description_ cid (int | None, optional): _description_. Defaults to None. passcode (str | None, optional): _description_. Defaults to None. Returns: _type_: _description_ """ collection = get_object_or_404(CaseCollection, pk=pk) collection.check_user_can_take(cid, passcode, request.user) cid_user_exam = collection.get_or_create_cid_user_exam( cid=cid, user_user=request.user ) if request.htmx: if "finish" in request.POST: cid_user_exam.completed = True cid_user_exam.save() collection.exam_user_status.create( cid_user_exam=cid_user_exam, status="submitted", extra="manual submission", ) return HttpResponse("True") else: raise Http404() cases = collection.cases.all().order_by("casedetail__sort_order").prefetch_related() case_details = CaseDetail.objects.filter(case__in=cases, collection=collection) if cid is not None: answers = CidReportAnswer.objects.filter(cid=cid, question__in=case_details) else: answers = UserReportAnswer.objects.filter( user=request.user, question__in=case_details ) answer_question_map = {} for ans in answers: answer_question_map[ans.question] = ans question_answer_tuples = [] answer_count = 0 for cd in case_details: self_review = cid_user_exam.selfreview_set.filter(case=cd.case) if cd in answer_question_map and ( answer_question_map[cd].answer or answer_question_map[cd].json_answer ): question_answer_tuples.append((cd, answer_question_map[cd], self_review)) answer_count += 1 else: question_answer_tuples.append((cd, None, self_review)) 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_user_answers(request, pk: int, case_number: int): return collection_case_view_take_answers(request, pk, case_number) def collection_case_view_take_answers( request, pk: int, case_number: int, cid=None, passcode=None ): """ """ collection = get_object_or_404(CaseCollection, pk=pk) collection.check_user_can_take(cid, passcode, request.user) case, case_count = collection.get_case_by_index(case_number, case_count=True) cid_user_exam = collection.get_or_create_cid_user_exam( cid=cid, user_user=request.user ) case_detail = CaseDetail.objects.get(case=case, collection=collection) return render( request, "atlas/collection_case_view_take_answers.html", { "collection": collection, "case": case, "case_detail": case_detail, }, ) def collection_case_view_take_user(request, pk: int, case_number: int): return collection_case_view_take(request, pk, case_number) def collection_case_view_take( request, pk: int, case_number: int, cid=None, passcode=None ): """ Displays the page from which the user takes a case in the collection Args: request (_type_): _description_ pk (int): _description_ case_number (int): _description_ cid (_type_, optional): _description_. Defaults to None. passcode (_type_, optional): _description_. Defaults to None. Raises: Http404: If user does not have access Returns: _type_: _description_ """ collection = get_object_or_404(CaseCollection, pk=pk) form = None answer: None | CidReportAnswer | UserReportAnswer = None collection.check_user_can_take(cid, passcode, request.user) case, case_count = collection.get_case_by_index(case_number, case_count=True) cid_user_exam = collection.get_or_create_cid_user_exam( cid=cid, user_user=request.user ) if cid_user_exam.start_time is None: cid_user_exam.start_time = timezone.now() cid_user_exam.save() case_detail = CaseDetail.objects.get(case=case, collection=collection) if not collection.review_only(): if collection.collection_type == "REP": if cid is not None: answer = case_detail.cidreportanswer_set.filter(cid=cid).first() ReportAnswerForm = CidReportAnswerForm else: answer = case_detail.userreportanswer_set.filter( user=request.user ).first() ReportAnswerForm = UserReportAnswerForm elif collection.collection_type == "QUE": if cid is not None: answer = case_detail.cidreportanswer_set.filter(cid=cid).first() ReportAnswerForm = CidQuestionAnswerForm else: answer = case_detail.userreportanswer_set.filter( user=request.user ).first() ReportAnswerForm = UserQuestionAnswerForm if request.method == "POST": if collection.collection_type in ("REP", "QUE"): if not collection.publish_results: if answer: form = ReportAnswerForm( request.POST, instance=answer, case_detail=case_detail ) else: form = ReportAnswerForm(request.POST, case_detail=case_detail) if not cid_user_exam.completed and not ( answer is not None and answer.completed ): if form.is_valid(): answer = form.save(commit=False) answer.set_cid_or_user(cid=cid, user=request.user) answer.question = case_detail # answer.published_date = timezone.now() if "complete_case" in request.POST: if not collection.self_review: raise Http404("Self review not enabled") answer.completed = True answer.save() cid_user_exam.end_time = timezone.now() cid_user_exam.save() if cid is not None: kwargs = {"pk": pk, "cid": cid, "passcode": passcode} redirect_url = "atlas:collection_case_view_take" overview_url = "atlas:collection_take_overview" else: kwargs = {"pk": pk} redirect_url = "atlas:collection_case_view_take_user" overview_url = "atlas:collection_take_overview_user" if "next" in request.POST: return redirect(redirect_url, case_number=case_number + 1, **kwargs) elif "previous" in request.POST: return redirect(redirect_url, case_number=case_number - 1, **kwargs) elif "finish" in request.POST: return redirect(overview_url, **kwargs) elif "goto" in request.POST: return redirect( redirect_url, case_number=int(request.POST.get("goto")), **kwargs, ) else: if collection.collection_type in ("REP", "QUE"): form = ReportAnswerForm(instance=answer, case_detail=case_detail) series_list = case.series.all().prefetch_related("images", "examination", "plane") prior_cases = case_detail.caseprior_set.all() series_to_load = [] for series in series_list: series_to_load.append((series, False, "")) for prior in prior_cases: for series in prior.prior_case.get_series(): series_to_load.append((series, True, prior.relation_text)) previous = case_number > 0 next = case_number < (case_count - 1) # Set what details a user is / is not able to view when taking the case # this can be configured via the collection edit menu if ( collection.publish_results or cid_user_exam.completed or ( not collection.feedback_once_collection_complete and answer is not None and answer.completed ) ): question_completed = True 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 self_review = cid_user_exam.selfreview_set.filter(case=case) resources = case.caseresource_set.all() else: question_completed = False 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 self_review = [] resources = case.caseresource_set.filter(pre_review=True) return render( request, "atlas/collection_case_view_take.html", { "form": form, "collection": collection, "case": case, "series_list": series_list, "series_to_load": series_to_load, "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, "resources": resources, "cid_user_exam": cid_user_exam, "question_completed": question_completed, "self_review": self_review, }, ) @user_is_collection_author_or_atlas_editor def collection_case_view(request, pk, case_number): """Edit/management overview of the case (as part of the collection) Args: request (_type_): _description_ pk (_type_): _description_ case_number (_type_): _description_ Raises: Http404: _description_ Returns: _type_: _description_ """ collection = get_object_or_404(CaseCollection, pk=pk) form = None answer = None case, case_count = collection.get_case_by_index(case_number, case_count=True) series_list = case.series.all().prefetch_related("images", "examination", "plane") previous = case_number > 0 next = case_number < (case_count - 1) return render( request, "atlas/case_detail.html", { "form": form, "collection": collection, "collection_length": case_count, "case": case, "series_list": series_list, "case_number": case_number, "previous": previous, "next": next, "answer": answer, }, ) def collection_dicom_json(request, pk): collection = get_object_or_404(CaseCollection, pk=pk) return JsonResponse(collection.get_ohif_dicom_json()) @user_is_author_or_atlas_editor def use_dates_as_descriptions(request, pk): case = get_object_or_404(Case, pk=pk) series: Series for series in case.series.all(): series.use_date_as_description() return HttpResponse("Done") def collection_case_dicom_json(request, exam_id, case_id): case_detail = CaseDetail.objects.get(case=case_id, collection=exam_id) priors = case_detail.caseprior_set.all() return JsonResponse(case_detail.case.get_case_dicom_json(priors=priors)) def case_dicom_json(request, pk): case = get_object_or_404(Case, pk=pk) return JsonResponse(case.get_case_dicom_json()) def series_dicom_json(request, pk): series = get_object_or_404(Series, pk=pk) return JsonResponse(series.get_ohif_dicom_json()) @user_is_collection_author_or_atlas_editor def delete_collection_cid_answers(request, exam_id, cid): collection = get_object_or_404(CaseCollection, pk=exam_id) 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, cid=cid ) temp = {} for ans in cid_user_answers: temp[ans.pk] = (ans.question.pk, ans.score, ans.cid, ans.answer) ans.delete() return JsonResponse(temp) @user_is_collection_author_or_atlas_editor def delete_collection_answers(request, exam_id): collection = get_object_or_404(CaseCollection, pk=exam_id) 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) temp = {} for ans in cid_user_answers: temp[ans.pk] = (ans.question.pk, ans.score, ans.cid, ans.answer) ans.delete() return JsonResponse(temp) # 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) cids = set() case_list = list(cases) # 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" or answer_score == None: index = case_list.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" and i != None)] ) 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, "collection": 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, }, ) GenericExamViews = ExamViews( CaseCollection, Case, None, CidReportAnswer, "atlas", "casecollection" ) class CollectionCloneMixin: def get_initial(self): old_object = get_object_or_404(self.model, pk=self.kwargs["exam_id"]) initial_data = model_to_dict(old_object, exclude=["id"]) # We manually transfer the forign keys / m2m relationships questions = old_object.cases.all().values_list("id", flat=True) authors = old_object.author.all().values_list("id", flat=True) self.cases = list(questions) self.author = list(authors) return initial_data def form_valid(self, form): object = form.save() # Reapply these otherwise they get lost? object.cases.set(self.cases) object.author.set(self.author) object.save() return HttpResponseRedirect(object.get_absolute_url()) class CollectionClone(CollectionCloneMixin, CaseCollectionCreate): """Clone exam view""" class SelfReviewDelete(DeleteView): model = SelfReview template_name = "confirm_delete.html" # success_url = reverse_lazy("atlas:finding_view") def get_initial(self): user_exam_id = self.kwargs["user_exam_id"] case_id = self.kwargs["case_id"] review_id = self.kwargs["pk"] self.user_exam = get_object_or_404(CidUserExam, pk=user_exam_id) self.case = get_object_or_404(Case, pk=case_id) self.review = get_object_or_404(SelfReview, pk=review_id) return {"user_exam": user_exam_id, "case": case_id, "review": review_id} def get_context_data(self, **kwargs): # This can also be accessed via view.**** in the template context = super(SelfReviewDelete, self).get_context_data(**kwargs) context["user_exam"] = self.user_exam context["case"] = self.case context["review"] = self.review return context def get_success_url(self) -> str: return self.user_exam.exam.get_case_take_url(self.case) class SelfReviewUpdate(UpdateView): model = SelfReview form_class = SelfReviewForm def get_initial(self): user_exam_id = self.kwargs["user_exam_id"] case_id = self.kwargs["case_id"] review_id = self.kwargs["pk"] self.user_exam = get_object_or_404(CidUserExam, pk=user_exam_id) self.case = get_object_or_404(Case, pk=case_id) self.review = get_object_or_404(SelfReview, pk=review_id) return {"user_exam": user_exam_id, "case": case_id, "review": review_id} def get_context_data(self, **kwargs): # This can also be accessed via view.**** in the template context = super(SelfReviewUpdate, self).get_context_data(**kwargs) context["user_exam"] = self.user_exam context["case"] = self.case context["review"] = self.review return context def get_success_url(self) -> str: return self.user_exam.exam.get_case_take_url(self.case) # def get_object(self): # return self.review # return # def get_initial(self): # user_exam_id = self.kwargs["user_exam_id"] # case_id = self.kwargs["case_id"] # self.user_exam = get_object_or_404(CidUserExam, pk=user_exam_id) # self.case = get_object_or_404(Case, pk=case_id) # return {"user_exam": user_exam_id, "case": case_id} class AddSelfReview(CreateView): model = SelfReview form_class = SelfReviewForm # fields = ("author", "note_type", "note") def get_form_kwargs(self): kwargs = super(AddSelfReview, self).get_form_kwargs() # update the kwargs for the form init method with yours kwargs.update(self.kwargs) # self.kwargs contains all url conf params # kwargs.update({"user": self.request.user}) return kwargs def get_initial(self): user_exam_id = self.kwargs["user_exam_id"] case_id = self.kwargs["case_id"] self.user_exam = get_object_or_404(CidUserExam, pk=user_exam_id) self.case = get_object_or_404(Case, pk=case_id) return {"user_exam": user_exam_id, "case": case_id} def get_context_data(self, **kwargs): # This can also be accessed via view.**** in the template context = super(AddSelfReview, self).get_context_data(**kwargs) context["user_exam"] = self.user_exam context["case"] = self.case return context def get_success_url(self) -> str: return self.user_exam.exam.get_case_take_url(self.case) # return reverse("atlas:collection_take_overview_user", kwargs={"pk": self.user_exam.collection.id }) # def form_valid(self, form): # model = form.save(commit=False) # # model.save() # # response = super().form_valid(form) # # return response @login_required def uncategorised_dicoms(request): dicoms = UncategorisedDicom.objects.filter(user=request.user) data = [] for d in dicoms: data.append((d.image.name, d.get_basic_dicom_tags())) return JsonResponse(data, safe=False) @login_required def combine_series(request): """Combines a list of series into the first series in the list NOTE: this will only migrate images it will not migrate other models such as SeriesFinding or other details """ series_ids = request.POST.getlist("series-ids") # series = get_object_or_404(Series, pk=series_id) if len(series_ids) < 2: return HttpResponse("Not enough series to combine") base_series = None series_to_add = [] for n, series_id in enumerate(series_ids): series = get_object_or_404(Series, pk=series_id) if not series.check_user_can_edit(request.user): return HttpResponse("You do not have permission to edit one of the series") if n == 0: base_series = series else: series_to_add.append(series) for series in series_to_add: for image in series.images.all(): image.series = base_series image.save() series.delete() return HttpResponse(f"Series {series_ids} combines") # return HttpResponse("Fail") class CaseCollectionAuthorUpdate(RevisionMixin, AuthorRequiredMixin, UpdateView): model = CaseCollection form_class = CaseCollectionAuthorForm template_name = "author_form.html" def get_context_data(self, **kwargs): context = super(CaseCollectionAuthorUpdate, self).get_context_data(**kwargs) context["collection"] = context["object"] return context class ExamGroupsUpdate(ExamGroupsUpdateBase): model = CaseCollection form_class = ExamGroupsForm def get_context_data(self, **kwargs): context = super(ExamGroupsUpdate, self).get_context_data(**kwargs) context["exam"] = context["object"] return context class CaseAuthorUpdate(RevisionMixin, AuthorRequiredMixin, UpdateView): model = Case form_class = CaseAuthorForm template_name = "author_form.html" def get_context_data(self, **kwargs): context = super(CaseAuthorUpdate, self).get_context_data(**kwargs) context["collection"] = context["object"] return context class SeriesAuthorUpdate(RevisionMixin, AuthorRequiredMixin, UpdateView): model = Series form_class = SeriesAuthorForm template_name = "author_form.html" def get_context_data(self, **kwargs): context = super(SeriesAuthorUpdate, self).get_context_data(**kwargs) context["collection"] = context["object"] return context def question_schemas_preset(request): schemas = [ json.dumps( { "type": "object", "title": "Questions", "required": ["laterality", "diagnosis"], "properties": { "test": { "type": "string", "title": "test string", "description": "What side is the lesion on?", }, "diagnosis": { "enum": [ "Meningioma", "Schwannoma", "Neurofibroma", "Ependymoma", ], "type": "string", "title": "Diagnosis", "description": "What is the diagnosis?", }, "laterality": { "enum": ["left", "right"], "type": "string", "title": "Laterality", "widget": "radio", "description": "What side is the lesion on?", }, "laterality (copy 2)": { "enum": ["left", "right"], "type": "string", "title": "Lateralityaoue", "widget": "radio", "description": "What side is the lesion on?", }, }, } ), ] return render( request, "atlas/question_schemas_preset.html", { "schemas": schemas, }, ) def collection_question_schemas(request, exam_id: int): collection = get_object_or_404(CaseCollection, pk=exam_id) questions = collection.cases.all().prefetch_related() case_details = ( CaseDetail.objects.filter(case__in=questions, collection=collection) .prefetch_related("case") .order_by("sort_order") ) return render( request, "atlas/collection_question_schemas.html", { "collection": collection, "case_details": case_details, }, ) def collection_reset_answers(request, exam_id: int): if request.htmx: collection = get_object_or_404(CaseCollection, pk=exam_id) # Select all case details (answers are linked to these) case_details = collection.casedetail_set.all().prefetch_related() # Delete all answers for case in case_details: cid_answers = case.cidreportanswer_set.all() cid_answers.delete() user_answers = case.userreportanswer_set.all() user_answers.delete() # User statuses collection.exam_user_status.all().delete() # CidUserExams collection.cid_users.all().delete() return HttpResponse("Success") else: raise Http404("Invalid request")