start atlas app
This commit is contained in:
Executable
+58
@@ -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)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AtlasConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'atlas'
|
||||||
Executable
+66
@@ -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
|
||||||
Executable
+42
@@ -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
@@ -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",
|
||||||
|
)
|
||||||
+378
@@ -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
|
||||||
@@ -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]
|
||||||
Executable
+114
@@ -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
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
Executable
+191
@@ -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
@@ -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()]
|
||||||
|
|
||||||
Reference in New Issue
Block a user