diff --git a/anatomy/migrations/0054_exam_valid_users.py b/anatomy/migrations/0054_exam_valid_users.py
new file mode 100644
index 00000000..b061fcfe
--- /dev/null
+++ b/anatomy/migrations/0054_exam_valid_users.py
@@ -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'),
+ ),
+ ]
diff --git a/anatomy/models.py b/anatomy/models.py
index a06e5126..0b38b1bd 100644
--- a/anatomy/models.py
+++ b/anatomy/models.py
@@ -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()
diff --git a/generic/urls.py b/generic/urls.py
index 0241b4c6..ea6baf43 100755
--- a/generic/urls.py
+++ b/generic/urls.py
@@ -26,4 +26,5 @@ urlpatterns = [
),
name="examination-autocomplete",
),
+ path("cids/manage/", views.create_examination, name="create_examination"),
]
diff --git a/longs/migrations/0055_exam_valid_users.py b/longs/migrations/0055_exam_valid_users.py
new file mode 100644
index 00000000..a390cab6
--- /dev/null
+++ b/longs/migrations/0055_exam_valid_users.py
@@ -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'),
+ ),
+ ]
diff --git a/longs/models.py b/longs/models.py
index 05862f62..bb710cb2 100644
--- a/longs/models.py
+++ b/longs/models.py
@@ -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)
diff --git a/rapids/migrations/0039_exam_valid_users.py b/rapids/migrations/0039_exam_valid_users.py
new file mode 100644
index 00000000..d24b5b6e
--- /dev/null
+++ b/rapids/migrations/0039_exam_valid_users.py
@@ -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'),
+ ),
+ ]
diff --git a/rapids/models.py b/rapids/models.py
index 4e83260e..f622f9f8 100644
--- a/rapids/models.py
+++ b/rapids/models.py
@@ -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(
- '
Click and hold to zoom'
- .format(self.image.url))
+ '
Click and hold to zoom'.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
\ No newline at end of file
+ return mark
diff --git a/sbas/migrations/0007_exam_valid_users.py b/sbas/migrations/0007_exam_valid_users.py
new file mode 100644
index 00000000..48a5b16a
--- /dev/null
+++ b/sbas/migrations/0007_exam_valid_users.py
@@ -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'),
+ ),
+ ]
diff --git a/sbas/models.py b/sbas/models.py
index 58755a54..bfb1f79c 100644
--- a/sbas/models.py
+++ b/sbas/models.py
@@ -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