clone rapids app into longs
This commit is contained in:
+475
@@ -0,0 +1,475 @@
|
||||
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"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(
|
||||
"<span class='warn'>{} answer unmarked:</span> {}".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(
|
||||
'<img src="{}" class="admin-rapid-image" /><span class="admin-rapid-image-info">Click and hold to zoom<span>'
|
||||
.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
|
||||
Reference in New Issue
Block a user