Files
penracourses/anatomy/models.py
T
Ross 5be2b148ca .
2021-09-29 19:33:52 +01:00

475 lines
13 KiB
Python

from django.contrib.contenttypes.fields import GenericRelation
from django.db import models
from django.utils import timezone
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 sortedm2m.fields import SortedManyToManyField
import string
from generic.models import Examination, ExamBase, QuestionNote
from collections import defaultdict
from helpers.images import image_as_base64
import reversion
image_storage = FileSystemStorage(
# Physical file location ROOT
location=u"{0}/".format(settings.MEDIA_ROOT),
# Url for file
base_url=u"{0}/".format(settings.MEDIA_URL),
)
def image_directory_path(instance, filename):
# file will be uploaded to MEDIA_ROOT/anatomy/picture/<filename>
return u"picture/anatomy/{0}".format(filename)
class BodyPart(models.Model):
bodypart = models.CharField(max_length=200)
def __str__(self):
return self.bodypart
class Structure(models.Model):
structure = models.CharField(max_length=200)
def __str__(self):
return self.structure
class Region(models.Model):
region = models.CharField(max_length=200)
def __str__(self):
return self.region
class Modality(models.Model):
modality = models.CharField(max_length=200)
def __str__(self):
return self.modality
class QuestionType(models.Model):
text = models.CharField(max_length=400)
def __str__(self):
return self.text
@reversion.register
class AnatomyQuestion(models.Model):
# author = models.ForeignKey('auth.User', on_delete=models.CASCADE)
# image = models.ImageField()
# feedback = models.TextField(null=True)
question_type = models.ForeignKey(
QuestionType, on_delete=models.SET_NULL, null=True, default=1
)
image = models.ImageField(upload_to=image_directory_path)
image_annotations = models.TextField(
blank=True,
help_text="Stores a JSON representation of annotations to be applied by cornerstonetools",
)
description = models.CharField(
max_length=400,
help_text="Short description of the image e.g. 'Sagittal CT Chest, Abdomen & Pelvis', will be displayed as the title.",
)
examination = models.ForeignKey(
Examination, on_delete=models.SET_NULL, null=True, blank=True
)
modality = models.ForeignKey(
Modality,
on_delete=models.SET_NULL,
null=True,
help_text="Modality of the image",
)
region = models.ForeignKey(
Region,
on_delete=models.SET_NULL,
null=True,
blank=True,
help_text="Region the image covers",
)
body_part = models.ForeignKey(
BodyPart, on_delete=models.SET_NULL, null=True, blank=True
)
structure = models.ForeignKey(
Structure, on_delete=models.SET_NULL, null=True, blank=True
)
created_date = models.DateTimeField(default=timezone.now)
open_access = models.BooleanField(
help_text="If a question should be freely available to browse", default=True
)
author = models.ManyToManyField(
settings.AUTH_USER_MODEL,
blank=True,
help_text="Author(s) of question",
related_name="anatomy_authored_questions",
)
notes = GenericRelation(QuestionNote)
class Meta:
permissions = ()
def __str__(self):
# Get first answer
return "{}/{}: {} [{}, {}]".format(
self.pk, self.question_type, self.get_primary_answer(), self.modality, self.structure
)
# Get associated exams
e = self.exams.all().values_list("name", flat=True)
exams = ", ".join(e)
if a is None:
return "({})".format(exams)
else:
return "{} ({}) [{}, {}]".format(
a.answer, exams, self.modality, self.structure
)
def get_absolute_url(self):
return reverse("anatomy:question_detail", kwargs={"pk": self.pk})
def get_primary_answer(self):
if len(self.answers.filter(proposed=False)) > 0:
return self.answers.filter(proposed=False).first().answer
else:
return "None yet..."
def get_exams(self):
e = self.exams.all().values_list("name", flat=True)
exams = ", ".join(e)
return exams
def get_unmarked_answer_string(self, exam_pk=None):
unmarked_answers = self.get_unmarked_answers(exam_pk)
if not unmarked_answers:
return "No answers to mark"
return format_html(
"<span class='warn'>{} answer unmarked:</span> {}".format(
len(unmarked_answers), ", ".join(unmarked_answers)
)
)
def get_unmarked_answers(self, exam_pk=None):
if exam_pk is None:
user_answers = set([i.answer_compare for i in self.cid_user_answers.all()])
else:
user_answers = set(
[
i.answer_compare
for i in self.cid_user_answers.filter(exam__id=exam_pk)
]
)
unmarked_answers = user_answers - self.get_marked_answers()
return unmarked_answers
def get_unmarked_answer_count(self, exam_pk=None):
return len(self.get_unmarked_answers(exam_pk))
def get_marked_answers(self):
return set(
[
i.answer_compare
for i in self.answers.filter(proposed=False)
if i.status != i.MarkOptions.UNMARKED
]
)
# correct_answers = set(
# [i.answer.get_compare_string() for i in self.answers.all()]
# )
# half_mark_answers = set(
# [i.answer.get_compare_string() for i in self.half_mark_answers.all()]
# )
# incorrect_answers = set(
# [i.answer.get_compare_string() for i in self.incorrect_answers.all()]
# )
# marked_answers = correct_answers | half_mark_answers | incorrect_answers
# return marked_answers
def get_annotations(self):
return self.image_annotations
def get_title(self):
return "{}".format(self.description)
def get_correct_unstripped_answers(self):
return set(
[
str(i)
for i in self.answers.filter(proposed=False)
if i.status == i.MarkOptions.CORRECT
]
)
def get_question_json(self, based=True):
"""Returns a json representation of the question"""
# Loop through rapidimage associations
images = []
annotations = []
feedback_images = []
if based:
images.append(image_as_base64(self.image))
else:
images.append("{}/{}".format(settings.REMOTE_URL, self.image.url))
if self.image_annotations:
annotations.append(str(self.image_annotations))
json = {
"images": images,
#"feedback_image": [],
"annotations": annotations,
"type": "anatomy",
"title": self.get_title(),
"question": str(self.question_type),
}
json["answers"] = list(self.get_correct_unstripped_answers())
return json
@reversion.register
class Answer(models.Model):
question = models.ForeignKey(
AnatomyQuestion, related_name="answers", on_delete=models.CASCADE
)
answer = models.TextField(max_length=500)
answer_compare = models.TextField(max_length=500)
class MarkOptions(models.TextChoices):
UNMARKED = "", _("Unmarked")
INCORRECT = "0", _("Incorrect")
HALF_MARK = "1", _("Half mark")
CORRECT = "2", _("Correct")
status = models.CharField(
max_length=1, choices=MarkOptions.choices, default=MarkOptions.UNMARKED
)
proposed = models.BooleanField(default=False)
def __str__(self):
return self.answer
def save(self, *args, **kwargs):
self.clean()
return super(Answer, self).save(*args, **kwargs)
def clean(self):
if self.answer:
self.answer = self.answer.strip()
s = self.answer.lower()
s = s.translate(str.maketrans("", "", string.punctuation))
self.answer_compare = s
# def get_compare_string(self):
# s = self.answer.lower().strip()
# s = s.translate(str.maketrans('', '', string.punctuation))
# return s
# class HalfMarkAnswers(models.Model):
# question = models.ForeignKey(AnatomyQuestion,
# related_name="half_mark_answers",
# on_delete=models.CASCADE)
# answer = models.CharField(max_length=500)
#
# def __str__(self):
# return self.answer
#
# def clean(self):
# if self.answer:
# self.answer.strip()
#
#
# class IncorrectAnswers(models.Model):
# question = models.ForeignKey(AnatomyQuestion,
# related_name="incorrect_answers",
# on_delete=models.CASCADE)
# answer = models.CharField(max_length=500)
#
# def __str__(self):
# return self.answer
#
# def clean(self):
# if self.answer:
# self.answer.strip()
@reversion.register
class Exam(ExamBase):
app_name = "anatomy"
exam_questions = SortedManyToManyField(
AnatomyQuestion, related_name="exams", blank="true"
)
time_limit = models.IntegerField(
help_text="Exam time limit (in seconds)", default=5400
)
author = models.ManyToManyField(
settings.AUTH_USER_MODEL,
blank=True,
help_text="Author of exam",
related_name="anatomy_exam_author",
)
def get_exam_json(self):
questions = self.exam_questions.all()
exam_questions = defaultdict(dict)
exam_order = []
for q in questions:
exam_order.append(q.id)
exam_questions[q.id] = {
"title": q.get_title(),
"question": str(q.question_type),
"images": [image_as_base64(q.image)],
"annotations": [str(q.image_annotations)],
"type": "anatomy",
}
exam_json = {
"eid": "anatomy/{}".format(self.id),
"cached": False,
"exam_type": "anatomy",
"exam_name": self.name,
"exam_mode": True,
"exam_order": exam_order,
"questions": exam_questions,
}
if self.time_limit:
exam_json["exam_time"] = self.time_limit
return exam_json
@reversion.register
class CidUserAnswer(models.Model):
"""User answers by candidate"""
question = models.ForeignKey(
AnatomyQuestion, related_name="cid_user_answers", on_delete=models.CASCADE
)
answer = models.TextField(max_length=500, blank=True)
answer_compare = models.TextField(max_length=500, blank=True)
cid = models.BigIntegerField(
blank=True,
null=True,
help_text="Candidate ID (limitied by BigIntegerField size)",
)
# Each user answer is associated with a particular exam
exam = models.ForeignKey(
Exam, related_name="cid_user_answers", on_delete=models.CASCADE, null=True
)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
def __str__(self):
try:
exam = self.exam
except (Exam.DoesNotExist, KeyError) as e:
exam = "None"
return "{}/{}/{}: {}".format(
exam, self.cid, self.question.get_primary_answer(), self.answer
)
def save(self, *args, **kwargs):
self.clean()
return super(CidUserAnswer, self).save(*args, **kwargs)
def clean(self):
if self.answer:
self.answer = self.answer.strip()
s = self.answer.lower()
s = s.translate(str.maketrans("", "", string.punctuation))
self.answer_compare = s
def get_absolute_url(self):
return reverse("anatomy:user_answer_view", kwargs={"pk": self.pk})
# 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_score(self):
q = self.question
ans = self.answer_compare
marked_ans = q.answers.filter(answer_compare__iexact=ans).first()
mark = "unmarked"
if marked_ans is not None:
if marked_ans.status == Answer.MarkOptions.CORRECT:
mark = 2
elif marked_ans.status == Answer.MarkOptions.HALF_MARK:
mark = 1
elif marked_ans.status == Answer.MarkOptions.INCORRECT:
mark = 0
return mark
if (
q.answers.filter(
answer_compare__iexact=ans, status=Answer.MarkOptions.CORRECT
).first()
is not None
):
mark = 2
elif (
q.answers.filter(
answer_compare__iexact=ans, status=Answer.MarkOptions.HALF_MARK
).first()
is not None
):
mark = 1
elif q.answers.filter(
answer_compare__iexact=ans, status=Answer.MarkOptions.INCORRECT
).first():
mark = 0
else:
mark = "unmarked"
return mark