furher shorts improvements

This commit is contained in:
Ross
2025-04-07 11:34:10 +01:00
parent afa1379ce4
commit d387e32777
16 changed files with 795 additions and 111 deletions
+109 -25
View File
@@ -1,14 +1,28 @@
from collections import defaultdict
import os
from django.db import models
from django.urls import reverse
from django.conf import settings
import pydicom
from atlas.models import Finding, Structure, Condition
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 generic.models import (
CidUser,
CidUserGroup,
ExamBase,
ExamCollection,
ExamUserStatus,
FindingBase,
QuestionBase,
UserAnswerBase,
UserUserGroup,
get_pretty_json,
)
from django.contrib.contenttypes.fields import GenericRelation
from django.core.validators import MaxValueValidator, MinValueValidator
from django.utils.html import mark_safe
@@ -17,16 +31,20 @@ 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.")
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"
@@ -60,6 +78,12 @@ class Question(QuestionBase):
help_text="Applies to the answer, not the examination",
)
marking_guidance = models.TextField(
null=True,
blank=True,
help_text="Marking guidance for the question. This is not shown to the candidate but is used by the marker to help them mark the question.",
)
author = models.ManyToManyField(
settings.AUTH_USER_MODEL,
blank=True,
@@ -70,6 +94,9 @@ class Question(QuestionBase):
def get_app_name(self):
return "shorts"
def get_findings_url(self):
return reverse("shorts:question_findings", kwargs={"question_id": self.pk})
def get_absolute_url(self):
return reverse("shorts:question_detail", kwargs={"pk": self.pk})
@@ -92,10 +119,10 @@ class Question(QuestionBase):
# 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_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:
@@ -122,15 +149,31 @@ class Question(QuestionBase):
except pydicom.errors.InvalidDicomError:
pass
def check_user_can_edit(self, user):
if user.is_superuser:
return True
if self.author.filter(id=user.id).exists():
return True
if user.groups.filter(name="shorts_checker").exists():
return True
return False
class QuestionImage(models.Model):
question = models.ForeignKey(Question, related_name="images", on_delete=models.CASCADE)
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",
)
# Image annotations are stored as findings in shorts cases
#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)
@@ -162,7 +205,9 @@ class QuestionImage(models.Model):
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)
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
@@ -170,19 +215,29 @@ class QuestionImage(models.Model):
if image_hash != "12345ABCD":
super().save(*args, **kwargs) # Call the "real" save() method.
class SampleAnswer(models.Model):
"""Model that defines sample answers for the question.
These are used to aid marking and for feedback purposes
"""
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)])
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)
@@ -192,10 +247,13 @@ class ExamQuestionDetail(models.Model):
class Meta:
ordering = ("sort_order",)
class Exam(ExamBase):
app_name = "shorts"
exam_questions = models.ManyToManyField(Question, through=ExamQuestionDetail, related_name="exams")
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",
@@ -238,11 +296,20 @@ class Exam(ExamBase):
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")
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"
@@ -325,6 +392,7 @@ class Exam(ExamBase):
def get_absolute_url(self):
return reverse("shorts:exam_overview", kwargs={"pk": self.pk})
class UserAnswer(UserAnswerBase):
"""User answers by candidate"""
@@ -349,7 +417,9 @@ class UserAnswer(UserAnswerBase):
Exam, related_name="cid_user_answers", on_delete=models.CASCADE, null=True
)
score = models.IntegerField(default=0, blank=True, validators=[MinValueValidator(0), MaxValueValidator(5)])
score = models.IntegerField(
default=0, blank=True, validators=[MinValueValidator(0), MaxValueValidator(5)]
)
class CallStateOptions(models.TextChoices):
CORRECT = "C", _("Correct Call")
@@ -398,4 +468,18 @@ class UserAnswer(UserAnswerBase):
return self.callstate.label
def get_answer_score(self, cached=True):
return self.score
return self.score
class QuestionFinding(FindingBase):
question = models.ForeignKey(
Question, related_name="findings", on_delete=models.SET_NULL, null=True
)
findings = models.ManyToManyField(Finding, blank=True)
structures = models.ManyToManyField(Structure, blank=True)
conditions = models.ManyToManyField(Condition, blank=True)
def __str__(self) -> str:
findings = self.findings.all().values_list("name")
return f"Findings: {self.question.id}/{findings}/{self.description}"