Files
penracourses/atlas/views.py
T
Ross 2f2b3a4ef3 .
2021-12-07 15:23:24 +00:00

894 lines
26 KiB
Python
Executable File

import json
from dal import autocomplete
from django.shortcuts import render, get_object_or_404, redirect
from django import forms
# from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required, user_passes_test
from django.contrib.auth.models import User
from django.core.exceptions import (
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 .forms import (
CaseForm,
ConditionForm,
FindingForm,
SeriesForm,
SeriesImageFormSet,
SeriesFormSet,
SeriesFindingForm,
CaseDifferentialFormSet,
StructureForm,
)
from .models import (
Case,
Condition,
PathologicalProcess,
Presentation,
Series,
Examination,
Finding,
Structure,
Subspecialty,
SeriesFinding,
SeriesImage,
)
from .tables import (
CaseTable,
ConditionTable,
FindingTable,
PathologicalProcessTable,
PresentationTable,
SeriesTable,
StructureTable,
SubspecialtyTable,
)
from .filters import (
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_checker,
user_is_author_or_atlas_checker_or_atlas_marker,
user_is_author_or_atlas_series_checker_or_atlas_marker,
user_is_atlas_checker,
user_is_author_or_atlas_series_checker,
user_is_atlas_marker,
user_is_author_or_atlas_checker_or_atlas_marker,
)
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_checker").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_checker_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_checker
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_checker
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_checker
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_checker
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_checker
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_checker
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_checker
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_checker
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 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")
@login_required
@user_is_author_or_atlas_checker
def case_clone(request, pk):
new_item = get_object_or_404(Case, pk=pk)
new_item.pk = None # autogen a new pk (item_id)
# new_item.name = "Copy of " + new_item.name #need to change uniques
form = CaseForm(request.POST or None, instance=new_item)
series_formset = SeriesFormSet()
if form.is_valid():
form.instance.author.add(request.user.id)
# logger.debug(formset.is_valid())
if series_formset.is_valid():
response = super().form_valid(form)
series_formset.instance = obj
series_formset.save()
return response
else:
return super().form_invalid(form)
context = {
"form": form,
"series_formset": series_formset,
# other context
}
return render(request, "atlas/case_form.html", context)
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_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 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 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
)
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()
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
)
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)
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_checker
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('<script>opener.closePopup(window, "%s", "%s", "#id_abnormality");</script>' % (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("<h1>Series does not appear to contain dicoms</h1>")
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(
"<h1>Series does not appear to contain dicoms (or field InstanceNumber)</h1>"
)
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(
"<h1>Series does not appear to contain dicoms (or field SeriesInstanceUID)</h1>"
)
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_checker
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,
# },
)