.
This commit is contained in:
@@ -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
@@ -15,7 +15,7 @@ from sortedm2m.fields import SortedManyToManyField
|
|||||||
|
|
||||||
import string
|
import string
|
||||||
|
|
||||||
from generic.models import Examination, ExamBase, QuestionNote
|
from generic.models import CidUser, Examination, ExamBase, QuestionNote
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from helpers.images import image_as_base64
|
from helpers.images import image_as_base64
|
||||||
@@ -354,6 +354,8 @@ class Exam(ExamBase):
|
|||||||
related_name="anatomy_exam_author",
|
related_name="anatomy_exam_author",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
valid_users = models.ManyToManyField(CidUser, blank=True, related_name="anatomy_exams")
|
||||||
|
|
||||||
def get_exam_json(self, based=True):
|
def get_exam_json(self, based=True):
|
||||||
questions = self.exam_questions.all()
|
questions = self.exam_questions.all()
|
||||||
|
|
||||||
|
|||||||
@@ -26,4 +26,5 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
name="examination-autocomplete",
|
name="examination-autocomplete",
|
||||||
),
|
),
|
||||||
|
path("cids/manage/", views.create_examination, name="create_examination"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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
@@ -28,7 +28,7 @@ from helpers.images import image_as_base64, pretty_print_dicom
|
|||||||
|
|
||||||
from anatomy.models import Modality
|
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
|
# 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")
|
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):
|
def get_exam_question_json(self, question_id):
|
||||||
q = get_object_or_404(Long, pk=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
@@ -24,7 +24,7 @@ from helpers.images import image_as_base64
|
|||||||
|
|
||||||
from django.contrib.contenttypes.fields import GenericRelation
|
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
|
import reversion
|
||||||
|
|
||||||
@@ -37,31 +37,33 @@ import pydicom.errors
|
|||||||
|
|
||||||
image_storage = FileSystemStorage(
|
image_storage = FileSystemStorage(
|
||||||
# Physical file location ROOT
|
# Physical file location ROOT
|
||||||
location=u"{0}".format(settings.MEDIA_ROOT),
|
location="{0}".format(settings.MEDIA_ROOT),
|
||||||
# Url for file
|
# Url for file
|
||||||
base_url=u"{0}".format(settings.MEDIA_URL),
|
base_url="{0}".format(settings.MEDIA_URL),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def image_directory_path(instance, filename):
|
def image_directory_path(instance, filename):
|
||||||
#return u"{0}".format(filename)
|
# return u"{0}".format(filename)
|
||||||
return u"rapids/picture/{0}".format(filename)
|
return "rapids/picture/{0}".format(filename)
|
||||||
|
|
||||||
|
|
||||||
def get_answer_compare(s):
|
def get_answer_compare(s):
|
||||||
s = s.strip().lower()
|
s = s.strip().lower()
|
||||||
s = s.translate(str.maketrans('', '', string.punctuation.replace("#", "")))
|
s = s.translate(str.maketrans("", "", string.punctuation.replace("#", "")))
|
||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
class Abnormality(models.Model):
|
class Abnormality(models.Model):
|
||||||
name = models.CharField(max_length=200,
|
name = models.CharField(
|
||||||
unique=True,
|
max_length=200, unique=True, help_text="Primary abnormality on the film(s)"
|
||||||
help_text="Primary abnormality on the film(s)")
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('name', )
|
ordering = ("name",)
|
||||||
|
|
||||||
|
|
||||||
class Region(models.Model):
|
class Region(models.Model):
|
||||||
@@ -69,13 +71,14 @@ class Region(models.Model):
|
|||||||
max_length=200,
|
max_length=200,
|
||||||
unique=True,
|
unique=True,
|
||||||
help_text="Region of the abnormality (not including lateralitiy)",
|
help_text="Region of the abnormality (not including lateralitiy)",
|
||||||
blank=True)
|
blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('name', )
|
ordering = ("name",)
|
||||||
|
|
||||||
|
|
||||||
class Examination(models.Model):
|
class Examination(models.Model):
|
||||||
@@ -85,7 +88,8 @@ class Examination(models.Model):
|
|||||||
return self.examination
|
return self.examination
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('examination', )
|
ordering = ("examination",)
|
||||||
|
|
||||||
|
|
||||||
class Answer(models.Model):
|
class Answer(models.Model):
|
||||||
question = models.ForeignKey(
|
question = models.ForeignKey(
|
||||||
@@ -119,8 +123,8 @@ class Answer(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class Rapid(models.Model):
|
class Rapid(models.Model):
|
||||||
#author = models.ForeignKey('auth.User', on_delete=models.CASCADE)
|
# author = models.ForeignKey('auth.User', on_delete=models.CASCADE)
|
||||||
#image = models.ImageField()
|
# image = models.ImageField()
|
||||||
question = models.TextField(null=True, blank=True)
|
question = models.TextField(null=True, blank=True)
|
||||||
feedback = 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 = models.ManyToManyField(
|
||||||
Region,
|
Region,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Region of the abnormality (laterality independent)")
|
help_text="Region of the abnormality (laterality independent)",
|
||||||
|
)
|
||||||
examination = models.ManyToManyField(
|
examination = models.ManyToManyField(
|
||||||
Examination, help_text="Name of the (primary) examination")
|
Examination, help_text="Name of the (primary) examination"
|
||||||
|
)
|
||||||
laterality = models.CharField(
|
laterality = models.CharField(
|
||||||
max_length=20,
|
max_length=20,
|
||||||
choices=LATERALITY_CHOICES,
|
choices=LATERALITY_CHOICES,
|
||||||
default=NONE,
|
default=NONE,
|
||||||
help_text="Applies to the answer, not the examination")
|
help_text="Applies to the answer, not the examination",
|
||||||
#condition = tagulous.models.TagField(
|
)
|
||||||
|
# condition = tagulous.models.TagField(
|
||||||
# to=Condition,
|
# to=Condition,
|
||||||
# blank=True,
|
# blank=True,
|
||||||
# help_text=
|
# help_text=
|
||||||
# "Associated condition. Will allow searching / filtering and tips / hints to be displayed. Conditions with spaces must be enclosed in quotes \"...\""
|
# "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')
|
# to=Sign, blank=True, help_text='Radiological signs in the question')
|
||||||
|
|
||||||
DEFAULT_SITE_ID = 1
|
DEFAULT_SITE_ID = 1
|
||||||
#site = models.ManyToManyField(
|
# site = models.ManyToManyField(
|
||||||
# Site,
|
# Site,
|
||||||
# related_name="site_rapid",
|
# related_name="site_rapid",
|
||||||
# blank=True,
|
# blank=True,
|
||||||
@@ -174,14 +181,16 @@ class Rapid(models.Model):
|
|||||||
verified = models.BooleanField(default=False)
|
verified = models.BooleanField(default=False)
|
||||||
created_date = models.DateTimeField(default=timezone.now)
|
created_date = models.DateTimeField(default=timezone.now)
|
||||||
published_date = models.DateTimeField(blank=True, null=True)
|
published_date = models.DateTimeField(blank=True, null=True)
|
||||||
author = models.ManyToManyField(settings.AUTH_USER_MODEL,
|
author = models.ManyToManyField(
|
||||||
blank=True,
|
settings.AUTH_USER_MODEL,
|
||||||
help_text='Author of question',
|
blank=True,
|
||||||
related_name="rapid_authored_questions")
|
help_text="Author of question",
|
||||||
|
related_name="rapid_authored_questions",
|
||||||
|
)
|
||||||
|
|
||||||
scrapped = models.BooleanField(
|
scrapped = models.BooleanField(
|
||||||
default=False,
|
default=False, help_text="Question has been scrapped and will not be shown"
|
||||||
help_text='Question has been scrapped and will not be shown')
|
)
|
||||||
|
|
||||||
open_access = models.BooleanField(
|
open_access = models.BooleanField(
|
||||||
help_text="If a question should be freely available to browse", default=True
|
help_text="If a question should be freely available to browse", default=True
|
||||||
@@ -189,9 +198,8 @@ class Rapid(models.Model):
|
|||||||
|
|
||||||
notes = GenericRelation(QuestionNote)
|
notes = GenericRelation(QuestionNote)
|
||||||
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
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):
|
def get_authors(self):
|
||||||
"""Returns a comma seperated text list of authors"""
|
"""Returns a comma seperated text list of authors"""
|
||||||
@@ -216,8 +224,10 @@ class Rapid(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
n = "Normal"
|
n = "Normal"
|
||||||
if not self.normal:
|
if not self.normal:
|
||||||
#n = self.answers.first()
|
# n = self.answers.first()
|
||||||
n = "{}/{} : {}".format(self.pk, self.abnormality.first(), self.region.first())
|
n = "{}/{} : {}".format(
|
||||||
|
self.pk, self.abnormality.first(), self.region.first()
|
||||||
|
)
|
||||||
exams = self.get_examinations()
|
exams = self.get_examinations()
|
||||||
lat = ""
|
lat = ""
|
||||||
if self.laterality != "NONE":
|
if self.laterality != "NONE":
|
||||||
@@ -227,8 +237,17 @@ class Rapid(models.Model):
|
|||||||
def get_primary_answer(self):
|
def get_primary_answer(self):
|
||||||
if self.normal:
|
if self.normal:
|
||||||
return "Normal"
|
return "Normal"
|
||||||
elif self.answers.filter(proposed=False, status=Answer.MarkOptions.CORRECT).count() > 0:
|
elif (
|
||||||
return self.answers.filter(proposed=False, status=Answer.MarkOptions.CORRECT).first().answer
|
self.answers.filter(
|
||||||
|
proposed=False, status=Answer.MarkOptions.CORRECT
|
||||||
|
).count()
|
||||||
|
> 0
|
||||||
|
):
|
||||||
|
return (
|
||||||
|
self.answers.filter(proposed=False, status=Answer.MarkOptions.CORRECT)
|
||||||
|
.first()
|
||||||
|
.answer
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return "None yet..."
|
return "None yet..."
|
||||||
|
|
||||||
@@ -261,11 +280,15 @@ class Rapid(models.Model):
|
|||||||
marked_answers = self.get_marked_answers()
|
marked_answers = self.get_marked_answers()
|
||||||
|
|
||||||
unmarked_answers = set(
|
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]
|
return [i.answer_compare for i in unmarked_answers]
|
||||||
|
|
||||||
# Marker code below is not used
|
# Marker code below is not used
|
||||||
if marker is None:
|
if marker is None:
|
||||||
return [i.answer_compare for i in unmarked_answers]
|
return [i.answer_compare for i in unmarked_answers]
|
||||||
@@ -275,7 +298,7 @@ class Rapid(models.Model):
|
|||||||
for answer in unmarked_answers:
|
for answer in unmarked_answers:
|
||||||
if answer.mark.filter(marker=marker).count() < 1:
|
if answer.mark.filter(marker=marker).count() < 1:
|
||||||
marker_unmarked.append(answer.answer_compare)
|
marker_unmarked.append(answer.answer_compare)
|
||||||
|
|
||||||
return marker_unmarked
|
return marker_unmarked
|
||||||
|
|
||||||
def get_unmarked_user_answer_count(self, exam_pk=None, marker=None):
|
def get_unmarked_user_answer_count(self, exam_pk=None, marker=None):
|
||||||
@@ -293,19 +316,12 @@ class Rapid(models.Model):
|
|||||||
if not include_normal:
|
if not include_normal:
|
||||||
queryset = queryset.filter(normal=False)
|
queryset = queryset.filter(normal=False)
|
||||||
|
|
||||||
user_answers = set(
|
user_answers = set([i.answer_compare for i in queryset])
|
||||||
[i.answer_compare for i in queryset]
|
|
||||||
)
|
|
||||||
|
|
||||||
return user_answers
|
return user_answers
|
||||||
|
|
||||||
def get_compare_answers(self):
|
def get_compare_answers(self):
|
||||||
return set(
|
return set([i.answer_compare for i in self.answers.filter()])
|
||||||
[
|
|
||||||
i.answer_compare
|
|
||||||
for i in self.answers.filter()
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_marked_answers(self):
|
def get_marked_answers(self):
|
||||||
return set(
|
return set(
|
||||||
@@ -352,11 +368,14 @@ class Rapid(models.Model):
|
|||||||
|
|
||||||
def get_image_url_array(self):
|
def get_image_url_array(self):
|
||||||
return json.dumps([f"{REMOTE_URL}{i.url}" for i in self.get_images()])
|
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):
|
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):
|
def get_laterality_string(self):
|
||||||
if self.laterality == self.NONE:
|
if self.laterality == self.NONE:
|
||||||
@@ -382,10 +401,12 @@ class Rapid(models.Model):
|
|||||||
|
|
||||||
compare_answers = self.get_compare_answers()
|
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
|
return new_answers
|
||||||
|
|
||||||
def get_question_json(self, based=True):
|
def get_question_json(self, based=True):
|
||||||
"""Returns json"""
|
"""Returns json"""
|
||||||
|
|
||||||
@@ -399,7 +420,9 @@ class Rapid(models.Model):
|
|||||||
if based:
|
if based:
|
||||||
feedback_images.append(image_as_base64(i.image))
|
feedback_images.append(image_as_base64(i.image))
|
||||||
else:
|
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))
|
feedback_images.append("{}/{}".format(settings.REMOTE_URL, i.image.url))
|
||||||
else:
|
else:
|
||||||
if based:
|
if based:
|
||||||
@@ -407,11 +430,9 @@ class Rapid(models.Model):
|
|||||||
else:
|
else:
|
||||||
images.append("{}/{}".format(settings.REMOTE_URL, i.image.url))
|
images.append("{}/{}".format(settings.REMOTE_URL, i.image.url))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
json = {
|
json = {
|
||||||
"images": images,
|
"images": images,
|
||||||
#"feedback_image": [],
|
# "feedback_image": [],
|
||||||
"annotations": annotations,
|
"annotations": annotations,
|
||||||
"type": "rapid",
|
"type": "rapid",
|
||||||
}
|
}
|
||||||
@@ -423,16 +444,15 @@ class Rapid(models.Model):
|
|||||||
return json
|
return json
|
||||||
|
|
||||||
|
|
||||||
#reversion.register(Rapid, follow=["images"])
|
# reversion.register(Rapid, follow=["images"])
|
||||||
#@reversion.register
|
# @reversion.register
|
||||||
class RapidImage(models.Model):
|
class RapidImage(models.Model):
|
||||||
rapid = models.ForeignKey(Rapid,
|
rapid = models.ForeignKey(Rapid, related_name="images", on_delete=models.CASCADE)
|
||||||
related_name="images",
|
|
||||||
on_delete=models.CASCADE)
|
|
||||||
image = models.FileField(upload_to=image_directory_path)
|
image = models.FileField(upload_to=image_directory_path)
|
||||||
|
|
||||||
image_annotations = models.TextField(
|
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",
|
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):
|
def image_tag(self):
|
||||||
if self.image:
|
if self.image:
|
||||||
return mark_safe(
|
return mark_safe(
|
||||||
'<img src="{}" class="admin-rapid-image" /><span class="admin-rapid-image-info">Click and hold to zoom<span>'
|
'<img src="{}" class="admin-rapid-image" /><span class="admin-rapid-image-info">Click and hold to zoom<span>'.format(
|
||||||
.format(self.image.url))
|
self.image.url
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
image_tag.short_description = 'Image'
|
image_tag.short_description = "Image"
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""Override save method to add image hash"""
|
"""Override save method to add image hash"""
|
||||||
@@ -461,25 +483,24 @@ class RapidImage(models.Model):
|
|||||||
# and generate a hash from the pixel data
|
# and generate a hash from the pixel data
|
||||||
# TODO: improve?
|
# TODO: improve?
|
||||||
dataset = pydicom.dcmread(self.image)
|
dataset = pydicom.dcmread(self.image)
|
||||||
#flatten = dataset.pixel_array.astype(str).flatten()
|
# flatten = dataset.pixel_array.astype(str).flatten()
|
||||||
#print("flatteded")
|
# print("flatteded")
|
||||||
#pre_join = ",".join(flatten)
|
# pre_join = ",".join(flatten)
|
||||||
#print(pre_join)
|
# print(pre_join)
|
||||||
#hash = hashlib.md5(pre_join.encode()).hexdigest()
|
# hash = hashlib.md5(pre_join.encode()).hexdigest()
|
||||||
#----
|
# ----
|
||||||
md5 = hashlib.md5()
|
md5 = hashlib.md5()
|
||||||
first = True
|
first = True
|
||||||
for i in dataset.pixel_array.astype(str).flatten():
|
for i in dataset.pixel_array.astype(str).flatten():
|
||||||
if first:
|
if first:
|
||||||
first=False
|
first = False
|
||||||
md5.update(f"{i}".encode())
|
md5.update(f"{i}".encode())
|
||||||
else:
|
else:
|
||||||
md5.update(f",{i}".encode())
|
md5.update(f",{i}".encode())
|
||||||
|
|
||||||
hash = md5.hexdigest()
|
hash = md5.hexdigest()
|
||||||
#----
|
# ----
|
||||||
|
|
||||||
|
|
||||||
self.is_dicom = True
|
self.is_dicom = True
|
||||||
except pydicom.errors.InvalidDicomError:
|
except pydicom.errors.InvalidDicomError:
|
||||||
self.image.file.open()
|
self.image.file.open()
|
||||||
@@ -490,13 +511,12 @@ class RapidImage(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class RapidCreationDefault(models.Model):
|
class RapidCreationDefault(models.Model):
|
||||||
#author = models.OneToOneField(User,
|
# author = models.OneToOneField(User,
|
||||||
author = models.OneToOneField(
|
author = models.OneToOneField(
|
||||||
settings.AUTH_USER_MODEL,
|
settings.AUTH_USER_MODEL, related_name="rapid_default", on_delete=models.CASCADE
|
||||||
related_name="rapid_default",
|
)
|
||||||
on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
#site = models.ManyToManyField(
|
# site = models.ManyToManyField(
|
||||||
# Site,
|
# Site,
|
||||||
# related_name="site_rapid_creation_default",
|
# related_name="site_rapid_creation_default",
|
||||||
# blank=True,
|
# blank=True,
|
||||||
@@ -504,24 +524,30 @@ class RapidCreationDefault(models.Model):
|
|||||||
# help_text="Default site to use when creating a new rapid")
|
# help_text="Default site to use when creating a new rapid")
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('rapids:rapid_create')
|
return reverse("rapids:rapid_create")
|
||||||
|
|
||||||
#@reversion.register
|
|
||||||
|
# @reversion.register
|
||||||
class Exam(ExamBase):
|
class Exam(ExamBase):
|
||||||
app_name = "rapids"
|
app_name = "rapids"
|
||||||
|
|
||||||
exam_questions = SortedManyToManyField(
|
exam_questions = SortedManyToManyField(Rapid, related_name="exams", blank="true")
|
||||||
Rapid, related_name="exams", blank="true"
|
|
||||||
)
|
|
||||||
|
|
||||||
time_limit = models.IntegerField(
|
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,
|
author = models.ManyToManyField(
|
||||||
blank=True,
|
settings.AUTH_USER_MODEL,
|
||||||
help_text='Author of exam',
|
blank=True,
|
||||||
related_name="rapid_exam_author")
|
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):
|
def get_normal_abnormal_breakdown(self):
|
||||||
# Inefficient but more extendible
|
# Inefficient but more extendible
|
||||||
@@ -534,9 +560,8 @@ class Exam(ExamBase):
|
|||||||
normal.append(q)
|
normal.append(q)
|
||||||
else:
|
else:
|
||||||
abnormal.append(q)
|
abnormal.append(q)
|
||||||
|
|
||||||
return len(normal)
|
|
||||||
|
|
||||||
|
return len(normal)
|
||||||
|
|
||||||
def get_exam_json(self, based=True):
|
def get_exam_json(self, based=True):
|
||||||
questions = self.exam_questions.all()
|
questions = self.exam_questions.all()
|
||||||
@@ -556,32 +581,32 @@ class Exam(ExamBase):
|
|||||||
if based:
|
if based:
|
||||||
feedback_images.append(image_as_base64(i.image))
|
feedback_images.append(image_as_base64(i.image))
|
||||||
else:
|
else:
|
||||||
feedback_images.append("{}/{}".format(settings.REMOTE_URL, i.image.url))
|
feedback_images.append(
|
||||||
|
"{}/{}".format(settings.REMOTE_URL, i.image.url)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
if based:
|
if based:
|
||||||
images.append(image_as_base64(i.image))
|
images.append(image_as_base64(i.image))
|
||||||
else:
|
else:
|
||||||
images.append("{}/{}".format(settings.REMOTE_URL, i.image.url))
|
images.append("{}/{}".format(settings.REMOTE_URL, i.image.url))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
exam_questions[q.id] = {
|
exam_questions[q.id] = {
|
||||||
"images": images,
|
"images": images,
|
||||||
#"feedback_image": [],
|
# "feedback_image": [],
|
||||||
#"annotations": [str(q.image_annotations)],
|
# "annotations": [str(q.image_annotations)],
|
||||||
"type": "rapid",
|
"type": "rapid",
|
||||||
}
|
}
|
||||||
|
|
||||||
if not self.exam_mode:
|
if not self.exam_mode:
|
||||||
exam_questions[q.id]["normal"] = q.normal
|
exam_questions[q.id]["normal"] = q.normal
|
||||||
exam_questions[q.id]["feedback_images"] = feedback_images
|
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_questions[q.id]["feedback_image"] = feedback_images
|
||||||
|
|
||||||
|
|
||||||
exam_json = {
|
exam_json = {
|
||||||
"eid": "rapid/{}".format(self.id),
|
"eid": "rapid/{}".format(self.id),
|
||||||
"cached": False,
|
"cached": False,
|
||||||
@@ -598,7 +623,7 @@ class Exam(ExamBase):
|
|||||||
return exam_json
|
return exam_json
|
||||||
|
|
||||||
def get_absolute_url(self):
|
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
|
@reversion.register
|
||||||
@@ -615,7 +640,11 @@ class CidUserAnswer(models.Model):
|
|||||||
|
|
||||||
answer_compare = 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)")
|
cid = models.BigIntegerField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
help_text="Candidate ID (limitied by BigIntegerField size)",
|
||||||
|
)
|
||||||
|
|
||||||
# Each user answer is associated with a particular exam
|
# Each user answer is associated with a particular exam
|
||||||
exam = models.ForeignKey(
|
exam = models.ForeignKey(
|
||||||
@@ -643,7 +672,7 @@ class CidUserAnswer(models.Model):
|
|||||||
self.answer = self.answer.strip()
|
self.answer = self.answer.strip()
|
||||||
|
|
||||||
s = self.answer.lower()
|
s = self.answer.lower()
|
||||||
s = s.translate(str.maketrans('', '', string.punctuation.replace("#", "")))
|
s = s.translate(str.maketrans("", "", string.punctuation.replace("#", "")))
|
||||||
|
|
||||||
self.answer_compare = s
|
self.answer_compare = s
|
||||||
|
|
||||||
@@ -671,9 +700,9 @@ class CidUserAnswer(models.Model):
|
|||||||
ans = self.answer_compare
|
ans = self.answer_compare
|
||||||
|
|
||||||
# TODO: this should be cleaned up as we don't want duplicates...
|
# TODO: this should be cleaned up as we don't want duplicates...
|
||||||
#try:
|
# try:
|
||||||
# marked_ans = q.answers.get(answer_compare__iexact=ans)
|
# marked_ans = q.answers.get(answer_compare__iexact=ans)
|
||||||
#except Answer.DoesNotExist:
|
# except Answer.DoesNotExist:
|
||||||
# marked_ans = None
|
# marked_ans = None
|
||||||
marked_ans = q.answers.filter(answer_compare__iexact=ans).first()
|
marked_ans = q.answers.filter(answer_compare__iexact=ans).first()
|
||||||
|
|
||||||
@@ -708,4 +737,4 @@ class CidUserAnswer(models.Model):
|
|||||||
mark = 0
|
mark = 0
|
||||||
else:
|
else:
|
||||||
mark = "unmarked"
|
mark = "unmarked"
|
||||||
return mark
|
return mark
|
||||||
|
|||||||
@@ -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
@@ -11,7 +11,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||||||
|
|
||||||
from sortedm2m.fields import SortedManyToManyField
|
from sortedm2m.fields import SortedManyToManyField
|
||||||
|
|
||||||
from generic.models import ExamBase, QuestionNote
|
from generic.models import CidUser, ExamBase, QuestionNote
|
||||||
|
|
||||||
import reversion
|
import reversion
|
||||||
|
|
||||||
@@ -159,6 +159,7 @@ class Exam(ExamBase):
|
|||||||
help_text='Author of exam',
|
help_text='Author of exam',
|
||||||
related_name="sba_exam_author")
|
related_name="sba_exam_author")
|
||||||
|
|
||||||
|
valid_users = models.ManyToManyField(CidUser, blank=True, related_name="sba_exams")
|
||||||
|
|
||||||
|
|
||||||
@reversion.register
|
@reversion.register
|
||||||
|
|||||||
Reference in New Issue
Block a user