start setting up shorts

This commit is contained in:
Ross
2025-04-07 09:12:30 +01:00
parent bcc449eb34
commit afa1379ce4
26 changed files with 2896 additions and 41 deletions
+401
View File
@@ -0,0 +1,401 @@
from collections import defaultdict
from django.db import models
from django.urls import reverse
from django.conf import settings
from rad.settings import REMOTE_URL
from django.utils.translation import gettext_lazy as _
import dicognito
import json
# Create your models here.
from generic.models import CidUser, CidUserGroup, ExamBase, ExamCollection, ExamUserStatus, QuestionBase, UserAnswerBase, UserUserGroup
from django.contrib.contenttypes.fields import GenericRelation
from django.core.validators import MaxValueValidator, MinValueValidator
from django.utils.html import mark_safe
from rapids.models import Abnormality, Examination, Region
from helpers.images import get_image_hash, image_as_base64
def image_directory_path(instance, filename):
# return u"{0}".format(filename)
return "shorts/picture/{0}".format(filename)
class Question(QuestionBase):
"""
"""
history = models.TextField(null=True, blank=True,
help_text="Single line history for the question, e.g. Age 14, male. Referral from ED. History: Painful wrist after falling off skateboard.")
NONE = "NONE"
LEFT = "LEFT"
RIGHT = "RIGHT"
BILATERAL = "BILAT"
LATERALITY_CHOICES = (
(NONE, "None"),
(LEFT, "Left"),
(RIGHT, "Right"),
(BILATERAL, "Bilateral"),
)
abnormality = models.ManyToManyField(
Abnormality,
blank=True,
help_text="The abnormality (laterality and region independent). Used for categorisation but does not affect the answer",
)
region = models.ManyToManyField(
Region,
blank=True,
help_text="Region of the abnormality (laterality independent)",
)
examination = models.ManyToManyField(
Examination, help_text="Name of the (primary) examination"
)
laterality = models.CharField(
max_length=20,
choices=LATERALITY_CHOICES,
default=NONE,
help_text="Applies to the answer, not the examination",
)
author = models.ManyToManyField(
settings.AUTH_USER_MODEL,
blank=True,
help_text="Author of question",
related_name="shorts_authored_questions",
)
def get_app_name(self):
return "shorts"
def get_absolute_url(self):
return reverse("shorts:question_detail", kwargs={"pk": self.pk})
def get_images(self, feedback=False):
qs = self.images.all()
if not feedback:
images = [i.image for i in qs if not i.feedback_image]
else:
images = [i.image for i in qs]
return images
def get_image_urls(self):
return ",".join([f"{REMOTE_URL}{i.url}" for i in self.get_images()])
def get_image_url_array(self):
return json.dumps([f"{REMOTE_URL}{i.url}" for i in self.get_images()])
# def GetNonFeedbackQuestionImages(self):
# return self.get_images()
def get_image_annotations(self):
return json.dumps(
[i.image_annotations for i in self.images.all() if not i.feedback_image]
)
def get_laterality_string(self):
if self.laterality == self.NONE:
s = ""
elif self.laterality == self.BILATERAL:
s = "bilateral "
elif self.laterality == self.RIGHT:
s = "right "
elif self.laterality == self.LEFT:
s = "left "
return s
def anonymise_images(self):
anonymizer = dicognito.anonymizer.Anonymizer()
for image in self.images.all():
file_path = os.path.join(settings.MEDIA_ROOT, 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 QuestionImage(models.Model):
question = models.ForeignKey(Question, related_name="images", on_delete=models.CASCADE)
image = models.FileField(upload_to=image_directory_path)
image_annotations = models.TextField(
blank=True,
null=True,
help_text="Stores a JSON representation of annotations to be applied by cornerstonetools",
)
feedback_image = models.BooleanField(default=False)
filename = models.CharField(
max_length=255,
null=True,
blank=True,
help_text="Name of the original file when uploaded",
)
description = models.CharField(max_length=255, null=True, blank=True)
image_md5_hash = models.CharField(max_length=32, null=True, blank=True)
is_dicom = models.BooleanField(default=False)
def image_tag(self):
if self.image:
return mark_safe(
'<img src="{}" class="admin-rapid-image" /><span class="admin-rapid-image-info">Click and hold to zoom<span>'.format(
self.image.url
)
)
else:
return ""
image_tag.short_description = "Image"
def save(self, *args, **kwargs):
"""Override save method to add image hash"""
if self.image:
image_hash, is_dicom = get_image_hash(self.image, hash_type="md5", direct_pixel_data=False)
self.is_dicom = is_dicom
self.image_md5_hash = image_hash
# Hack for tests
if image_hash != "12345ABCD":
super().save(*args, **kwargs) # Call the "real" save() method.
class SampleAnswer(models.Model):
question = models.ForeignKey(
Question, related_name="sample_answers", on_delete=models.CASCADE
)
answer = models.TextField()
score = models.IntegerField(default=0, blank=True, validators=[MinValueValidator(0), MaxValueValidator(5)])
proposed = models.BooleanField(default=False)
def __str__(self):
return self.answer
class ExamQuestionDetail(models.Model):
exam = models.ForeignKey("Exam", on_delete=models.CASCADE)
question = models.ForeignKey(Question, on_delete=models.CASCADE)
sort_order = models.IntegerField(default=1000)
class Meta:
ordering = ("sort_order",)
class Exam(ExamBase):
app_name = "shorts"
exam_questions = models.ManyToManyField(Question, through=ExamQuestionDetail, related_name="exams")
time_limit = models.IntegerField(
help_text="Exam time limit (in seconds). Default is 2 hours",
default=2 * 60 * 60,
)
author = models.ManyToManyField(
settings.AUTH_USER_MODEL,
blank=True,
help_text="Author of exam",
related_name="shorts_exam_author",
)
markers = models.ManyToManyField(
settings.AUTH_USER_MODEL,
blank=True,
help_text="Authorised markers for the exam",
related_name="shorts_exam_markers",
)
valid_cid_users = models.ManyToManyField(
CidUser, blank=True, related_name="shorts_exams"
)
valid_user_users = models.ManyToManyField(
settings.AUTH_USER_MODEL, blank=True, related_name="user_shorts_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.",
related_name="shorts_cid_user_groups",
)
user_user_groups = models.ManyToManyField(
UserUserGroup,
blank=True,
help_text="These groups define which candidates are able to be added to the exams/collection.",
related_name="shorts_user_user_groups",
)
exam_user_status = GenericRelation(ExamUserStatus, related_query_name="shorts_exams")
cid_user_exam = GenericRelation("generic.CidUserExam", related_query_name="shorts_exams")
examcollection = models.ForeignKey(ExamCollection, blank=True, null=True, on_delete=models.SET_NULL, related_name="shorts_exams")
def get_app_name(self):
return "shorts"
def get_exam_json(self, based=True):
# TODO
return {}
questions = self.get_questions()
exam_questions = defaultdict(dict)
exam_order = []
q: Rapid
for q in questions:
exam_order.append(q.id)
# Loop through rapidimage associations
images = []
feedback_images = []
image_titles = []
for i in q.images.all():
if i.feedback_image == True:
if based:
feedback_images.append(image_as_base64(i.image))
else:
feedback_images.append(
"{}/{}".format(settings.REMOTE_URL, i.image.url)
)
else:
if based:
images.append(image_as_base64(i.image))
else:
images.append("{}/{}".format(settings.REMOTE_URL, i.image.url))
if i.description:
image_titles.append(i.description)
else:
image_titles.append("")
exam_questions[q.id] = {
"images": images,
# "feedback_image": [],
# "annotations": [str(q.image_annotations)],
"type": "rapid",
}
if not self.exam_mode:
exam_questions[q.id]["normal"] = q.normal
exam_questions[q.id]["feedback_images"] = feedback_images
exam_questions[q.id]["answers"] = list(
q.get_correct_unstripped_answers()
)
if self.include_history and q.history:
exam_questions[q.id]["history"] = q.history
if any(image_titles):
exam_questions[q.id]["image_titles"] = image_titles
# if feedback_images:
# exam_questions[q.id]["feedback_image"] = feedback_images
exam_json = {
"eid": "rapid/{}".format(self.id),
"cached": False,
"exam_type": "rapid",
"exam_name": self.name,
"exam_mode": self.exam_mode,
"exam_order": exam_order,
"questions": exam_questions,
}
if self.time_limit:
exam_json["exam_time"] = self.time_limit
return exam_json
def get_absolute_url(self):
return reverse("shorts:exam_overview", kwargs={"pk": self.pk})
class UserAnswer(UserAnswerBase):
"""User answers by candidate"""
app_name = "shorts"
question = models.ForeignKey(
Question, related_name="cid_user_answers", on_delete=models.CASCADE
)
answer = models.TextField(blank=True)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
blank=True,
null=True,
related_name="user_shorts_user_answers",
)
# Each user answer is associated with a particular exam
exam = models.ForeignKey(
Exam, related_name="cid_user_answers", on_delete=models.CASCADE, null=True
)
score = models.IntegerField(default=0, blank=True, validators=[MinValueValidator(0), MaxValueValidator(5)])
class CallStateOptions(models.TextChoices):
CORRECT = "C", _("Correct Call")
OVERCALL = "O", _("Overcall")
UNDERCALL = "U", _("Undercall")
INCORRECTCALL = "W", _("Incorrect Call")
PARTIALCALL = "P", _("Partial Call")
callstate = models.CharField(
max_length=1, choices=CallStateOptions.choices, blank=True, null=True
)
def __str__(self):
name = self.get_candidate_name()
try:
exam = self.exam
except (Exam.DoesNotExist, KeyError) as e:
exam = "None"
return "{}/{}/{}: {}".format(
exam, name, self.question.get_primary_answer(), self.answer
)
def save(self, *args, **kwargs):
self.clean()
return super(UserAnswer, self).save(*args, **kwargs)
def clean(self):
if self.answer:
self.answer = self.answer.strip()
else:
self.answer = ""
# def get_compare_string(self):
# # strip here should be unneccasry (providing clean is now working)
# s = self.answer.lower().strip()
# s = s.translate(str.maketrans('', '', string.punctuation))
# return s
def get_answer(self):
return self.answer
def get_answer_callstate(self):
if not self.callstate:
return ""
return self.callstate.label
def get_answer_score(self, cached=True):
return self.score