furher shorts improvements
This commit is contained in:
+109
-25
@@ -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}"
|
||||
|
||||
Reference in New Issue
Block a user