from django.db import models from django.utils import timezone import tagulous import tagulous.models 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 django.utils.html import mark_safe from sortedm2m.fields import SortedManyToManyField import string image_storage = FileSystemStorage( # Physical file location ROOT location=u"{0}/rapids/".format(settings.MEDIA_ROOT), # Url for file base_url=u"{0}rapids/".format(settings.MEDIA_URL), ) def image_directory_path(instance, filename): return u"{0}".format(filename) return u"picture/{0}".format(filename) class Abnormality(models.Model): name = models.CharField(max_length=200, unique=True, help_text="Primary abnormality on the film(s)") def __str__(self): return self.name class Meta: ordering = ('name', ) class Region(models.Model): name = models.CharField( max_length=200, unique=True, help_text="Region of the abnormality (not including lateralitiy)", blank=True) def __str__(self): return self.name class Meta: ordering = ('name', ) class Examination(models.Model): examination = models.CharField(max_length=200) def __str__(self): return self.examination class Meta: ordering = ('examination', ) class Site(models.Model): site = models.CharField(max_length=200) initials = models.CharField(max_length=200) def __str__(self): return self.site class Condition(tagulous.models.TagModel): pass class Sign(tagulous.models.TagModel): pass class Answer(models.Model): question = models.ForeignKey( "Rapid", 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 class Rapid(models.Model): #author = models.ForeignKey('auth.User', on_delete=models.CASCADE) #image = models.ImageField() question = models.TextField(null=True, blank=True) feedback = models.TextField(null=True, blank=True) normal = models.BooleanField(default=False, help_text="Tick if true") NONE = "NONE" LEFT = "LEFT" RIGHT = "RIGHT" BILATERAL = "BILAT" LATERALITY_CHOICES = ( (NONE, "None"), (LEFT, "Left"), (RIGHT, "Right"), (BILATERAL, "Bilateral"), ) abnormality = models.ManyToManyField( Abnormality, blank=True, help_text="The abnormality (laterality and region independent). Used for categorisation but does not affect the answer", ) region = models.ManyToManyField( Region, blank=True, help_text="Region of the abnormality (laterality independent)") examination = models.ManyToManyField( Examination, help_text="Name of the (primary) examination") laterality = models.CharField( max_length=20, choices=LATERALITY_CHOICES, default=NONE, help_text="Applies to the answer, not the examination") condition = models.CharField(max_length=255, blank=True) #condition = tagulous.models.TagField( # to=Condition, # blank=True, # help_text= # "Associated condition. Will allow searching / filtering and tips / hints to be displayed. Conditions with spaces must be enclosed in quotes \"...\"" #) sign = models.CharField(max_length=255, blank=True) #sign = tagulous.models.TagField( # to=Sign, blank=True, help_text='Radiological signs in the question') DEFAULT_SITE_ID = 1 site = models.ManyToManyField( Site, blank=True, default=DEFAULT_SITE_ID, help_text= "If we know the source of the image") verified = models.BooleanField(default=False) created_date = models.DateTimeField(default=timezone.now) published_date = models.DateTimeField(blank=True, null=True) author = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True, help_text='Author of question', related_name="rapid_authored_questions") scrapped = models.BooleanField( default=False, help_text='Question has been scrapped and will not be shown') open_access = models.BooleanField( help_text="If an question should be freely available to browse", default=True ) def get_absolute_url(self): return reverse('rapids:rapid_detail', kwargs={'pk': self.pk}) def get_authors(self): """Returns a comma seperated text list of authors""" authors = ", ".join([i.username for i in self.author.all()]) return authors def get_regions(self): """Returns a comma seperated text list of regions""" regions = ", ".join([i.name for i in self.region.all()]) return regions def get_abnormalities(self): """Returns a comma seperated text list of regions""" abnormalities = ", ".join([i.name for i in self.abnormality.all()]) return abnormalities def get_examinations(self): """Returns a comma seperated text list of regions""" examinations = ", ".join([i.examination for i in self.examination.all()]) return examinations def __str__(self): n = "Normal" if not self.normal: #n = self.answers.first() n = "{} / {}".format(self.abnormality.first(), self.region.first()) exams = self.get_examinations() lat = "" if self.laterality != "NONE": lat = self.laterality return "{} / {} {}".format(exams, lat, n) def GetPrimaryAnswer(self): if len(self.answers.all()) > 0: return self.answers.all().first().answer elif self.normal: return "Normal" 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): # If normal no answers to mark if self.normal: return [] user_answers = set( [i.answer_compare for i in self.cid_user_answers.all() if i.normal == False] ) 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 GetImages(self, feedback=False): qs = self.images.all() if not feedback: images = [i.image for i in qs if not i.feedback_image] else: images = [i.image for i in qs] return images def GetImageUrls(self): return ",".join(["http://penracourses.org.uk{}".format(i.url) for i in self.GetImages()]) #def GetNonFeedbackQuestionImages(self): #return self.GetImages() class RapidImage(models.Model): rapid = models.ForeignKey(Rapid, related_name="images", on_delete=models.CASCADE) image = models.ImageField(upload_to=image_directory_path, storage=image_storage) feedback_image = models.BooleanField(default=False) def image_tag(self): if self.image: return mark_safe( 'Click and hold to zoom' .format(self.image.url)) else: return "" image_tag.short_description = 'Image' class Note(models.Model): rapid = models.ForeignKey(Rapid, related_name="rapid_notes", on_delete=models.CASCADE, null=True) #author = models.ForeignKey(User, author = models.ForeignKey( settings.AUTH_USER_MODEL, related_name="rapid_notes", on_delete=models.CASCADE) note = models.TextField() created_on = models.DateTimeField(auto_now_add=True) def get_absolute_url(self): self.pk = self.rapid_id return reverse('rapids:rapid_detail', kwargs={'pk': self.pk}) def __str__(self): return "{} [{}] {}".format(self.rapid, self.author, self.created_on) class RapidCreationDefault(models.Model): #author = models.OneToOneField(User, author = models.OneToOneField( settings.AUTH_USER_MODEL, related_name="rapid_default", on_delete=models.CASCADE) site = models.ManyToManyField( Site, blank=True, default=1, help_text="Default site to use when creating a new rapid") def get_absolute_url(self): return reverse('rapids:rapid_create') class Exam(models.Model): name = models.CharField(max_length=200) exam_questions = SortedManyToManyField( Rapid, related_name="exams", blank="true" ) active = models.BooleanField( help_text="If an exam should be available", default=True ) publish_results = models.BooleanField( help_text="If an exams results should be available", default=True ) recreate_json = models.BooleanField( help_text="If the json cache needs updating", default=False ) time_limit = models.IntegerField( help_text="Exam time limit (in seconds). Default is 2100 secondse (35 minutes)", default=2100 ) def __str__(self): return self.name def get_absolute_url(self): return reverse("rapids:exam_overview", kwargs={"pk": self.pk}) def get_exam_name(self): return self.name def get_json_url(self): return reverse("rapids:exam_json", args=(self.pk,)) def get_question_index(self, question): return list(self.exam_questions.all()).index(question) class CidUserAnswer(models.Model): """User answers by candidate""" question = models.ForeignKey( Rapid, related_name="cid_user_answers", on_delete=models.CASCADE ) # For rapids the answer can be normal in which case the below field is true normal = models.BooleanField(default=False) 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 # First step we check that the normal/abnormal states match if q.normal != self.normal: # If they don't match the answer is wrong (score is 0) return 0 # If both are normal full marks elif q.normal and self.normal: return 2 # Then compare answer strings (as per anatomy questions) 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