2733 lines
86 KiB
Python
Executable File
2733 lines
86 KiB
Python
Executable File
import json
|
|
import pprint
|
|
from dal import autocomplete
|
|
from django.shortcuts import render, get_object_or_404, redirect
|
|
from django import forms
|
|
from django.utils import timezone
|
|
|
|
# from django.contrib.auth.models import User
|
|
from django.contrib.auth.decorators import login_required, user_passes_test
|
|
from django.contrib.auth.models import User
|
|
from django.core.exceptions import (
|
|
FieldError,
|
|
ObjectDoesNotExist,
|
|
PermissionDenied,
|
|
ViewDoesNotExist,
|
|
)
|
|
|
|
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
|
|
from django.views.generic 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,
|
|
FindingForm,
|
|
JsonAnswerForm,
|
|
QuestionSchemaForm,
|
|
ResourceForm,
|
|
SelfReviewForm,
|
|
SeriesAuthorForm,
|
|
SeriesForm,
|
|
SeriesImageFormSet,
|
|
SeriesFormSet,
|
|
SeriesFindingForm,
|
|
CaseDifferentialFormSet,
|
|
StructureForm,
|
|
UserQuestionAnswerForm,
|
|
UserReportAnswerForm,
|
|
)
|
|
from .models import (
|
|
Case,
|
|
CaseCollection,
|
|
CaseDetail,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
def series_detail(request, pk, finding_pk=None):
|
|
series = get_object_or_404(Series, pk=pk)
|
|
|
|
editing_finding = -1
|
|
if finding_pk is not None:
|
|
finding = get_object_or_404(SeriesFinding, pk=finding_pk)
|
|
series_finding_form = SeriesFindingForm(None, instance=finding)
|
|
editing_finding = finding.pk
|
|
else:
|
|
series_finding_form = SeriesFindingForm(series_id=series.id)
|
|
|
|
# logging.debug(atlas.subspecialty.first().name.all())
|
|
return render(
|
|
request,
|
|
"atlas/series.html",
|
|
{
|
|
"series": series,
|
|
"series_finding_form": series_finding_form,
|
|
"editing_finding": editing_finding,
|
|
},
|
|
)
|
|
|
|
|
|
@login_required
|
|
# @user_is_atlas_editor
|
|
def 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"
|
|
|
|
|
|
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, 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):
|
|
# save exam orders (there must be a better way to do this)
|
|
# exam_orders = {}
|
|
# for exam in self.object.exams.all():
|
|
# exam_orders[exam] = list(exam.exam_questions.all())
|
|
|
|
self.object = form.save(commit=False)
|
|
self.object.save()
|
|
|
|
form.instance.author.add(self.request.user.id)
|
|
|
|
context = self.get_context_data(form=form)
|
|
case_formset = context["case_formset"]
|
|
# logger.debug(formset.is_valid())
|
|
if case_formset.is_valid():
|
|
response = super().form_valid(form)
|
|
case_formset.instance = self.object
|
|
case_formset.save()
|
|
|
|
# restore exam orders
|
|
# for exam in self.object.exams.all():
|
|
# if exam in exam_orders and self.object in exam_orders[exam]:
|
|
# print(exam_orders[exam])
|
|
# exam.exam_questions.set(exam_orders[exam])
|
|
# exam.save()
|
|
|
|
return response
|
|
else:
|
|
return super().form_invalid(form)
|
|
|
|
|
|
class SeriesUpdate(
|
|
RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView
|
|
):
|
|
model = Series
|
|
form_class = SeriesForm
|
|
|
|
# fields = '__all__'
|
|
# #fields = [ 'condition' ]
|
|
# #initial = {'date_of_death': '05/01/2018'}
|
|
# exclude = [ 'created_date', 'published_date' ]
|
|
def get_context_data(self, **kwargs):
|
|
context = super(SeriesUpdate, self).get_context_data(**kwargs)
|
|
if self.request.POST:
|
|
context["image_formset"] = SeriesImageFormSet(
|
|
self.request.POST, self.request.FILES, instance=self.object
|
|
)
|
|
|
|
context["image_formset"].full_clean()
|
|
else:
|
|
context["image_formset"] = SeriesImageFormSet(instance=self.object)
|
|
return context
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super(SeriesUpdate, self).get_form_kwargs()
|
|
kwargs.update({"user": self.request.user})
|
|
return kwargs
|
|
|
|
def form_valid(self, form):
|
|
self.object = form.save(commit=False)
|
|
self.object.save()
|
|
|
|
form.instance.author.add(self.request.user.id)
|
|
|
|
context = self.get_context_data(form=form)
|
|
image_formset = context["image_formset"]
|
|
# logger.debug(formset.is_valid())
|
|
if image_formset.is_valid():
|
|
response = super().form_valid(form)
|
|
image_formset.instance = self.object
|
|
image_formset.save()
|
|
return response
|
|
else:
|
|
return super().form_invalid(form)
|
|
|
|
|
|
class 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('<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.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("<h1>Series does not appear to contain dicoms</h1>")
|
|
|
|
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"<pre>{dict_diff}</pre><br/>{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"<pre>{dict_diff}</pre>")
|
|
|
|
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(
|
|
"<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)
|
|
|
|
|
|
# Vendored in from BaseZipView
|
|
class SeriesImagesZipView(SeriesImagesZipViewBase, SuperuserRequiredMixin):
|
|
series_object = Series
|
|
|
|
|
|
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")
|
|
|
|
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.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},
|
|
)
|
|
|
|
|
|
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_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()
|
|
|
|
if cid is not None:
|
|
c = CidUser.objects.get(cid=cid)
|
|
collection.exam_user_status.create(
|
|
cid_user=c, user_user=None, status="submitted", extra="manual submission"
|
|
)
|
|
else:
|
|
collection.exam_user_status.create(
|
|
cid_user=None, user_user=request.user, 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:
|
|
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()
|
|
|
|
if not collection.review_only():
|
|
case_detail = CaseDetail.objects.get(case=case, collection=collection)
|
|
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")
|
|
|
|
print("case_no")
|
|
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,
|
|
"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 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)
|
|
|
|
# questions = exam.exam_questions.all().prefetch_related("answers")
|
|
|
|
# cid_user_answers = CidReportAnswer.objects.select_related("question").filter(
|
|
# question__in=questions, exam__id=pk
|
|
# )
|
|
|
|
cids = set()
|
|
|
|
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 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,
|
|
},
|
|
)
|