399 lines
11 KiB
Python
399 lines
11 KiB
Python
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
|
|
|
|
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",
|
|
)
|
|
|
|
class Meta:
|
|
permissions = ()
|
|
|
|
def __str__(self):
|
|
# Get first answer
|
|
return "{} [{}, {}]".format(
|
|
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):
|
|
unmarked_answers = self.get_unmarked_answers()
|
|
|
|
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):
|
|
user_answers = set(
|
|
[i.answer_compare for i in self.cid_user_answers.all()]
|
|
)
|
|
unmarked_answers = user_answers - self.get_marked_answers()
|
|
|
|
return unmarked_answers
|
|
|
|
def get_unmarked_answer_count(self):
|
|
return len(self.get_unmarked_answers())
|
|
|
|
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
|
|
|
|
|
|
|
|
@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": "{}".format(q.description),
|
|
"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
|
|
|
|
|
|
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_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
|
|
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 |