From 3d9c019bdca78b06c513de45b327959b21110ec6 Mon Sep 17 00:00:00 2001 From: Ross Date: Sat, 11 Dec 2021 22:21:12 +0000 Subject: [PATCH] . --- anatomy/migrations/0054_exam_valid_users.py | 19 ++ anatomy/models.py | 4 +- generic/urls.py | 1 + longs/migrations/0055_exam_valid_users.py | 19 ++ longs/models.py | 4 +- rapids/migrations/0039_exam_valid_users.py | 19 ++ rapids/models.py | 243 +++++++++++--------- sbas/migrations/0007_exam_valid_users.py | 19 ++ sbas/models.py | 3 +- 9 files changed, 221 insertions(+), 110 deletions(-) create mode 100644 anatomy/migrations/0054_exam_valid_users.py create mode 100644 longs/migrations/0055_exam_valid_users.py create mode 100644 rapids/migrations/0039_exam_valid_users.py create mode 100644 sbas/migrations/0007_exam_valid_users.py 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