import json 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/ 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_user_answer_string(self, exam_pk=None): unmarked_answers = self.get_unmarked_user_answers(exam_pk) if not unmarked_answers: return "No answers to mark" return format_html( "{} answer unmarked: {}".format( len(unmarked_answers), ", ".join(unmarked_answers) ) ) def get_unmarked_user_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_user_answer_count(self, exam_pk=None): return len(self.get_unmarked_user_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 def get_image_url(self): return "https://www.penracourses.org.uk{}".format(self.image.url) def get_image_url_array(self): return json.dumps(["https://www.penracourses.org.uk{}".format(self.image.url)]) def get_image_annotations(self): return json.dumps([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": 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