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 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/ 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 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.GetPrimaryAnswer(), 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 GetPrimaryAnswer(self): if len(self.answers.all()) > 0: return self.answers.all().first().answer else: return "None yet..." def GetExams(self): e = self.exams.all().values_list("name", flat=True) exams = ", ".join(e) return exams def GetUnmarkedAnswersString(self): unmarked_answers = self.GetUnmarkedAnswers() if not unmarked_answers: return "No answers to mark" return format_html( "{} answer unmarked: {}".format( len(unmarked_answers), ", ".join(unmarked_answers) ) ) def GetUnmarkedAnswers(self): user_answers = set( [i.answer_compare for i in self.cid_user_answers.all()] ) unmarked_answers = user_answers - self.GetMarkedAnswers() return unmarked_answers def GetUnmarkedAnswerCount(self): return len(self.GetUnmarkedAnswers()) def GetMarkedAnswers(self): return set( [ i.answer_compare for i in self.answers.all() 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 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 ) 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() 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.GetPrimaryAnswer(), 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