diff --git a/atlas/__init__.py b/atlas/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/atlas/admin.py b/atlas/admin.py
new file mode 100755
index 00000000..9a99f7a0
--- /dev/null
+++ b/atlas/admin.py
@@ -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)
\ No newline at end of file
diff --git a/atlas/apps.py b/atlas/apps.py
new file mode 100644
index 00000000..f365394c
--- /dev/null
+++ b/atlas/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class AtlasConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'atlas'
diff --git a/atlas/decorators.py b/atlas/decorators.py
new file mode 100755
index 00000000..89b4b26e
--- /dev/null
+++ b/atlas/decorators.py
@@ -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
\ No newline at end of file
diff --git a/atlas/filters.py b/atlas/filters.py
new file mode 100755
index 00000000..d0c6ac54
--- /dev/null
+++ b/atlas/filters.py
@@ -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
\ No newline at end of file
diff --git a/atlas/forms.py b/atlas/forms.py
new file mode 100755
index 00000000..f96fd7ca
--- /dev/null
+++ b/atlas/forms.py
@@ -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
+ # 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
+ # 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",
+)
\ No newline at end of file
diff --git a/atlas/migrations/__init__.py b/atlas/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/atlas/models.py b/atlas/models.py
new file mode 100644
index 00000000..b0cbfd60
--- /dev/null
+++ b/atlas/models.py
@@ -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('Invalid image url', img), len(images)
+ return format_html('
', thumbnail), len(images)
+
+ def get_block(self):
+ examination = self.get_examination_full()
+ thumb, image_number = self.get_thumbnail()
+ return format_html(
+ "{}
{}
Images: {}
", 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
diff --git a/atlas/serializers.py b/atlas/serializers.py
new file mode 100644
index 00000000..a447defc
--- /dev/null
+++ b/atlas/serializers.py
@@ -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]
\ No newline at end of file
diff --git a/atlas/tables.py b/atlas/tables.py
new file mode 100755
index 00000000..daf42c16
--- /dev/null
+++ b/atlas/tables.py
@@ -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("{}".format("".join(blocks)))
+
+ obj = value.first()
+
+ if obj is None:
+ return format_html("No image")
+
+ return obj.get_thumbnail()[0]
+
+ # image_object = obj.image
+ # try:
+ # thumbnailer = get_thumbnailer(image_object)
+ # return format_html('
', thumbnailer["exam-list"])
+ # except InvalidImageFormatError:
+ # return format_html('Invalid image url', 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('
[{}]', thumbnailer["exam-list"], value.count())
+ except InvalidImageFormatError:
+ return format_html('Invalid image url
[{}]', 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("test: {}", value)
+ obj = value.first()
+
+ if obj is None:
+ return format_html("No image")
+
+ 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("""Popup""", record.pk)
\ No newline at end of file
diff --git a/atlas/tests.py b/atlas/tests.py
new file mode 100755
index 00000000..7ce503c2
--- /dev/null
+++ b/atlas/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/atlas/urls.py b/atlas/urls.py
new file mode 100755
index 00000000..102eff23
--- /dev/null
+++ b/atlas/urls.py
@@ -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//", 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/", views.series_detail, name="series_detail"),
+ path(
+ "series//order_dicom",
+ views.series_order_dicom,
+ name="series_order_dicom",
+ ),
+ path(
+ "series//download",
+ views.SeriesImagesZipView.as_view(),
+ name="series_download",
+ ),
+ path(
+ "series//order_dicom_instance",
+ views.series_order_dicom_instance,
+ name="series_order_dicom_instance",
+ ),
+ path(
+ "series//order_dicom_SeriesInstanceUID",
+ views.series_order_dicom_SeriesInstanceUID,
+ name="series_order_dicom_SeriesInstanceUID",
+ ),
+ path(
+ "series//order_upload_filename",
+ views.series_order_upload_filename,
+ name="series_order_upload_filename",
+ ),
+ path(
+ "series//delete",
+ views.SeriesDelete.as_view(),
+ name="series_delete",
+ ),
+ # path("unchecked/", views.unchecked_list, name="unchecked_list"),
+ # path("verified//", views.verified_detail, name="verified_detail"),
+ path("question//", views.case_detail, name="case_detail"),
+ path("question//json", views.question_json, name="question_json"),
+ path(
+ "question//json/unbased",
+ views.question_json_unbased,
+ name="question_json_unbased",
+ ),
+ path(
+ "question//json/recreate",
+ views.question_json_recreate,
+ name="question_json_recreate",
+ ),
+ path("question//split", views.atlas_split, name="atlas_split"),
+ path("question//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//scrap", views.atlas_scrap, name="atlas_scrap"),
+ path("question//delete", views.AtlasDelete.as_view(), name="atlas_delete"),
+ path(
+ "exam//review",
+ views.question_review,
+ name="question_review",
+ ),
+ path("exam////mark", views.mark_answer, name="mark_answer"),
+ path("exam////mark_override", views.mark_answer_override, name="mark_answer_override"),
+ path("exam///mark", views.mark_question_overview, name="mark_question_overview"),
+ path("exam//mark", views.AtlasExamViews.mark_overview, name="mark_overview"),
+ # path("exam///", views.exam_take, name="exam_take"),
+ path(
+ "exam//question//",
+ views.AtlasExamViews.exam_case_detail,
+ name="exam_case_detail",
+ ),
+ path("exam//", views.AtlasExamViews.exam_overview, name="exam_overview"),
+ path(
+ "exam//json_edit",
+ views.AtlasExamViews.exam_json_edit,
+ name="exam_json_edit",
+ ),
+ path(
+ "exam//question_json_refresh",
+ views.refresh_exam_question_json,
+ name="refresh_exam_question_json",
+ ),
+ path("exam//scores", views.exam_scores_cid, name="exam_scores_cid"),
+ path(
+ "exam//scores//",
+ views.exam_scores_cid_user,
+ name="exam_scores_cid_user",
+ ),
+ path(
+ "exam//toggle_active",
+ views.AtlasExamViews.exam_toggle_active,
+ name="exam_toggle_active",
+ ),
+ path(
+ "exam//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//clone", views.ExamClone.as_view(), name="exam_clone"),
+ path("exam//update", views.ExamUpdate.as_view(), name="exam_update"),
+ path("exam//delete", views.ExamDelete.as_view(), name="exam_delete"),
+ path("exam/json/", views.AtlasExamViews.active_exams, name="active_exams"),
+ path("exam/json/", views.AtlasExamViews.exam_json, name="exam_json"),
+ path(
+ "exam/json//unbased",
+ views.AtlasExamViews.exam_json_unbased,
+ name="exam_json_unbased",
+ ),
+ path(
+ "exam/json//",
+ views.AtlasExamViews.exam_question_json,
+ name="exam_question_json",
+ ),
+ path(
+ "exam/json//unbased/",
+ views.AtlasExamViews.exam_question_json_unbased,
+ name="exam_question_json_unbased",
+ ),
+ path(
+ "exam/json//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/",
+ 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//update",
+ views.AtlasUpdate.as_view(),
+ name="atlas_update",
+ ),
+ path(
+ "series//update",
+ views.SeriesUpdate.as_view(),
+ name="series_update",
+ ),
+ path(
+ "user_answers/",
+ views.UserAnswerTableView.as_view(),
+ name="user_answer_table_view",
+ ),
+ path(
+ "user_answers/", views.UserAnswerView.as_view(), name="user_answer_view"
+ ),
+ path(
+ "user_answers//delete",
+ views.UserAnswerDelete.as_view(),
+ name="user_answer_delete",
+ ),
+ path(
+ "user_answers/delete",
+ views.user_answer_delete_multiple,
+ name="user_answer_delete_multiple",
+ ),
+]
diff --git a/atlas/views.py b/atlas/views.py
new file mode 100755
index 00000000..aa2501fa
--- /dev/null
+++ b/atlas/views.py
@@ -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('' % (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(
+ ''
+ % (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("Series does not appear to contain dicoms
")
+
+ 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(
+ "Series does not appear to contain dicoms (or field InstanceNumber)
"
+ )
+
+ 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(
+ "Series does not appear to contain dicoms (or field SeriesInstanceUID)
"
+ )
+
+ 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()]
+