start atlas app

This commit is contained in:
Ross
2021-11-23 19:25:26 +00:00
parent 732bd7892c
commit 92c52896d0
13 changed files with 1576 additions and 0 deletions
View File
Executable
+58
View File
@@ -0,0 +1,58 @@
from django.contrib import admin
from .models import Case, Series, SeriesImage
import tagulous.admin
from django.forms import ModelForm
from reversion.admin import VersionAdmin
from django.forms.widgets import RadioSelect
from tinymce.widgets import TinyMCE
# from generic.models import Examination, Sign, Site, Condition
from generic.models import Examination, Sign, Condition
# Register your models here.
# admin.site.register(Examination)
# admin.site.register(Site)
admin.site.register(SeriesImage)
class AtlasAdminForm(ModelForm):
class Meta:
model = Case
fields = [
"description",
"history",
"sign",
"findings",
"author",
"series",
]
widgets = {
# "normal": RadioSelect(
# choices=[(True, 'Yes'),
# (False, 'No')])
"findings" : TinyMCE(attrs={'cols': 80, 'rows': 30}),
"mark_scheme" : TinyMCE(attrs={'cols': 80, 'rows': 30}),
}
class AtlasAdmin(VersionAdmin):
form = AtlasAdminForm
save_on_top = True
save_as = True
view_on_site = True
admin.site.register(Case, AtlasAdmin)
class SeriesAdmin(VersionAdmin):
pass
admin.site.register(Series, SeriesAdmin)
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class AtlasConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'atlas'
+66
View File
@@ -0,0 +1,66 @@
from django.core.exceptions import PermissionDenied
from .models import Atlas, Series
def user_is_author_or_atlas_series_checker_or_atlas_marker(function):
def wrap(request, *args, **kwargs):
series = Series.objects.get(pk=kwargs['pk'])
if request.user in series.get_author_objects() or request.user.groups.filter(name='atlas_checker').exists() or request.user.groups.filter(name='atlas_marker').exists() or request.user.is_superuser:
return function(request, *args, **kwargs)
else:
raise PermissionDenied
wrap.__doc__ = function.__doc__
wrap.__name__ = function.__name__
return wrap
def user_is_author_or_atlas_series_checker(function):
def wrap(request, *args, **kwargs):
series = Series.objects.get(pk=kwargs['pk'])
if request.user in series.get_author_objects() or request.user.groups.filter(name='atlas_checker').exists() or request.user.is_superuser:
return function(request, *args, **kwargs)
else:
raise PermissionDenied
wrap.__doc__ = function.__doc__
wrap.__name__ = function.__name__
return wrap
def user_is_author_or_atlas_checker_or_atlas_marker(function):
def wrap(request, *args, **kwargs):
atlas = Atlas.objects.get(pk=kwargs['pk'])
if request.user in atlas.author.all() or request.user.groups.filter(name='atlas_checker').exists() or request.user.groups.filter(name='atlas_marker').exists() or request.user.is_superuser:
return function(request, *args, **kwargs)
else:
raise PermissionDenied
wrap.__doc__ = function.__doc__
wrap.__name__ = function.__name__
return wrap
def user_is_author_or_atlas_checker(function):
def wrap(request, *args, **kwargs):
atlas = Atlas.objects.get(pk=kwargs['pk'])
if request.user in atlas.author.all() or request.user.groups.filter(name='atlas_checker').exists() or request.user.is_superuser:
return function(request, *args, **kwargs)
else:
raise PermissionDenied
wrap.__doc__ = function.__doc__
wrap.__name__ = function.__name__
return wrap
def user_is_atlas_checker(function):
def wrap(request, *args, **kwargs):
if request.user.groups.filter(name='atlas_checker').exists() or request.user.is_superuser:
return function(request, *args, **kwargs)
else:
raise PermissionDenied
wrap.__doc__ = function.__doc__
wrap.__name__ = function.__name__
return wrap
def user_is_atlas_marker(function):
def wrap(request, *args, **kwargs):
if request.user.groups.filter(name='atlas_marker').exists() or request.user.is_superuser:
return function(request, *args, **kwargs)
else:
raise PermissionDenied
wrap.__doc__ = function.__doc__
wrap.__name__ = function.__name__
return wrap
+42
View File
@@ -0,0 +1,42 @@
import django_filters
from .models import Case, Series
from django.contrib.auth.models import User
def get_authors(request):
if request is None:
return User.objects.none()
if not request.user.groups.filter(name="atlas_checker").exists():
queryset = User.objects.filter(id=request.user.id)
else:
queryset = User.objects.all()
return queryset
class AtlasFilter(django_filters.FilterSet):
author = django_filters.ModelMultipleChoiceFilter(queryset=get_authors, null_label="No author")
class Meta:
model = Case
fields = (#"site",
"created_date", "open_access", "author")
def __init__(self, data=None, queryset=None, prefix=None, strict=None, user=None, request=None):
if not request.user.groups.filter(name="atlas_checker").exists():
#queryset = queryset.filter(open_access=True) | queryset.filter(author__id=request.user.id)
queryset = queryset.filter(author__id=request.user.id)
super(AtlasFilter, self).__init__(data=data, queryset=queryset, prefix=prefix, request=request)
pass
class SeriesFilter(django_filters.FilterSet):
class Meta:
model = Series
fields = ("modality","atlas", "examination", "author")
def __init__(self, data=None, queryset=None, prefix=None, strict=None, user=None, request=None):
if not request.user.groups.filter(name="atlas_checker").exists():
#queryset = queryset.filter(open_access=True) | queryset.filter(author__id=request.user.id)
queryset = queryset.filter(author__id=request.user.id)
super(SeriesFilter, self).__init__(data=data, queryset=queryset, prefix=prefix, request=request)
pass
Executable
+160
View File
@@ -0,0 +1,160 @@
from django.forms import (
Form,
ModelForm,
ModelMultipleChoiceField,
ModelChoiceField,
ChoiceField,
CharField,
)
from django.forms import inlineformset_factory
from atlas.models import (
Case,
Series,
SeriesImage,
)
from anatomy.models import Modality
from generic.models import Examination, Condition, Sign
#from generic.models import Examination, Site, Condition, Sign
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.forms.widgets import RadioSelect, TextInput, Textarea
from tinymce.widgets import TinyMCE
class ExaminationForm(ModelForm):
class Meta:
model = Examination
fields = ["examination"]
class SeriesForm(ModelForm):
class Media:
# Django also includes a few javascript files necessary
# for the operation of this form element. You need to
# include <script src="/admin/jsi18n"></script>
# in the template.
css = {
"all": ["css/widgets.css"],
}
# Adding this javascript is crucial
js = ["jsi18n.js", "tesseract.min.js"]
def __init__(self, *args, **kwargs):
super(SeriesForm, self).__init__(*args, **kwargs)
#self.fields["examination"] = ModelChoiceField(
# queryset=Examination.objects.all(),
# widget=FilteredSelectMultiple(verbose_name="Examination", is_stacked=False),
#)
self.fields["modality"] = ModelChoiceField(
required=True,
queryset=Modality.objects.all(),
)
ModelForm.__init__(self, *args, **kwargs)
class Meta:
model = Series
exclude = ["author"]
class AtlasForm(ModelForm):
#exams = ModelMultipleChoiceField(required=False, queryset=Exam.objects.all(),widget=FilteredSelectMultiple(verbose_name="Exams", is_stacked=False))
class Media:
# Django also includes a few javascript files necessary
# for the operation of this form element. You need to
# include <script src="/admin/jsi18n"></script>
# in the template.
css = {
"all": ["css/widgets.css"],
}
# Adding this javascript is crucial
js = ["jsi18n.js", "tesseract.min.js"]
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user') # To get request.user. Do not use kwargs.pop('user', None) due to potential security hole
if kwargs.get("instance"):
# We get the 'initial' keyword argument or initialize it
# as a dict if it didn't exist.
initial = kwargs.setdefault("initial", {})
# The widget for a ModelMultipleChoiceField expects
# a list of primary key for the selected data.
initial["exams"] = [t.pk for t in kwargs["instance"].exams.all()]
ModelForm.__init__(self, *args, **kwargs)
super(AtlasForm, self).__init__(*args, **kwargs)
class Meta:
model = Case
# fields = ['due_back']
# fields = '__all__'
fields = [
#"examination",
#"site",
"description",
"history",
"findings",
"condition",
"sign",
"open_access",
]
# fields = ['question', 'findings', 'subspecialty', 'references']
widgets = {
# "normal": RadioSelect(
# choices=[(True, 'Yes'),
# (False, 'No')])
"findings" : TinyMCE(attrs={'cols': 80, 'rows': 20}),
"mark_scheme" : TinyMCE(attrs={'cols': 80, 'rows': 30}),
"description" : Textarea(attrs={'cols': 80, 'rows': 5}),
"history" : Textarea(attrs={'cols': 80, 'rows': 5}),
}
def save(self, commit=True):
# Get the unsaved Case instance
instance = ModelForm.save(self, False)
# Prepare a 'save_m2m' method for the form,
old_save_m2m = self.save_m2m
def save_m2m():
old_save_m2m()
#instance.exams.clear()
#for exam in self.cleaned_data["exams"]:
# # We need the id here
# instance.exams.add(exam.pk)
self.save_m2m = save_m2m
# Do we need to save all changes now?
# if commit:
instance.save()
self.save_m2m()
return instance
SeriesFormSet = inlineformset_factory(
Case,
Series.case.through,
exclude=[],
can_delete=True,
extra=0,
max_num=10,
field_classes="testing",
)
SeriesImageFormSet = inlineformset_factory(
Series,
SeriesImage,
exclude=[],
can_delete=True,
extra=0,
max_num=2000,
field_classes="testing",
)
View File
+378
View File
@@ -0,0 +1,378 @@
import json
from rad.settings import REMOTE_URL
from django.db.models.fields.files import ImageField
from django.db.models.fields.related import ForeignKey
from django.db import models
from django.shortcuts import get_object_or_404
from django.utils import timezone
import tagulous
import tagulous.models
from django.core.files.storage import FileSystemStorage
from django.conf import settings
from django.utils.html import format_html
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from django.utils.html import mark_safe
from django.core.exceptions import ValidationError
from sortedm2m.fields import SortedManyToManyField
import string
from collections import defaultdict
from helpers.images import image_as_base64, pretty_print_dicom
from anatomy.models import Modality
from generic.models import Examination, Condition, Sign, ExamBase, Plane, Contrast, QuestionNote
# from generic.models import Examination, Site, Condition, Sign
from easy_thumbnails.files import get_thumbnailer
from easy_thumbnails.exceptions import InvalidImageFormatError
import pydicom
import pydicom.errors
import datetime
from django.utils import timezone
import reversion
from django.contrib.contenttypes.fields import GenericRelation
image_storage = FileSystemStorage(
# Physical file location ROOT
location=u"{0}atlas/".format(settings.MEDIA_ROOT),
# Url for file
base_url=u"{0}atlas/".format(settings.MEDIA_URL),
)
def image_directory_path(instance, filename):
return u"atlas/picture/{0}".format(filename)
def findMiddle(input_list):
middle = float(len(input_list)) / 2
if middle % 2 != 0:
return input_list[int(middle - 0.5)]
else:
return input_list[int(middle)]
return (input_list[int(middle)], input_list[int(middle - 1)])
@reversion.register
class Case(models.Model):
class SubspecialtyChoices(models.TextChoices):
BREAST = 'BR', _('Breast')
CARDIAC = 'CA', _('Cardiac')
GASTRO = 'GI', _('Gastrointestinal and hepatobiliary')
HEADNECK = 'HN', _('Head and Neck')
MSK = 'MS', _('Musculoskeletal')
NEURO = 'NE', _('Neuroradiology')
OBSGYN = 'OG', _('Obstectric and Gynaecological')
PAED = 'PA', _('Paediatric')
URO = 'UR', _('Uroradiology')
VASC = 'VA', _('Vascular')
HAEMONC = 'HA', _('Haemotology and Oncology')
# author = models.ForeignKey('auth.User', on_delete=models.CASCADE)
# image = models.ImageField()
description = models.TextField(
blank=True,
help_text="Description of the case, for admin organisation, will not be visible when taking",
)
history = models.TextField(null=True, blank=True)
findings = models.TextField(null=True, blank=True)
subpecialty = models.CharField(max_length=2, choices=SubspecialtyChoices.choices)
condition = tagulous.models.TagField(
to=Condition,
blank=True,
help_text='Associated condition. Will allow searching / filtering and tips / hints to be displayed. Conditions with spaces must be enclosed in quotes "..."',
)
sign = tagulous.models.TagField(
to=Sign, blank=True, help_text="Radiological signs in the question"
)
verified = models.BooleanField(default=False)
created_date = models.DateTimeField(default=timezone.now)
published_date = models.DateTimeField(blank=True, null=True)
author = models.ManyToManyField(
settings.AUTH_USER_MODEL,
blank=True,
help_text="Author of question",
related_name="atlas_authored_questions",
)
scrapped = models.BooleanField(
default=False, help_text="Question has been scrapped and will not be shown"
)
open_access = models.BooleanField(
help_text="If a question should be freely available to browse", default=True
)
series = SortedManyToManyField("Series", related_name="atlas")
notes = GenericRelation(QuestionNote)
#question_file = models.FileField(upload_to=question_file_directory_path, blank=True, null=True)
def get_absolute_url(self):
return reverse("atlas:case_detail", kwargs={"pk": self.pk})
def get_authors(self):
"""Returns a comma seperated text list of authors"""
authors = ", ".join([i.username for i in self.author.all()])
return authors
def get_author_objects(self):
"""Returns a comma seperated text list of authors"""
authors = [i for i in self.author.all()]
return authors
def __str__(self):
examinations = [series.get_examination() for series in self.series.all()]
return "{}/{} : {}".format(self.pk, self.description, ", ".join(examinations))
def get_images(self, findings=False):
qs = self.images.all()
if not findings:
images = [i.image for i in qs if not i.findings_image]
else:
images = [i.image for i in qs]
return images
def get_image_urls(self):
return ",".join(
["https://www.penracourses.org.uk{}".format(i.url) for i in self.get_images()]
)
def get_image_url_array(self):
return json.dumps(["https://www.penracourses.org.uk{}".format(i.url) for i in self.get_images()])
class SeriesImage(models.Model):
image = models.FileField(upload_to=image_directory_path)
position = models.IntegerField(default=0)
upload_filename = models.CharField(max_length=255, blank=True)
series = models.ForeignKey(
"Series", related_name="images", on_delete=models.SET_NULL, null=True
)
class Meta:
ordering = ['position']
def get_dicom_info(self):
try:
info = pretty_print_dicom(pydicom.read_file(self.image))
except pydicom.errors.InvalidDicomError:
info = "File is not a dicom."
return(info)
@reversion.register
class Series(models.Model):
modality = models.ForeignKey(
Modality, related_name="series_modality", on_delete=models.SET_NULL, null=True
)
examination = models.ForeignKey(
Examination,
help_text="Name of the examination",
related_name="series_examination",
on_delete=models.SET_NULL,
null=True,
)
plane = models.ForeignKey(
Plane,
help_text="Plane of the examination",
related_name="series_plane",
on_delete=models.SET_NULL,
null=True,
blank=True,
)
contrast = models.ForeignKey(
Contrast,
help_text="MRI / CT contrast",
related_name="series_contrast",
on_delete=models.SET_NULL,
null=True,
blank=True,
)
description = models.TextField(
blank=True,
help_text="Description of stack, for admin organisation, will not be visible when taking",
)
author = models.ManyToManyField(
settings.AUTH_USER_MODEL,
blank=True,
related_name="series",
)
open_access = models.BooleanField(
help_text="If a question should be freely available to browse", default=True
)
def __str__(self):
if self.atlas:
atlas_id = ", ".format([atlas.pk for atlas in self.atlas.all()])
#atlas_id = self.atlas.pk
else:
atlas_id = "None"
return "{}/{} : {} [{}]".format(self.pk,
self.get_examination(), self.description, atlas_id
)
def get_author_objects(self):
"""Returns a comma seperated text list of authors"""
if self.author:
return self.author.all()
else:
return ["None"]
if self.atlas:
authors = [i for i in self.atlas.author.all()]
else:
authors = []
return authors
def get_author_display(self):
return ", ".join([i.username for i in self.get_author_objects()])
def get_examination(self):
"""Returns a comma seperated text list of regions"""
return str(self.examination)
def get_examination_full(self):
examination = ""
plane = ""
contrast = ""
if self.examination:
examination = self.examination
if self.plane:
plane = " {}".format(self.plane)
if self.contrast:
contrast = " {}".format(self.contrast)
return "{}{}{}".format(examination, plane, contrast)
def get_absolute_url(self):
return reverse("atlas:series_detail", kwargs={"pk": self.pk})
def get_image_urls(self, findings=False):
images = [
"https://www.penracourses.org.uk{}".format(i.image.url)
for i in self.images.all()
]
return ",".join(images)
def get_image_url_array(self, findings=False):
images = [
"https://www.penracourses.org.uk{}".format(i.image.url)
for i in self.images.all()
]
return json.dumps(images)
def get_thumbnail(self):
images = self.images.all()
if len(images) < 1:
return "No images", 0
img = findMiddle(images).image
try:
thumbnailer = get_thumbnailer(img)
thumbnail = thumbnailer["exam-list"]
except InvalidImageFormatError:
return format_html('<span title="{}">Invalid image url<span>', img), len(images)
return format_html('<img src="/media/{}" />', thumbnail), len(images)
def get_block(self):
examination = self.get_examination_full()
thumb, image_number = self.get_thumbnail()
return format_html(
"<div>{}<br/>{}<br/>Images: {}</div>", examination, thumb, image_number
)
def order_by_upload_filename(self):
images = self.images.all()
filenames = []
map = {}
for i in images:
filenames.append(i.upload_filename)
map[i.upload_filename] = i
filenames = sorted(filenames)
n = 1
for f in filenames:
i = map[f]
i.position = n
i.save()
n = n + 1
def order_by_dicom(self, field="SliceLocation"):
images = self.images.all()
files = []
for i in images:
files.append((i, pydicom.dcmread(i.image.path)))
#print("file count: {}".format(len(files)))
# skip files with no SliceLocation (eg scout views)
slices = []
map = {}
skipcount = 0
for i, f in files:
if hasattr(f, field):
slices.append(f)
#map[f.SliceLocation] = i
map[f[field].value] = i
else:
skipcount = skipcount + 1
print("skipped, no {}: {}".format(field, skipcount))
# ensure they are in the correct order
slices = sorted(slices, key=lambda s: s[field].value)
#print(slices)
n = 1
for f in slices:
i = map[f[field].value]
i.position = n
i.save()
n = n + 1
def get_total_image_size(self):
images = self.images.all()
size = 0
for i in images:
size += i.image.size
return size
+11
View File
@@ -0,0 +1,11 @@
# serializers.py
from rest_framework import serializers, permissions
from .models import Exam
class ExamSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Exam
fields = ('name', 'id', 'active', "publish_results")
permission_classes = [permissions.IsAuthenticated]
+114
View File
@@ -0,0 +1,114 @@
import django_tables2 as tables
from django_tables2.utils import A
from .models import Case, Series
from django.utils.html import format_html
from easy_thumbnails.files import get_thumbnailer
from easy_thumbnails.exceptions import InvalidImageFormatError
class AtlasImageColumn(tables.Column):
def render(self, value):
blocks =[]
for obj in value.all():
blocks.append(obj.get_block())
return format_html("<span class='multi-image-block'>{}</span>".format("".join(blocks)))
obj = value.first()
if obj is None:
return format_html("<span>No image<span>")
return obj.get_thumbnail()[0]
# image_object = obj.image
# try:
# thumbnailer = get_thumbnailer(image_object)
# return format_html('<img src="/media/{}" />', thumbnailer["exam-list"])
# except InvalidImageFormatError:
# return format_html('<span title="{}">Invalid image url<span>', image_object)
class SeriesImageColumn(tables.Column):
def render(self, value):
obj = value.first()
if obj is None:
return "None"
image_object = obj.image
try:
thumbnailer = get_thumbnailer(image_object)
return format_html('<img src="/media/{}" /><br/>[{}]', thumbnailer["exam-list"], value.count())
except InvalidImageFormatError:
return format_html('<span title="{}">Invalid image url<span><br/>[{}]', image_object, value.count())
class AtlasTable(tables.Table):
edit = tables.LinkColumn(
"atlas:atlas_update", text="Edit", args=[A("pk")], orderable=False
)
view = tables.LinkColumn(
"atlas:case_detail", text="View", args=[A("pk")], orderable=False
)
clone = tables.LinkColumn(
"atlas:case_clone", text="Clone", args=[A("pk")], orderable=False
)
delete = tables.LinkColumn(
"atlas:atlas_delete", text="Delete", args=[A("pk")], orderable=False
)
series = AtlasImageColumn("Images", orderable=False)
#series = tables.ManyToManyColumn(verbose_name="Exams")
exams = tables.ManyToManyColumn(verbose_name="Exams")
selection = tables.CheckBoxColumn(accessor="pk", orderable=False)
class Meta:
model = Case
template_name = "django_tables2/bootstrap4.html"
fields = ("description", "history", "created_date", "author")
sequence = ("view", "series", "exams")
class PopupLinkColumn(tables.Column):
def render(self, value):
return format_html("<span>test: {}<span>", value)
obj = value.first()
if obj is None:
return format_html("<span>No image<span>")
return obj.get_thumbnail()[0]
def create_link(test):
print(test)
return "Hello"
class SeriesTable(tables.Table):
edit = tables.LinkColumn(
"atlas:series_update", text="Edit", args=[A("pk")], orderable=False
)
view = tables.LinkColumn(
"atlas:series_detail", text="View", args=[A("pk")], orderable=False
)
popup = tables.Column(empty_values=(), orderable=False)
images = SeriesImageColumn("Images", orderable=False)
delete = tables.LinkColumn(
"atlas:series_delete", text="Delete", args=[A("pk")], orderable=False
)
selection = tables.CheckBoxColumn(accessor="pk", orderable=False)
class Meta:
model = Series
template_name = "django_tables2/bootstrap4.html"
fields = ("modality", "examination", "atlas", "description", "author")
sequence = ("view", "popup", "images")
def render_popup(self, value, record):
print(self)
return format_html("""<a href="#" onclick="return window.create_popup_window('/atlas/series/{}', 'Series')" >Popup</a>""", record.pk)
Executable
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
Executable
+191
View File
@@ -0,0 +1,191 @@
from django.urls import path, include
from . import views
app_name = "atlas"
urlpatterns = [
# path('', views.question_list, name='question_list'),
path("", views.AtlasExamViews.index, name="index"),
path("author/<int:pk>/", views.author_detail, name="author_detail"),
path("author/", views.author_list, name="author_list"),
path("question/", views.CaseView.as_view(), name="atlas_view"),
path("series/", views.SeriesView.as_view(), name="series_view"),
path("series/<int:pk>", views.series_detail, name="series_detail"),
path(
"series/<int:pk>/order_dicom",
views.series_order_dicom,
name="series_order_dicom",
),
path(
"series/<int:pk>/download",
views.SeriesImagesZipView.as_view(),
name="series_download",
),
path(
"series/<int:pk>/order_dicom_instance",
views.series_order_dicom_instance,
name="series_order_dicom_instance",
),
path(
"series/<int:pk>/order_dicom_SeriesInstanceUID",
views.series_order_dicom_SeriesInstanceUID,
name="series_order_dicom_SeriesInstanceUID",
),
path(
"series/<int:pk>/order_upload_filename",
views.series_order_upload_filename,
name="series_order_upload_filename",
),
path(
"series/<int:pk>/delete",
views.SeriesDelete.as_view(),
name="series_delete",
),
# path("unchecked/", views.unchecked_list, name="unchecked_list"),
# path("verified/<int:pk>/", views.verified_detail, name="verified_detail"),
path("question/<int:pk>/", views.case_detail, name="case_detail"),
path("question/<int:pk>/json", views.question_json, name="question_json"),
path(
"question/<int:pk>/json/unbased",
views.question_json_unbased,
name="question_json_unbased",
),
path(
"question/<int:pk>/json/recreate",
views.question_json_recreate,
name="question_json_recreate",
),
path("question/<int:pk>/split", views.atlas_split, name="atlas_split"),
path("question/<int:pk>/clone", views.AtlasClone.as_view(), name="case_clone"),
# path("verified/", views.verified, name="verified"),
# path("all_questions/", views.all_questions, name="all_questions"),
path("question/<int:pk>/scrap", views.atlas_scrap, name="atlas_scrap"),
path("question/<int:pk>/delete", views.AtlasDelete.as_view(), name="atlas_delete"),
path(
"exam/<int:pk>/review",
views.question_review,
name="question_review",
),
path("exam/<int:exam_id>/<int:question_number>/<int:cid>/mark", views.mark_answer, name="mark_answer"),
path("exam/<int:exam_id>/<int:question_number>/<int:cid>/mark_override", views.mark_answer_override, name="mark_answer_override"),
path("exam/<int:exam_id>/<int:sk>/mark", views.mark_question_overview, name="mark_question_overview"),
path("exam/<int:pk>/mark", views.AtlasExamViews.mark_overview, name="mark_overview"),
# path("exam/<int:pk>/<int:sk>/", views.exam_take, name="exam_take"),
path(
"exam/<int:pk>/question/<int:sk>/",
views.AtlasExamViews.exam_case_detail,
name="exam_case_detail",
),
path("exam/<int:pk>/", views.AtlasExamViews.exam_overview, name="exam_overview"),
path(
"exam/<int:pk>/json_edit",
views.AtlasExamViews.exam_json_edit,
name="exam_json_edit",
),
path(
"exam/<int:pk>/question_json_refresh",
views.refresh_exam_question_json,
name="refresh_exam_question_json",
),
path("exam/<int:pk>/scores", views.exam_scores_cid, name="exam_scores_cid"),
path(
"exam/<int:pk>/scores/<int:sk>/",
views.exam_scores_cid_user,
name="exam_scores_cid_user",
),
path(
"exam/<int:pk>/toggle_active",
views.AtlasExamViews.exam_toggle_active,
name="exam_toggle_active",
),
path(
"exam/<int:pk>/toggle_results_published",
views.AtlasExamViews.exam_toggle_results_published,
name="exam_toggle_results_published",
),
path(
"exam/submit", views.AtlasExamViews.postExamAnswers, name="exam_answers_submit"
),
path("exam/", views.AtlasExamViews.exam_list, name="exam_list"),
path("exam/all", views.AtlasExamViews.exam_list_all, name="exam_list_all"),
path("exam/create", views.ExamCreate.as_view(), name="exam_create"),
path("exam/<int:exam_id>/clone", views.ExamClone.as_view(), name="exam_clone"),
path("exam/<int:pk>/update", views.ExamUpdate.as_view(), name="exam_update"),
path("exam/<int:pk>/delete", views.ExamDelete.as_view(), name="exam_delete"),
path("exam/json/", views.AtlasExamViews.active_exams, name="active_exams"),
path("exam/json/<int:pk>", views.AtlasExamViews.exam_json, name="exam_json"),
path(
"exam/json/<int:pk>/unbased",
views.AtlasExamViews.exam_json_unbased,
name="exam_json_unbased",
),
path(
"exam/json/<int:pk>/<int:sk>",
views.AtlasExamViews.exam_question_json,
name="exam_question_json",
),
path(
"exam/json/<int:pk>/unbased/<int:sk>",
views.AtlasExamViews.exam_question_json_unbased,
name="exam_question_json_unbased",
),
path(
"exam/json/<int:pk>/recreate",
views.AtlasExamViews.exam_json_recreate,
name="exam_json_recreate",
),
path("create/", views.AtlasCreate.as_view(), name="create"),
path("create/series", views.SeriesCreate.as_view(), name="series_create"),
path(
"create/series/<int:pk>",
views.SeriesCreate.as_view(),
name="series_id_create",
),
path(
"create/defaults",
views.AtlasCreationDefaultView.as_view(),
name="create_defaults",
),
# path("region/create/", views.create_region, name="create_region"),
# path("region/ajax/get_region_id", views.get_region_id, name="get_region_id"),
# path("abnormality/create/", views.create_abnormality, name="create_abnormality"),
# path(
# "abnormality/ajax/get_abnormality_id",
# views.get_abnormality_id,
# name="get_abnormality_id",
# ),
path("examination/create/", views.create_examination, name="create_examination"),
path(
"examination/ajax/get_examination_id",
views.get_examination_id,
name="get_examination_id",
),
path(
"question/<int:pk>/update",
views.AtlasUpdate.as_view(),
name="atlas_update",
),
path(
"series/<int:pk>/update",
views.SeriesUpdate.as_view(),
name="series_update",
),
path(
"user_answers/",
views.UserAnswerTableView.as_view(),
name="user_answer_table_view",
),
path(
"user_answers/<int:pk>", views.UserAnswerView.as_view(), name="user_answer_view"
),
path(
"user_answers/<int:pk>/delete",
views.UserAnswerDelete.as_view(),
name="user_answer_delete",
),
path(
"user_answers/delete",
views.user_answer_delete_multiple,
name="user_answer_delete_multiple",
),
]
Executable
+547
View File
@@ -0,0 +1,547 @@
import json
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 (
ObjectDoesNotExist,
PermissionDenied,
ViewDoesNotExist,
)
from django.contrib.auth.mixins import LoginRequiredMixin
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 (
AtlasForm,
SeriesForm,
SeriesImageFormSet,
MarkAtlasQuestionDoubleForm,
MarkAtlasQuestionSingleForm,
SeriesFormSet,
ExaminationForm,
ExamForm,
)
from .models import (
Case,
Series,
Examination,
)
from .tables import AtlasTable, SeriesTable, UserAnswerTable
from .filters import AtlasFilter, SeriesFilter, UserAnswerFilter
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 atlas.forms import AtlasCreationDefaultForm
from atlas.models import AtlasCreationDefault
from generic.views import ExamCloneMixin, ExamViews
from reversion.views import RevisionMixin
import reversion
from rest_framework import viewsets
from .serializers import ExamSerializer
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):
atlas = 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", {"question": atlas})
@login_required
@user_is_author_or_atlas_series_checker_or_atlas_marker
def atlas_series_detail(request, pk):
series = get_object_or_404(Series, pk=pk)
# if request.user not in atlas.author.all():
# raise PermissionDenied
# logging.debug(atlas.subspecialty.first().name.all())
return render(request, "atlas/atlas_series.html", {"series": series})
@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 AtlasDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView):
model = Case
success_url = reverse_lazy("atlas:atlas_view")
class SeriesDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView):
model = Series
success_url = reverse_lazy("atlas:atlas_series_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 = AtlasForm(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/atlas_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()
# Do this in the form.save()
# new_exams = list(form.cleaned_data['exams'].values_list("pk", flat=True))
# self.object.exams.clear()
# self.object.exams.add(*new_exams)
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 AtlasCreateBase(RevisionMixin, LoginRequiredMixin, CreateView):
model = Case
form_class = AtlasForm
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["series_formset"].full_clean()
else:
context["series_formset"] = SeriesFormSet()
return context
def form_valid(self, form):
self.object = form.save(commit=False)
# Add exam objects
self.object.save()
# Do this in the form.save()
# new_exams = list(form.cleaned_data['exams'].values_list("pk", flat=True))
# self.object.exams.clear()
# self.object.exams.add(*new_exams)
form.instance.author.add(self.request.user.id)
context = self.get_context_data(form=form)
series_formset = context["series_formset"]
if series_formset.is_valid():
response = super().form_valid(form)
series_formset.instance = self.object
series_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):
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 = AtlasForm
# 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["series_formset"].full_clean()
else:
context["series_formset"] = SeriesFormSet(instance=self.object)
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()
# Do this in the form.save()
# new_exams = list(form.cleaned_data['exams'].values_list("pk", flat=True))
# self.object.exams.clear()
# self.object.exams.add(*new_exams)
form.instance.author.add(self.request.user.id)
context = self.get_context_data(form=form)
series_formset = context["series_formset"]
# logger.debug(formset.is_valid())
if series_formset.is_valid():
response = super().form_valid(form)
series_formset.instance = self.object
series_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 AtlasClone(AtlasCreateBase):
"""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})
@login_required
def create_examination(request):
form = ExaminationForm(request.POST or None)
if form.is_valid():
instance = form.save()
return HttpResponse(
'<script>opener.closePopup(window, "%s", "%s", "#id_examination");</script>'
% (instance.pk, instance)
)
return render(
request, "atlas/create_simple.html", {"form": form, "name": "Examination"}
)
@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 AtlasView(LoginRequiredMixin, SingleTableMixin, FilterView):
model = Case
table_class = AtlasTable
template_name = "atlas/view.html"
filterset_class = AtlasFilter
class SeriesView(LoginRequiredMixin, SingleTableMixin, FilterView):
model = Series
table_class = SeriesTable
template_name = "atlas/view.html"
filterset_class = SeriesFilter
@login_required
@user_is_author_or_atlas_series_checker
def atlas_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:atlas_series_detail", pk=pk)
@login_required
@user_is_author_or_atlas_series_checker
def atlas_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:atlas_series_detail", pk=pk)
@login_required
@user_is_author_or_atlas_series_checker
def atlas_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:atlas_series_detail", pk=pk)
@login_required
@user_is_author_or_atlas_series_checker
def atlas_series_order_upload_filename(request, pk):
series = get_object_or_404(Series, pk=pk)
series.order_by_upload_filename()
return redirect("atlas: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()]