.
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.1.4 on 2023-06-12 09:55
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('atlas', '0049_alter_condition_parent_alter_condition_synonym_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='series',
|
||||||
|
name='description',
|
||||||
|
field=models.TextField(blank=True, help_text='Description of stack, for admin organisation, will not be visible when taking'),
|
||||||
|
),
|
||||||
|
]
|
||||||
+10
-181
@@ -10,8 +10,6 @@ from django.utils import timezone
|
|||||||
|
|
||||||
|
|
||||||
import pydicom
|
import pydicom
|
||||||
import dicognito.anonymizer
|
|
||||||
|
|
||||||
|
|
||||||
from django.core.files.storage import FileSystemStorage
|
from django.core.files.storage import FileSystemStorage
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -43,13 +41,11 @@ from generic.models import (
|
|||||||
Plane,
|
Plane,
|
||||||
Contrast,
|
Contrast,
|
||||||
QuestionNote,
|
QuestionNote,
|
||||||
|
SeriesBase,
|
||||||
)
|
)
|
||||||
|
|
||||||
# from generic.models import Examination, Site, Condition, Sign
|
# from generic.models import Examination, Site, Condition, Sign
|
||||||
|
|
||||||
from easy_thumbnails.files import get_thumbnailer
|
|
||||||
from easy_thumbnails.exceptions import InvalidImageFormatError
|
|
||||||
|
|
||||||
import pydicom.errors
|
import pydicom.errors
|
||||||
import datetime
|
import datetime
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@@ -343,7 +339,6 @@ class SeriesImage(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class SeriesFinding(models.Model):
|
class SeriesFinding(models.Model):
|
||||||
|
|
||||||
series = models.ForeignKey(
|
series = models.ForeignKey(
|
||||||
"Series", related_name="findings", on_delete=models.SET_NULL, null=True
|
"Series", related_name="findings", on_delete=models.SET_NULL, null=True
|
||||||
)
|
)
|
||||||
@@ -364,7 +359,7 @@ class SeriesFinding(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
@reversion.register
|
@reversion.register
|
||||||
class Series(models.Model):
|
class Series(SeriesBase):
|
||||||
modality = models.ForeignKey(
|
modality = models.ForeignKey(
|
||||||
Modality,
|
Modality,
|
||||||
related_name="atlas_series_modality",
|
related_name="atlas_series_modality",
|
||||||
@@ -394,10 +389,6 @@ class Series(models.Model):
|
|||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
description = models.TextField(
|
|
||||||
blank=True,
|
|
||||||
help_text="Description of stack",
|
|
||||||
)
|
|
||||||
|
|
||||||
author = models.ManyToManyField(
|
author = models.ManyToManyField(
|
||||||
settings.AUTH_USER_MODEL,
|
settings.AUTH_USER_MODEL,
|
||||||
@@ -407,13 +398,6 @@ class Series(models.Model):
|
|||||||
|
|
||||||
# findings = models.TextField(null=True, blank=True, help_text="Findings on the series / stack")
|
# findings = models.TextField(null=True, blank=True, help_text="Findings on the series / stack")
|
||||||
|
|
||||||
open_access = models.BooleanField(
|
|
||||||
help_text="If a series should be freely available to browse", default=True
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.pk}:{self.description}"
|
|
||||||
|
|
||||||
def get_full_str(self):
|
def get_full_str(self):
|
||||||
if self.case:
|
if self.case:
|
||||||
case_id = ", ".format([case.pk for case in self.case.all()])
|
case_id = ", ".format([case.pk for case in self.case.all()])
|
||||||
@@ -424,167 +408,10 @@ class Series(models.Model):
|
|||||||
self.pk, self.get_examination_full(), self.description, case_id
|
self.pk, self.get_examination_full(), self.description, case_id
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse("series:question_detail", kwargs={"pk": self.pk})
|
|
||||||
|
|
||||||
# def get_string(self):
|
|
||||||
# examination = self.get_examination_full()
|
|
||||||
# return
|
|
||||||
|
|
||||||
def get_author_objects(self):
|
|
||||||
"""Returns a comma seperated text list of authors"""
|
|
||||||
if self.author:
|
|
||||||
return self.author.all()
|
|
||||||
else:
|
|
||||||
return ["None"]
|
|
||||||
|
|
||||||
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):
|
def get_absolute_url(self):
|
||||||
return reverse("atlas:series_detail", kwargs={"pk": self.pk})
|
return reverse("atlas:series_detail", kwargs={"pk": self.pk})
|
||||||
|
|
||||||
def get_image_urls(self, findings=False):
|
|
||||||
images = [f"{REMOTE_URL}{i.image.url}" for i in self.images.all()]
|
|
||||||
|
|
||||||
return ",".join(images)
|
|
||||||
|
|
||||||
def get_image_url_array_not_json(self):
|
|
||||||
return self.get_image_url_array(json_output=False)
|
|
||||||
|
|
||||||
def get_image_url_array(self, findings=False, json_output=True):
|
|
||||||
images = [f"{REMOTE_URL}{i.image.url}" for i in self.images.all()]
|
|
||||||
|
|
||||||
if json_output:
|
|
||||||
return json.dumps(images)
|
|
||||||
else:
|
|
||||||
# This is a mess...
|
|
||||||
return format_html('", "'.join(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_thumbnail_link(self):
|
|
||||||
return format_html(
|
|
||||||
"<a href='{}'>{}<a/>", self.get_absolute_url(), self.get_thumbnail()[0]
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
def anonymise_images(self):
|
|
||||||
# NOTE: this will not maintain the correct hashed filename
|
|
||||||
# but that doesn't matter as we will never get the same anonymisation
|
|
||||||
# even with the same dicom...
|
|
||||||
anonymizer = dicognito.anonymizer.Anonymizer()
|
|
||||||
|
|
||||||
for series_image in self.images.all():
|
|
||||||
file_path = os.path.join(settings.MEDIA_ROOT, series_image.image.name)
|
|
||||||
|
|
||||||
try:
|
|
||||||
with pydicom.dcmread(file_path) as dataset:
|
|
||||||
anonymizer.anonymize(dataset)
|
|
||||||
dataset.save_as(file_path)
|
|
||||||
except pydicom.errors.InvalidDicomError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class CaseCollection(models.Model):
|
class CaseCollection(models.Model):
|
||||||
@@ -647,8 +474,11 @@ class CaseCollection(models.Model):
|
|||||||
CidUser, blank=True, related_name="casecollection_exams"
|
CidUser, blank=True, related_name="casecollection_exams"
|
||||||
)
|
)
|
||||||
|
|
||||||
cid_user_groups = models.ManyToManyField(CidUserGroup, blank=True, help_text="These groups define which candidates are able to be added to the exams/collection.")
|
cid_user_groups = models.ManyToManyField(
|
||||||
|
CidUserGroup,
|
||||||
|
blank=True,
|
||||||
|
help_text="These groups define which candidates are able to be added to the exams/collection.",
|
||||||
|
)
|
||||||
|
|
||||||
archive = models.BooleanField(default=False)
|
archive = models.BooleanField(default=False)
|
||||||
exam_mode = models.BooleanField(default=False)
|
exam_mode = models.BooleanField(default=False)
|
||||||
@@ -693,7 +523,6 @@ class CaseCollection(models.Model):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
if self.valid_cid_users.exists():
|
if self.valid_cid_users.exists():
|
||||||
|
|
||||||
user = self.valid_cid_users.filter(cid=cid).first()
|
user = self.valid_cid_users.filter(cid=cid).first()
|
||||||
|
|
||||||
if not user or user.passcode != passcode:
|
if not user or user.passcode != passcode:
|
||||||
@@ -771,13 +600,13 @@ class BaseReportAnswer(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
class CidReportAnswer(BaseReportAnswer):
|
class CidReportAnswer(BaseReportAnswer):
|
||||||
cid = models.BigIntegerField(
|
cid = models.BigIntegerField(
|
||||||
blank=False,
|
blank=False,
|
||||||
help_text="Candidate ID",
|
help_text="Candidate ID",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class UserReportAnswer(BaseReportAnswer):
|
class UserReportAnswer(BaseReportAnswer):
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||||
settings.AUTH_USER_MODEL, on_delete=models.CASCADE
|
|
||||||
)
|
|
||||||
|
|||||||
+3
-1
@@ -25,7 +25,9 @@ class AtlasImageColumn(tables.Column):
|
|||||||
for obj in value.all():
|
for obj in value.all():
|
||||||
blocks.append(obj.get_block())
|
blocks.append(obj.get_block())
|
||||||
return format_html(
|
return format_html(
|
||||||
"<span class='multi-image-block'>{}</span>".format("".join(blocks))
|
"""<span class='multi-image-block'>
|
||||||
|
{}
|
||||||
|
</span>""".format("".join(blocks))
|
||||||
)
|
)
|
||||||
|
|
||||||
obj = value.first()
|
obj = value.first()
|
||||||
|
|||||||
+309
-50
@@ -1,4 +1,5 @@
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
import json
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@@ -23,6 +24,22 @@ from django.contrib.auth.models import User
|
|||||||
|
|
||||||
from django.db.models.signals import post_save
|
from django.db.models.signals import post_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
from rad.settings import REMOTE_URL
|
||||||
|
from django.utils.html import format_html
|
||||||
|
|
||||||
|
from easy_thumbnails.files import get_thumbnailer
|
||||||
|
from easy_thumbnails.exceptions import InvalidImageFormatError
|
||||||
|
|
||||||
|
import pydicom
|
||||||
|
import dicognito.anonymizer
|
||||||
|
|
||||||
|
|
||||||
|
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)]
|
||||||
|
|
||||||
|
|
||||||
class Plane(models.Model):
|
class Plane(models.Model):
|
||||||
@@ -57,16 +74,19 @@ class Examination(models.Model):
|
|||||||
|
|
||||||
class Site(models.Model):
|
class Site(models.Model):
|
||||||
"""Model to hold site details"""
|
"""Model to hold site details"""
|
||||||
full_name = models.CharField(max_length=255, blank=True, help_text="Name of the site")
|
|
||||||
short_code = models.CharField(max_length=255, blank=True, help_text="Shortcode/name of the site")
|
|
||||||
|
|
||||||
|
full_name = models.CharField(
|
||||||
|
max_length=255, blank=True, help_text="Name of the site"
|
||||||
|
)
|
||||||
|
short_code = models.CharField(
|
||||||
|
max_length=255, blank=True, help_text="Shortcode/name of the site"
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.short_code
|
return self.short_code
|
||||||
|
|
||||||
|
|
||||||
class QuestionBase(models.Model):
|
class QuestionBase(models.Model):
|
||||||
|
|
||||||
authors_only = models.BooleanField(
|
authors_only = models.BooleanField(
|
||||||
help_text="If true only question authors will be able to view.",
|
help_text="If true only question authors will be able to view.",
|
||||||
default=False,
|
default=False,
|
||||||
@@ -96,6 +116,181 @@ class QuestionBase(models.Model):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class SeriesBase(models.Model):
|
||||||
|
description = models.TextField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Description of stack, for admin organisation, will not be visible when taking",
|
||||||
|
)
|
||||||
|
|
||||||
|
open_access = models.BooleanField(
|
||||||
|
help_text="If a series should be freely available to browse", default=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.pk}:{self.description}"
|
||||||
|
|
||||||
|
def get_author_objects(self):
|
||||||
|
"""Returns a comma seperated text list of authors"""
|
||||||
|
if self.author:
|
||||||
|
return self.author.all()
|
||||||
|
else:
|
||||||
|
return ["None"]
|
||||||
|
|
||||||
|
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_image_urls(self):
|
||||||
|
images = [f"{REMOTE_URL}{i.image.url}" for i in self.images.all()]
|
||||||
|
|
||||||
|
return ",".join(images)
|
||||||
|
|
||||||
|
def get_image_url_array_not_json(self):
|
||||||
|
return self.get_image_url_array(json_output=False)
|
||||||
|
|
||||||
|
def get_image_url_array(self, json_output=True):
|
||||||
|
images = [f"{REMOTE_URL}{i.image.url}" for i in self.images.all()]
|
||||||
|
|
||||||
|
if json_output:
|
||||||
|
return json.dumps(images)
|
||||||
|
else:
|
||||||
|
# This is a mess...
|
||||||
|
return format_html('", "'.join(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_thumbnail_link(self):
|
||||||
|
return format_html(
|
||||||
|
"<a href='{}'>{}<a/>", self.get_absolute_url(), self.get_thumbnail()[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_block(self):
|
||||||
|
examination = self.get_examination_full()
|
||||||
|
thumb, image_number = self.get_thumbnail()
|
||||||
|
return format_html(
|
||||||
|
"""<span>
|
||||||
|
<span class='series-block-examination'>{}</span><br/>
|
||||||
|
<span class='series-block-thumbnail'>{}</span><br/>
|
||||||
|
<span class='series-block-image-number'>Images: <span class='series-block-image-number-count'>{}</span></span>
|
||||||
|
</span>""",
|
||||||
|
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
|
||||||
|
|
||||||
|
def anonymise_images(self):
|
||||||
|
# NOTE: this will not maintain the correct hashed filename
|
||||||
|
# but that doesn't matter as we will never get the same anonymisation
|
||||||
|
# even with the same dicom...
|
||||||
|
anonymizer = dicognito.anonymizer.Anonymizer()
|
||||||
|
|
||||||
|
for series_image in self.images.all():
|
||||||
|
file_path = os.path.join(settings.MEDIA_ROOT, series_image.image.name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with pydicom.dcmread(file_path) as dataset:
|
||||||
|
anonymizer.anonymize(dataset)
|
||||||
|
dataset.save_as(file_path)
|
||||||
|
except pydicom.errors.InvalidDicomError:
|
||||||
|
pass
|
||||||
|
|
||||||
class ExamBase(models.Model):
|
class ExamBase(models.Model):
|
||||||
name = models.CharField(max_length=200, help_text="Name of the exam")
|
name = models.CharField(max_length=200, help_text="Name of the exam")
|
||||||
# exam_questions = SortedManyToManyField(Long, related_name="exams", blank="true")
|
# exam_questions = SortedManyToManyField(Long, related_name="exams", blank="true")
|
||||||
@@ -162,7 +357,6 @@ class ExamBase(models.Model):
|
|||||||
|
|
||||||
exam_results_emailed = models.DateTimeField(default=None, null=True)
|
exam_results_emailed = models.DateTimeField(default=None, null=True)
|
||||||
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
@@ -191,10 +385,14 @@ class ExamBase(models.Model):
|
|||||||
return reverse("{}:exam_overview".format(self.app_name), kwargs={"pk": self.pk})
|
return reverse("{}:exam_overview".format(self.app_name), kwargs={"pk": self.pk})
|
||||||
|
|
||||||
def get_cid_edit_url(self):
|
def get_cid_edit_url(self):
|
||||||
return reverse("{}:exam_cids_edit".format(self.app_name), kwargs={"exam_id": self.pk})
|
return reverse(
|
||||||
|
"{}:exam_cids_edit".format(self.app_name), kwargs={"exam_id": self.pk}
|
||||||
|
)
|
||||||
|
|
||||||
def get_user_edit_url(self):
|
def get_user_edit_url(self):
|
||||||
return reverse("{}:exam_users_edit".format(self.app_name), kwargs={"exam_id": self.pk})
|
return reverse(
|
||||||
|
"{}:exam_users_edit".format(self.app_name), kwargs={"exam_id": self.pk}
|
||||||
|
)
|
||||||
|
|
||||||
def get_take_url(self):
|
def get_take_url(self):
|
||||||
return f"{settings.REMOTE_URL}/rts"
|
return f"{settings.REMOTE_URL}/rts"
|
||||||
@@ -206,7 +404,9 @@ class ExamBase(models.Model):
|
|||||||
if cid is None:
|
if cid is None:
|
||||||
return reverse("{}:exam_json".format(self.app_name), args=(self.pk,))
|
return reverse("{}:exam_json".format(self.app_name), args=(self.pk,))
|
||||||
else:
|
else:
|
||||||
return reverse("{}:exam_json_cid".format(self.app_name), args=(self.pk,cid, passcode))
|
return reverse(
|
||||||
|
"{}:exam_json_cid".format(self.app_name), args=(self.pk, cid, passcode)
|
||||||
|
)
|
||||||
|
|
||||||
def get_question_index(self, question):
|
def get_question_index(self, question):
|
||||||
return list(self.exam_questions.all()).index(question)
|
return list(self.exam_questions.all()).index(question)
|
||||||
@@ -223,7 +423,14 @@ class ExamBase(models.Model):
|
|||||||
authors = [i for i in self.author.all()]
|
authors = [i for i in self.author.all()]
|
||||||
return authors
|
return authors
|
||||||
|
|
||||||
def check_cid_user(self, cid: int|None, passcode: str|None, request: HttpRequest|None=None, user_id: int|None=None, allow_authors: bool=True):
|
def check_cid_user(
|
||||||
|
self,
|
||||||
|
cid: int | None,
|
||||||
|
passcode: str | None,
|
||||||
|
request: HttpRequest | None = None,
|
||||||
|
user_id: int | None = None,
|
||||||
|
allow_authors: bool = True,
|
||||||
|
):
|
||||||
print(f"Check cid user: {cid=}, {passcode=}, exam_id={self.pk}")
|
print(f"Check cid user: {cid=}, {passcode=}, exam_id={self.pk}")
|
||||||
if request is not None and request.user.is_superuser:
|
if request is not None and request.user.is_superuser:
|
||||||
return True
|
return True
|
||||||
@@ -243,7 +450,6 @@ class ExamBase(models.Model):
|
|||||||
|
|
||||||
# Then test CID data
|
# Then test CID data
|
||||||
if self.valid_cid_users.exists():
|
if self.valid_cid_users.exists():
|
||||||
|
|
||||||
cid_user = self.valid_cid_users.filter(cid=cid).first()
|
cid_user = self.valid_cid_users.filter(cid=cid).first()
|
||||||
|
|
||||||
if cid_user and cid_user.passcode == passcode:
|
if cid_user and cid_user.passcode == passcode:
|
||||||
@@ -253,7 +459,9 @@ class ExamBase(models.Model):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_or_create_cid_user_exam(self, cid_user=None, user_user=None, start_time=None):
|
def get_or_create_cid_user_exam(
|
||||||
|
self, cid_user=None, user_user=None, start_time=None
|
||||||
|
):
|
||||||
content_type = ContentType.objects.get_for_model(self)
|
content_type = ContentType.objects.get_for_model(self)
|
||||||
|
|
||||||
if cid_user is not None:
|
if cid_user is not None:
|
||||||
@@ -270,11 +478,9 @@ class ExamBase(models.Model):
|
|||||||
if c:
|
if c:
|
||||||
return c
|
return c
|
||||||
|
|
||||||
|
|
||||||
if start_time is None:
|
if start_time is None:
|
||||||
start_time = timezone.now()
|
start_time = timezone.now()
|
||||||
|
|
||||||
|
|
||||||
if cid_user is not None:
|
if cid_user is not None:
|
||||||
new = CidUserExam(
|
new = CidUserExam(
|
||||||
content_type=content_type,
|
content_type=content_type,
|
||||||
@@ -334,13 +540,10 @@ class ExamBase(models.Model):
|
|||||||
user_score = "Score not generated"
|
user_score = "Score not generated"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
percentage = (
|
percentage = f" ({user_score / int(self.stats_max_possible) * 100:.2f}%)"
|
||||||
f" ({user_score / int(self.stats_max_possible) * 100:.2f}%)"
|
|
||||||
)
|
|
||||||
except TypeError:
|
except TypeError:
|
||||||
percentage = ""
|
percentage = ""
|
||||||
|
|
||||||
|
|
||||||
exam_text.append(
|
exam_text.append(
|
||||||
f"""Results\n{'-'*len('Results')}
|
f"""Results\n{'-'*len('Results')}
|
||||||
# Score: {user_score} / {int(self.stats_max_possible)}{percentage}
|
# Score: {user_score} / {int(self.stats_max_possible)}{percentage}
|
||||||
@@ -349,11 +552,8 @@ class ExamBase(models.Model):
|
|||||||
stats = self.get_exam_stats(name=False)
|
stats = self.get_exam_stats(name=False)
|
||||||
exam_text.append(f"Stats\n{'-'*len('Stats')}\n{stats}")
|
exam_text.append(f"Stats\n{'-'*len('Stats')}\n{stats}")
|
||||||
|
|
||||||
|
|
||||||
if self.app_name == "rapids":
|
if self.app_name == "rapids":
|
||||||
exam_text.append(
|
exam_text.append(f"Answers breakdown: {callstate}")
|
||||||
f"Answers breakdown: {callstate}"
|
|
||||||
)
|
|
||||||
|
|
||||||
msg = "\n\n".join(exam_text)
|
msg = "\n\n".join(exam_text)
|
||||||
|
|
||||||
@@ -376,7 +576,6 @@ class ExamBase(models.Model):
|
|||||||
if supervisor_email:
|
if supervisor_email:
|
||||||
emails.append(supervisor_email)
|
emails.append(supervisor_email)
|
||||||
|
|
||||||
|
|
||||||
if additional_emails is not None:
|
if additional_emails is not None:
|
||||||
emails.extend(additional_emails)
|
emails.extend(additional_emails)
|
||||||
|
|
||||||
@@ -396,10 +595,15 @@ class ExamBase(models.Model):
|
|||||||
# self.save()
|
# self.save()
|
||||||
return [True, ""]
|
return [True, ""]
|
||||||
|
|
||||||
|
|
||||||
class ExamUserStatus(models.Model):
|
class ExamUserStatus(models.Model):
|
||||||
datetime = models.DateTimeField(auto_now_add=True)
|
datetime = models.DateTimeField(auto_now_add=True)
|
||||||
cid_user = models.ForeignKey("CidUser", blank=True, null=True, on_delete=models.SET_NULL)
|
cid_user = models.ForeignKey(
|
||||||
user_user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL)
|
"CidUser", blank=True, null=True, on_delete=models.SET_NULL
|
||||||
|
)
|
||||||
|
user_user = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL
|
||||||
|
)
|
||||||
status = models.CharField(max_length=255)
|
status = models.CharField(max_length=255)
|
||||||
extra = models.CharField(max_length=255)
|
extra = models.CharField(max_length=255)
|
||||||
|
|
||||||
@@ -416,7 +620,6 @@ class ExamUserStatus(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class UserAnswerBase(models.Model):
|
class UserAnswerBase(models.Model):
|
||||||
|
|
||||||
cid = models.BigIntegerField(
|
cid = models.BigIntegerField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
@@ -426,16 +629,13 @@ class UserAnswerBase(models.Model):
|
|||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
updated = models.DateTimeField(auto_now=True)
|
updated = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
ordering = ["cid"]
|
ordering = ["cid"]
|
||||||
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return "{}/{}/{}".format(self.get_candidate_name(), self.exam, self.question.pk)
|
return "{}/{}/{}".format(self.get_candidate_name(), self.exam, self.question.pk)
|
||||||
|
|
||||||
|
|
||||||
def get_candidate_name(self) -> str:
|
def get_candidate_name(self) -> str:
|
||||||
if self.cid is not None:
|
if self.cid is not None:
|
||||||
return str(self.cid)
|
return str(self.cid)
|
||||||
@@ -512,6 +712,7 @@ class QuestionNote(models.Model):
|
|||||||
def get_short_str(self):
|
def get_short_str(self):
|
||||||
return f"{self.note_type} - {self.note}"
|
return f"{self.note_type} - {self.note}"
|
||||||
|
|
||||||
|
|
||||||
EXAM_TYPES = (
|
EXAM_TYPES = (
|
||||||
("physics", "physics_exams"),
|
("physics", "physics_exams"),
|
||||||
("rapids", "rapid_exams"),
|
("rapids", "rapid_exams"),
|
||||||
@@ -521,25 +722,31 @@ EXAM_TYPES = (
|
|||||||
("casecollection", "casecollection_exams"),
|
("casecollection", "casecollection_exams"),
|
||||||
)
|
)
|
||||||
|
|
||||||
class CidUser(models.Model):
|
|
||||||
|
|
||||||
|
class CidUser(models.Model):
|
||||||
cid = models.IntegerField(blank=True, null=True, unique=True)
|
cid = models.IntegerField(blank=True, null=True, unique=True)
|
||||||
passcode = models.CharField(blank=True, max_length=50)
|
passcode = models.CharField(blank=True, max_length=50)
|
||||||
active = models.BooleanField(default=True)
|
active = models.BooleanField(default=True)
|
||||||
internal_candidate = models.BooleanField(default=False)
|
internal_candidate = models.BooleanField(default=False)
|
||||||
name = models.CharField(blank=True, max_length=255)
|
name = models.CharField(blank=True, max_length=255)
|
||||||
email = models.EmailField(blank=True)
|
email = models.EmailField(blank=True)
|
||||||
supervisor = models.ForeignKey("Supervisor", on_delete=models.SET_NULL, blank=True, null=True, related_name="cid_user")
|
supervisor = models.ForeignKey(
|
||||||
|
"Supervisor",
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
related_name="cid_user",
|
||||||
|
)
|
||||||
|
|
||||||
login_email_sent = models.BooleanField(default=False)
|
login_email_sent = models.BooleanField(default=False)
|
||||||
results_email_sent = models.BooleanField(default=False)
|
results_email_sent = models.BooleanField(default=False)
|
||||||
|
|
||||||
group = models.ForeignKey("CidUserGroup", on_delete=models.SET_NULL, null=True, blank=True)
|
group = models.ForeignKey(
|
||||||
|
"CidUserGroup", on_delete=models.SET_NULL, null=True, blank=True
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
indexes = [
|
indexes = [models.Index(fields=["cid"])]
|
||||||
models.Index(fields=["cid"])
|
|
||||||
]
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return str(self.cid)
|
return str(self.cid)
|
||||||
@@ -648,7 +855,6 @@ class CidUser(models.Model):
|
|||||||
|
|
||||||
login_url = reverse("cid_selector")
|
login_url = reverse("cid_selector")
|
||||||
|
|
||||||
|
|
||||||
msg = f"""
|
msg = f"""
|
||||||
See below for your details for: {self.group}
|
See below for your details for: {self.group}
|
||||||
|
|
||||||
@@ -699,14 +905,16 @@ class CidUser(models.Model):
|
|||||||
return reverse("generic:candidate_email_results", kwargs={"cid": self.cid})
|
return reverse("generic:candidate_email_results", kwargs={"cid": self.cid})
|
||||||
|
|
||||||
def get_email_results_resend_url(self):
|
def get_email_results_resend_url(self):
|
||||||
return reverse("generic:candidate_email_results_resend", kwargs={"cid": self.cid})
|
return reverse(
|
||||||
|
"generic:candidate_email_results_resend", kwargs={"cid": self.cid}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_next_cid():
|
def get_next_cid():
|
||||||
"""Returns the next available CID"""
|
"""Returns the next available CID"""
|
||||||
return CidUser.objects.order_by("-cid").first().cid + 1
|
return CidUser.objects.order_by("-cid").first().cid + 1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class CidUserExam(models.Model):
|
class CidUserExam(models.Model):
|
||||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||||
object_id = models.PositiveIntegerField()
|
object_id = models.PositiveIntegerField()
|
||||||
@@ -715,8 +923,20 @@ class CidUserExam(models.Model):
|
|||||||
start_time = models.DateTimeField(blank=True, null=True)
|
start_time = models.DateTimeField(blank=True, null=True)
|
||||||
end_time = models.DateTimeField(blank=True, null=True)
|
end_time = models.DateTimeField(blank=True, null=True)
|
||||||
|
|
||||||
cid_user = models.ForeignKey(CidUser, on_delete=models.CASCADE, blank=True, null=True, related_name="cid_userexam")
|
cid_user = models.ForeignKey(
|
||||||
user_user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, blank=True, null=True, related_name="user_userexam")
|
CidUser,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
related_name="cid_userexam",
|
||||||
|
)
|
||||||
|
user_user = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
related_name="user_userexam",
|
||||||
|
)
|
||||||
|
|
||||||
manual_submission = models.BooleanField(default=False)
|
manual_submission = models.BooleanField(default=False)
|
||||||
|
|
||||||
@@ -728,7 +948,10 @@ class CidUserExam(models.Model):
|
|||||||
user = self.user_user.username
|
user = self.user_user.username
|
||||||
else:
|
else:
|
||||||
user = self.cid_user.cid
|
user = self.cid_user.cid
|
||||||
return f"{user}: {self.start_time:%Y-%m-%d %H:%M} {self.end_time:%Y-%m-%d %H:%M}"
|
return (
|
||||||
|
f"{user}: {self.start_time:%Y-%m-%d %H:%M} {self.end_time:%Y-%m-%d %H:%M}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
CID_GROUP_EXAMS = (
|
CID_GROUP_EXAMS = (
|
||||||
("SBAs", "sba_cid_user_groups"),
|
("SBAs", "sba_cid_user_groups"),
|
||||||
@@ -738,6 +961,7 @@ CID_GROUP_EXAMS = (
|
|||||||
("Longs", "longs_cid_user_groups"),
|
("Longs", "longs_cid_user_groups"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CidUserGroup(models.Model):
|
class CidUserGroup(models.Model):
|
||||||
name = models.CharField(blank=True, max_length=50)
|
name = models.CharField(blank=True, max_length=50)
|
||||||
archive = models.BooleanField(default=False)
|
archive = models.BooleanField(default=False)
|
||||||
@@ -760,7 +984,6 @@ class CidUserGroup(models.Model):
|
|||||||
|
|
||||||
return dict(exams)
|
return dict(exams)
|
||||||
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("generic:cid_group_update", kwargs={"pk": self.pk})
|
return reverse("generic:cid_group_update", kwargs={"pk": self.pk})
|
||||||
|
|
||||||
@@ -773,9 +996,15 @@ USER_GROUP_EXAMS = (
|
|||||||
("Longs", "longs_user_user_groups"),
|
("Longs", "longs_user_user_groups"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class UserUserGroup(models.Model):
|
class UserUserGroup(models.Model):
|
||||||
name = models.CharField(blank=True, max_length=50, help_text="Name of the User Group")
|
name = models.CharField(
|
||||||
archive = models.BooleanField(default=False, help_text="Archived groups remain on the test system but are not displayed by default")
|
blank=True, max_length=50, help_text="Name of the User Group"
|
||||||
|
)
|
||||||
|
archive = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Archived groups remain on the test system but are not displayed by default",
|
||||||
|
)
|
||||||
|
|
||||||
users = models.ManyToManyField(
|
users = models.ManyToManyField(
|
||||||
settings.AUTH_USER_MODEL,
|
settings.AUTH_USER_MODEL,
|
||||||
@@ -804,12 +1033,14 @@ class UserUserGroup(models.Model):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("generic:user_group_update", kwargs={"pk": self.pk})
|
return reverse("generic:user_group_update", kwargs={"pk": self.pk})
|
||||||
|
|
||||||
|
|
||||||
class UserGrades(models.Model):
|
class UserGrades(models.Model):
|
||||||
name = models.CharField(max_length=100)
|
name = models.CharField(max_length=100)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
USER_EXAM_TYPES = (
|
USER_EXAM_TYPES = (
|
||||||
("Physics", "user_physics_exams"),
|
("Physics", "user_physics_exams"),
|
||||||
("Rapids", "user_rapid_exams"),
|
("Rapids", "user_rapid_exams"),
|
||||||
@@ -819,14 +1050,26 @@ USER_EXAM_TYPES = (
|
|||||||
# ("CaseCollection", "user_casecollection_exams"),
|
# ("CaseCollection", "user_casecollection_exams"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class UserProfile(models.Model):
|
class UserProfile(models.Model):
|
||||||
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||||
supervisor = models.ForeignKey("Supervisor", on_delete=models.SET_NULL, blank=True, null=True, related_name="trainee")
|
supervisor = models.ForeignKey(
|
||||||
|
"Supervisor",
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
related_name="trainee",
|
||||||
|
)
|
||||||
registration_number = models.CharField(max_length=25, blank=True)
|
registration_number = models.CharField(max_length=25, blank=True)
|
||||||
grade = models.ForeignKey(UserGrades, null=True, blank=True, help_text="User grade", on_delete=models.CASCADE)
|
grade = models.ForeignKey(
|
||||||
|
UserGrades,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="User grade",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
peninsula_trainee = models.BooleanField(default=False)
|
peninsula_trainee = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
|
||||||
def getusername(self):
|
def getusername(self):
|
||||||
return self.user.username
|
return self.user.username
|
||||||
|
|
||||||
@@ -846,22 +1089,38 @@ class UserProfile(models.Model):
|
|||||||
|
|
||||||
return dict(available_exams)
|
return dict(available_exams)
|
||||||
|
|
||||||
|
|
||||||
class Supervisor(models.Model):
|
class Supervisor(models.Model):
|
||||||
"""Model to hold individual supervisor details (email and name)
|
"""Model to hold individual supervisor details (email and name)
|
||||||
|
|
||||||
This can be linked with a user account.
|
This can be linked with a user account.
|
||||||
"""
|
"""
|
||||||
email = models.EmailField(unique=True, help_text="The (nhs.net) email address of the supervisor")
|
|
||||||
|
email = models.EmailField(
|
||||||
|
unique=True, help_text="The (nhs.net) email address of the supervisor"
|
||||||
|
)
|
||||||
name = models.CharField(max_length=255, help_text="Name of the supervisor")
|
name = models.CharField(max_length=255, help_text="Name of the supervisor")
|
||||||
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True, blank=True, help_text="If the supervisor has an account on the test system that can be associated here")
|
user = models.OneToOneField(
|
||||||
site = models.ForeignKey("Site", on_delete=models.SET_NULL, null=True, blank=True, help_text="Hospital site at which the supervisor is based")
|
settings.AUTH_USER_MODEL,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="If the supervisor has an account on the test system that can be associated here",
|
||||||
|
)
|
||||||
|
site = models.ForeignKey(
|
||||||
|
"Site",
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="Hospital site at which the supervisor is based",
|
||||||
|
)
|
||||||
active = models.BooleanField(default=True)
|
active = models.BooleanField(default=True)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"{self.name} ({self.email})"
|
return f"{self.name} ({self.email})"
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('generic:supervisor_detail', kwargs={'pk': self.pk})
|
return reverse("generic:supervisor_detail", kwargs={"pk": self.pk})
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
self.email = self.email.strip()
|
self.email = self.email.strip()
|
||||||
@@ -873,7 +1132,7 @@ def create_user_profile(sender, instance, created, **kwargs):
|
|||||||
if created:
|
if created:
|
||||||
UserProfile.objects.create(user=instance)
|
UserProfile.objects.create(user=instance)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=User)
|
@receiver(post_save, sender=User)
|
||||||
def save_user_profile(sender, instance, **kwargs):
|
def save_user_profile(sender, instance, **kwargs):
|
||||||
instance.userprofile.save()
|
instance.userprofile.save()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.1.4 on 2023-06-12 09:55
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('longs', '0069_alter_long_feedback'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='longseries',
|
||||||
|
name='open_access',
|
||||||
|
field=models.BooleanField(default=True, help_text='If a series should be freely available to browse'),
|
||||||
|
),
|
||||||
|
]
|
||||||
+3
-180
@@ -32,13 +32,12 @@ from generic.models import (
|
|||||||
CidUserGroup,
|
CidUserGroup,
|
||||||
ExamUserStatus,
|
ExamUserStatus,
|
||||||
Examination,
|
Examination,
|
||||||
# Condition,
|
|
||||||
# Sign,
|
|
||||||
ExamBase,
|
ExamBase,
|
||||||
Plane,
|
Plane,
|
||||||
Contrast,
|
Contrast,
|
||||||
QuestionBase,
|
QuestionBase,
|
||||||
QuestionNote,
|
QuestionNote,
|
||||||
|
SeriesBase,
|
||||||
UserAnswerBase,
|
UserAnswerBase,
|
||||||
UserUserGroup,
|
UserUserGroup,
|
||||||
)
|
)
|
||||||
@@ -70,14 +69,6 @@ def image_directory_path(instance, filename):
|
|||||||
return "longs/picture/{0}".format(filename)
|
return "longs/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
|
@reversion.register
|
||||||
class Long(QuestionBase):
|
class Long(QuestionBase):
|
||||||
@@ -380,7 +371,7 @@ class LongSeriesImage(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
@reversion.register
|
@reversion.register
|
||||||
class LongSeries(models.Model):
|
class LongSeries(SeriesBase):
|
||||||
modality = models.ForeignKey(
|
modality = models.ForeignKey(
|
||||||
Modality, related_name="series_modality", on_delete=models.SET_NULL, null=True
|
Modality, related_name="series_modality", on_delete=models.SET_NULL, null=True
|
||||||
)
|
)
|
||||||
@@ -407,16 +398,6 @@ class LongSeries(models.Model):
|
|||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
# long = models.ManyToManyField(
|
|
||||||
# "Long",
|
|
||||||
# help_text="The question(s) this series should be associated with",
|
|
||||||
# related_name="series",
|
|
||||||
# blank=True,
|
|
||||||
# )
|
|
||||||
description = models.TextField(
|
|
||||||
blank=True,
|
|
||||||
help_text="Description of stack, for admin organisation, will not be visible when taking",
|
|
||||||
)
|
|
||||||
|
|
||||||
author = models.ManyToManyField(
|
author = models.ManyToManyField(
|
||||||
settings.AUTH_USER_MODEL,
|
settings.AUTH_USER_MODEL,
|
||||||
@@ -424,169 +405,11 @@ class LongSeries(models.Model):
|
|||||||
related_name="long_series",
|
related_name="long_series",
|
||||||
)
|
)
|
||||||
|
|
||||||
open_access = models.BooleanField(
|
|
||||||
help_text="If a question should be freely available to browse", default=True
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
# if self.long:
|
|
||||||
# long_id = ", ".format([long.pk for long in self.long.all()])
|
|
||||||
# # long_id = self.long.pk
|
|
||||||
# else:
|
|
||||||
# long_id = "None"
|
|
||||||
return "{}/{} : {}".format(self.pk, self.get_examination(), self.description)
|
return "{}/{} : {}".format(self.pk, self.get_examination(), self.description)
|
||||||
|
|
||||||
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.long:
|
|
||||||
authors = [i for i in self.long.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):
|
def get_absolute_url(self):
|
||||||
return reverse("longs:long_series_detail", kwargs={"pk": self.pk})
|
return reverse("longs:series_detail", kwargs={"pk": self.pk})
|
||||||
|
|
||||||
def get_image_urls(self, feedback=False):
|
|
||||||
images = [f"{REMOTE_URL}{i.image.url}" for i in self.images.all()]
|
|
||||||
|
|
||||||
return ",".join(images)
|
|
||||||
|
|
||||||
def get_image_url_array(self, feedback=False):
|
|
||||||
images = [f"{REMOTE_URL}{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
|
|
||||||
|
|
||||||
|
|
||||||
# class LongImage(models.Model):
|
|
||||||
# long = models.ForeignKey(Long,
|
|
||||||
# related_name="images",
|
|
||||||
# on_delete=models.CASCADE)
|
|
||||||
# image = models.ImageField(upload_to=image_directory_path, storage=image_storage)
|
|
||||||
#
|
|
||||||
# feedback_image = models.BooleanField(default=False)
|
|
||||||
#
|
|
||||||
# def image_tag(self):
|
|
||||||
# if self.image:
|
|
||||||
# return mark_safe(
|
|
||||||
# '<img src="{}" class="admin-long-image" /><span class="admin-long-image-info">Click and hold to zoom<span>'
|
|
||||||
# .format(self.image.url))
|
|
||||||
# else:
|
|
||||||
# return ""
|
|
||||||
#
|
|
||||||
# image_tag.short_description = 'Image'
|
|
||||||
|
|
||||||
|
|
||||||
class LongCreationDefault(models.Model):
|
class LongCreationDefault(models.Model):
|
||||||
|
|||||||
Reference in New Issue
Block a user