This commit is contained in:
Ross
2021-12-11 22:21:12 +00:00
parent 98ff9f2d39
commit 3d9c019bdc
9 changed files with 221 additions and 110 deletions
@@ -0,0 +1,19 @@
# Generated by Django 3.2.8 on 2021-12-11 22:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('generic', '0015_ciduser_ciduserexam'),
('anatomy', '0053_anatomyquestion_structure'),
]
operations = [
migrations.AddField(
model_name='exam',
name='valid_users',
field=models.ManyToManyField(blank=True, related_name='anatomy_exams', to='generic.CidUser'),
),
]
+3 -1
View File
@@ -15,7 +15,7 @@ from sortedm2m.fields import SortedManyToManyField
import string
from generic.models import Examination, ExamBase, QuestionNote
from generic.models import CidUser, Examination, ExamBase, QuestionNote
from collections import defaultdict
from helpers.images import image_as_base64
@@ -354,6 +354,8 @@ class Exam(ExamBase):
related_name="anatomy_exam_author",
)
valid_users = models.ManyToManyField(CidUser, blank=True, related_name="anatomy_exams")
def get_exam_json(self, based=True):
questions = self.exam_questions.all()
+1
View File
@@ -26,4 +26,5 @@ urlpatterns = [
),
name="examination-autocomplete",
),
path("cids/manage/", views.create_examination, name="create_examination"),
]
+19
View File
@@ -0,0 +1,19 @@
# Generated by Django 3.2.8 on 2021-12-11 22:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('generic', '0015_ciduser_ciduserexam'),
('longs', '0054_alter_exam_open_access'),
]
operations = [
migrations.AddField(
model_name='exam',
name='valid_users',
field=models.ManyToManyField(blank=True, related_name='longs_exams', to='generic.CidUser'),
),
]
+3 -1
View File
@@ -28,7 +28,7 @@ from helpers.images import image_as_base64, pretty_print_dicom
from anatomy.models import Modality
from generic.models import Examination, Condition, Sign, ExamBase, Plane, Contrast, QuestionNote
from generic.models import CidUser, Examination, Condition, Sign, ExamBase, Plane, Contrast, QuestionNote
# from generic.models import Examination, Site, Condition, Sign
@@ -597,6 +597,8 @@ class Exam(ExamBase):
double_mark = models.BooleanField(default=True, help_text="Defines if an exam is expected to be double marked")
valid_users = models.ManyToManyField(CidUser, blank=True, related_name="longs_exams")
def get_exam_question_json(self, question_id):
q = get_object_or_404(Long, pk=question_id)
@@ -0,0 +1,19 @@
# Generated by Django 3.2.8 on 2021-12-11 22:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('generic', '0015_ciduser_ciduserexam'),
('rapids', '0038_alter_rapidimage_image_md5_hash'),
]
operations = [
migrations.AddField(
model_name='exam',
name='valid_users',
field=models.ManyToManyField(blank=True, related_name='rapids_exams', to='generic.CidUser'),
),
]
+136 -107
View File
@@ -24,7 +24,7 @@ from helpers.images import image_as_base64
from django.contrib.contenttypes.fields import GenericRelation
from generic.models import Site, Condition, Sign, ExamBase, QuestionNote
from generic.models import CidUser, Site, Condition, Sign, ExamBase, QuestionNote
import reversion
@@ -37,31 +37,33 @@ import pydicom.errors
image_storage = FileSystemStorage(
# Physical file location ROOT
location=u"{0}".format(settings.MEDIA_ROOT),
location="{0}".format(settings.MEDIA_ROOT),
# Url for file
base_url=u"{0}".format(settings.MEDIA_URL),
base_url="{0}".format(settings.MEDIA_URL),
)
def image_directory_path(instance, filename):
#return u"{0}".format(filename)
return u"rapids/picture/{0}".format(filename)
# return u"{0}".format(filename)
return "rapids/picture/{0}".format(filename)
def get_answer_compare(s):
s = s.strip().lower()
s = s.translate(str.maketrans('', '', string.punctuation.replace("#", "")))
s = s.translate(str.maketrans("", "", string.punctuation.replace("#", "")))
return s
class Abnormality(models.Model):
name = models.CharField(max_length=200,
unique=True,
help_text="Primary abnormality on the film(s)")
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', )
ordering = ("name",)
class Region(models.Model):
@@ -69,13 +71,14 @@ class Region(models.Model):
max_length=200,
unique=True,
help_text="Region of the abnormality (not including lateralitiy)",
blank=True)
blank=True,
)
def __str__(self):
return self.name
class Meta:
ordering = ('name', )
ordering = ("name",)
class Examination(models.Model):
@@ -85,7 +88,8 @@ class Examination(models.Model):
return self.examination
class Meta:
ordering = ('examination', )
ordering = ("examination",)
class Answer(models.Model):
question = models.ForeignKey(
@@ -119,8 +123,8 @@ class Answer(models.Model):
class Rapid(models.Model):
#author = models.ForeignKey('auth.User', on_delete=models.CASCADE)
#image = models.ImageField()
# 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)
@@ -146,25 +150,28 @@ class Rapid(models.Model):
region = models.ManyToManyField(
Region,
blank=True,
help_text="Region of the abnormality (laterality independent)")
help_text="Region of the abnormality (laterality independent)",
)
examination = models.ManyToManyField(
Examination, help_text="Name of the (primary) examination")
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 = tagulous.models.TagField(
help_text="Applies to the answer, not the examination",
)
# 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 = tagulous.models.TagField(
# )
# sign = tagulous.models.TagField(
# to=Sign, blank=True, help_text='Radiological signs in the question')
DEFAULT_SITE_ID = 1
#site = models.ManyToManyField(
# site = models.ManyToManyField(
# Site,
# related_name="site_rapid",
# blank=True,
@@ -174,14 +181,16 @@ class Rapid(models.Model):
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")
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')
default=False, help_text="Question has been scrapped and will not be shown"
)
open_access = models.BooleanField(
help_text="If a question should be freely available to browse", default=True
@@ -189,9 +198,8 @@ class Rapid(models.Model):
notes = GenericRelation(QuestionNote)
def get_absolute_url(self):
return reverse('rapids:question_detail', kwargs={'pk': self.pk})
return reverse("rapids:question_detail", kwargs={"pk": self.pk})
def get_authors(self):
"""Returns a comma seperated text list of authors"""
@@ -216,8 +224,10 @@ class Rapid(models.Model):
def __str__(self):
n = "Normal"
if not self.normal:
#n = self.answers.first()
n = "{}/{} : {}".format(self.pk, self.abnormality.first(), self.region.first())
# n = self.answers.first()
n = "{}/{} : {}".format(
self.pk, self.abnormality.first(), self.region.first()
)
exams = self.get_examinations()
lat = ""
if self.laterality != "NONE":
@@ -227,8 +237,17 @@ class Rapid(models.Model):
def get_primary_answer(self):
if self.normal:
return "Normal"
elif self.answers.filter(proposed=False, status=Answer.MarkOptions.CORRECT).count() > 0:
return self.answers.filter(proposed=False, status=Answer.MarkOptions.CORRECT).first().answer
elif (
self.answers.filter(
proposed=False, status=Answer.MarkOptions.CORRECT
).count()
> 0
):
return (
self.answers.filter(proposed=False, status=Answer.MarkOptions.CORRECT)
.first()
.answer
)
else:
return "None yet..."
@@ -261,11 +280,15 @@ class Rapid(models.Model):
marked_answers = self.get_marked_answers()
unmarked_answers = set(
[i for i in user_answer_queryset if i.normal == False and i.answer_compare not in marked_answers]
[
i
for i in user_answer_queryset
if i.normal == False and i.answer_compare not in marked_answers
]
)
return [i.answer_compare for i in unmarked_answers]
# Marker code below is not used
if marker is None:
return [i.answer_compare for i in unmarked_answers]
@@ -275,7 +298,7 @@ class Rapid(models.Model):
for answer in unmarked_answers:
if answer.mark.filter(marker=marker).count() < 1:
marker_unmarked.append(answer.answer_compare)
return marker_unmarked
def get_unmarked_user_answer_count(self, exam_pk=None, marker=None):
@@ -293,19 +316,12 @@ class Rapid(models.Model):
if not include_normal:
queryset = queryset.filter(normal=False)
user_answers = set(
[i.answer_compare for i in queryset]
)
user_answers = set([i.answer_compare for i in queryset])
return user_answers
def get_compare_answers(self):
return set(
[
i.answer_compare
for i in self.answers.filter()
]
)
return set([i.answer_compare for i in self.answers.filter()])
def get_marked_answers(self):
return set(
@@ -352,11 +368,14 @@ class Rapid(models.Model):
def get_image_url_array(self):
return json.dumps([f"{REMOTE_URL}{i.url}" for i in self.get_images()])
#def GetNonFeedbackQuestionImages(self):
#return self.get_images()
# def GetNonFeedbackQuestionImages(self):
# return self.get_images()
def get_image_annotations(self):
return json.dumps([i.image_annotations for i in self.images.all() if not i.feedback_image])
return json.dumps(
[i.image_annotations for i in self.images.all() if not i.feedback_image]
)
def get_laterality_string(self):
if self.laterality == self.NONE:
@@ -382,10 +401,12 @@ class Rapid(models.Model):
compare_answers = self.get_compare_answers()
new_answers = [a for a in answers if get_answer_compare(a) not in compare_answers]
new_answers = [
a for a in answers if get_answer_compare(a) not in compare_answers
]
return new_answers
def get_question_json(self, based=True):
"""Returns json"""
@@ -399,7 +420,9 @@ class Rapid(models.Model):
if based:
feedback_images.append(image_as_base64(i.image))
else:
feedback_images.append("{}/{}".format(settings.REMOTE_URL, i.image.url))
feedback_images.append(
"{}/{}".format(settings.REMOTE_URL, i.image.url)
)
feedback_images.append("{}/{}".format(settings.REMOTE_URL, i.image.url))
else:
if based:
@@ -407,11 +430,9 @@ class Rapid(models.Model):
else:
images.append("{}/{}".format(settings.REMOTE_URL, i.image.url))
json = {
"images": images,
#"feedback_image": [],
# "feedback_image": [],
"annotations": annotations,
"type": "rapid",
}
@@ -423,16 +444,15 @@ class Rapid(models.Model):
return json
#reversion.register(Rapid, follow=["images"])
#@reversion.register
# reversion.register(Rapid, follow=["images"])
# @reversion.register
class RapidImage(models.Model):
rapid = models.ForeignKey(Rapid,
related_name="images",
on_delete=models.CASCADE)
rapid = models.ForeignKey(Rapid, related_name="images", on_delete=models.CASCADE)
image = models.FileField(upload_to=image_directory_path)
image_annotations = models.TextField(
blank=True, null=True,
blank=True,
null=True,
help_text="Stores a JSON representation of annotations to be applied by cornerstonetools",
)
@@ -445,12 +465,14 @@ class RapidImage(models.Model):
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))
'<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'
image_tag.short_description = "Image"
def save(self, *args, **kwargs):
"""Override save method to add image hash"""
@@ -461,25 +483,24 @@ class RapidImage(models.Model):
# and generate a hash from the pixel data
# TODO: improve?
dataset = pydicom.dcmread(self.image)
#flatten = dataset.pixel_array.astype(str).flatten()
#print("flatteded")
#pre_join = ",".join(flatten)
#print(pre_join)
#hash = hashlib.md5(pre_join.encode()).hexdigest()
#----
# flatten = dataset.pixel_array.astype(str).flatten()
# print("flatteded")
# pre_join = ",".join(flatten)
# print(pre_join)
# hash = hashlib.md5(pre_join.encode()).hexdigest()
# ----
md5 = hashlib.md5()
first = True
for i in dataset.pixel_array.astype(str).flatten():
if first:
first=False
first = False
md5.update(f"{i}".encode())
else:
else:
md5.update(f",{i}".encode())
hash = md5.hexdigest()
#----
# ----
self.is_dicom = True
except pydicom.errors.InvalidDicomError:
self.image.file.open()
@@ -490,13 +511,12 @@ class RapidImage(models.Model):
class RapidCreationDefault(models.Model):
#author = models.OneToOneField(User,
# author = models.OneToOneField(User,
author = models.OneToOneField(
settings.AUTH_USER_MODEL,
related_name="rapid_default",
on_delete=models.CASCADE)
settings.AUTH_USER_MODEL, related_name="rapid_default", on_delete=models.CASCADE
)
#site = models.ManyToManyField(
# site = models.ManyToManyField(
# Site,
# related_name="site_rapid_creation_default",
# blank=True,
@@ -504,24 +524,30 @@ class RapidCreationDefault(models.Model):
# help_text="Default site to use when creating a new rapid")
def get_absolute_url(self):
return reverse('rapids:rapid_create')
return reverse("rapids:rapid_create")
#@reversion.register
# @reversion.register
class Exam(ExamBase):
app_name = "rapids"
exam_questions = SortedManyToManyField(
Rapid, related_name="exams", blank="true"
)
exam_questions = SortedManyToManyField(Rapid, related_name="exams", blank="true")
time_limit = models.IntegerField(
help_text="Exam time limit (in seconds). Default is 2100 secondse (35 minutes)", default=2100
help_text="Exam time limit (in seconds). Default is 2100 secondse (35 minutes)",
default=2100,
)
author = models.ManyToManyField(settings.AUTH_USER_MODEL,
blank=True,
help_text='Author of exam',
related_name="rapid_exam_author")
author = models.ManyToManyField(
settings.AUTH_USER_MODEL,
blank=True,
help_text="Author of exam",
related_name="rapid_exam_author",
)
valid_users = models.ManyToManyField(
CidUser, blank=True, related_name="rapids_exams"
)
def get_normal_abnormal_breakdown(self):
# Inefficient but more extendible
@@ -534,9 +560,8 @@ class Exam(ExamBase):
normal.append(q)
else:
abnormal.append(q)
return len(normal)
return len(normal)
def get_exam_json(self, based=True):
questions = self.exam_questions.all()
@@ -556,32 +581,32 @@ class Exam(ExamBase):
if based:
feedback_images.append(image_as_base64(i.image))
else:
feedback_images.append("{}/{}".format(settings.REMOTE_URL, i.image.url))
feedback_images.append(
"{}/{}".format(settings.REMOTE_URL, i.image.url)
)
else:
if based:
images.append(image_as_base64(i.image))
else:
images.append("{}/{}".format(settings.REMOTE_URL, i.image.url))
exam_questions[q.id] = {
"images": images,
#"feedback_image": [],
#"annotations": [str(q.image_annotations)],
# "feedback_image": [],
# "annotations": [str(q.image_annotations)],
"type": "rapid",
}
if not self.exam_mode:
exam_questions[q.id]["normal"] = q.normal
exam_questions[q.id]["feedback_images"] = feedback_images
exam_questions[q.id]["answers"] = list(q.get_correct_unstripped_answers())
exam_questions[q.id]["answers"] = list(
q.get_correct_unstripped_answers()
)
#if feedback_images:
# if feedback_images:
# exam_questions[q.id]["feedback_image"] = feedback_images
exam_json = {
"eid": "rapid/{}".format(self.id),
"cached": False,
@@ -598,7 +623,7 @@ class Exam(ExamBase):
return exam_json
def get_absolute_url(self):
return reverse('rapids:exam_overview', kwargs={'pk': self.pk})
return reverse("rapids:exam_overview", kwargs={"pk": self.pk})
@reversion.register
@@ -615,7 +640,11 @@ class CidUserAnswer(models.Model):
answer_compare = models.TextField(max_length=500, blank=True)
cid = models.BigIntegerField(blank=True, null=True, help_text="Candidate ID (limitied by BigIntegerField size)")
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(
@@ -643,7 +672,7 @@ class CidUserAnswer(models.Model):
self.answer = self.answer.strip()
s = self.answer.lower()
s = s.translate(str.maketrans('', '', string.punctuation.replace("#", "")))
s = s.translate(str.maketrans("", "", string.punctuation.replace("#", "")))
self.answer_compare = s
@@ -671,9 +700,9 @@ class CidUserAnswer(models.Model):
ans = self.answer_compare
# TODO: this should be cleaned up as we don't want duplicates...
#try:
# try:
# marked_ans = q.answers.get(answer_compare__iexact=ans)
#except Answer.DoesNotExist:
# except Answer.DoesNotExist:
# marked_ans = None
marked_ans = q.answers.filter(answer_compare__iexact=ans).first()
@@ -708,4 +737,4 @@ class CidUserAnswer(models.Model):
mark = 0
else:
mark = "unmarked"
return mark
return mark
+19
View File
@@ -0,0 +1,19 @@
# Generated by Django 3.2.8 on 2021-12-11 22:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('generic', '0015_ciduser_ciduserexam'),
('sbas', '0006_alter_exam_open_access'),
]
operations = [
migrations.AddField(
model_name='exam',
name='valid_users',
field=models.ManyToManyField(blank=True, related_name='sba_exams', to='generic.CidUser'),
),
]
+2 -1
View File
@@ -11,7 +11,7 @@ from django.utils.translation import ugettext_lazy as _
from sortedm2m.fields import SortedManyToManyField
from generic.models import ExamBase, QuestionNote
from generic.models import CidUser, ExamBase, QuestionNote
import reversion
@@ -159,6 +159,7 @@ class Exam(ExamBase):
help_text='Author of exam',
related_name="sba_exam_author")
valid_users = models.ManyToManyField(CidUser, blank=True, related_name="sba_exams")
@reversion.register