From 4354786ef5288bf47d872491dc9a8bd5da18500c Mon Sep 17 00:00:00 2001 From: Ross Date: Wed, 8 Jun 2022 18:59:06 +0100 Subject: [PATCH] . --- generic/views.py | 2 +- longs/models.py | 251 +++++++++++++++++++++++++---------------------- 2 files changed, 136 insertions(+), 117 deletions(-) diff --git a/generic/views.py b/generic/views.py index a94fdc38..753960e4 100644 --- a/generic/views.py +++ b/generic/views.py @@ -2055,7 +2055,7 @@ class CidUserCreate(RevisionMixin, CidManagerRequiredMixin, CreateView): form_class = CidUserForm -class CidUserUpdate(RevisionMixin, CidManagerRequiredMixin, UpdateView): +class CidUserUpdate(CidManagerRequiredMixin, UpdateView): model = CidUser form_class = CidUserForm diff --git a/longs/models.py b/longs/models.py index 1570ae44..6bb96530 100644 --- a/longs/models.py +++ b/longs/models.py @@ -28,7 +28,18 @@ from helpers.images import image_as_base64, pretty_print_dicom from anatomy.models import Modality -from generic.models import CidUser, CidUserGroup, Examination, Condition, Sign, ExamBase, Plane, Contrast, QuestionNote, UserUserGroup +from generic.models import ( + CidUser, + CidUserGroup, + Examination, + Condition, + Sign, + ExamBase, + Plane, + Contrast, + QuestionNote, + UserUserGroup, +) # from generic.models import Examination, Site, Condition, Sign @@ -47,14 +58,14 @@ from django.contrib.contenttypes.fields import GenericRelation image_storage = FileSystemStorage( # Physical file location ROOT - location=u"{0}longs/".format(settings.MEDIA_ROOT), + location="{0}longs/".format(settings.MEDIA_ROOT), # Url for file - base_url=u"{0}longs/".format(settings.MEDIA_URL), + base_url="{0}longs/".format(settings.MEDIA_URL), ) def image_directory_path(instance, filename): - return u"longs/picture/{0}".format(filename) + return "longs/picture/{0}".format(filename) def findMiddle(input_list): @@ -127,14 +138,15 @@ class Long(models.Model): json_creation_time = models.DateTimeField(blank=True, default=None, null=True) - question_json_id = models.IntegerField(default=1, help_text="Auto incrementing json creation number") - + question_json_id = models.IntegerField( + default=1, help_text="Auto incrementing json creation number" + ) series = SortedManyToManyField("LongSeries", related_name="long") notes = GenericRelation(QuestionNote) - #question_file = models.FileField(upload_to=question_file_directory_path, blank=True, null=True) + # question_file = models.FileField(upload_to=question_file_directory_path, blank=True, null=True) def get_absolute_url(self): return reverse("longs:question_detail", kwargs={"pk": self.pk}) @@ -170,9 +182,7 @@ class Long(models.Model): return images def get_image_urls(self): - return ",".join( - [f"{REMOTE_URL}{i.url}" for i in self.get_images()] - ) + return ",".join([f"{REMOTE_URL}{i.url}" for i in self.get_images()]) def get_image_url_array(self): return json.dumps([f"{REMOTE_URL}{i.url}" for i in self.get_images()]) @@ -199,48 +209,50 @@ class Long(models.Model): for answer in unmarked_answers: if answer.mark.filter(marker=marker).count() < 1: marker_unmarked.append(answer) - - return marker_unmarked + return marker_unmarked def get_question_json(self, based=True, feedback=False): # self == q? - #q = get_object_or_404(Long, pk=question_id) + # q = get_object_or_404(Long, pk=question_id) question_id = self.pk - if not based: image_titles = [] images = [] question_series = self.series.all() # Loop through longimage associations for i, series in enumerate(question_series): - #image_array = [] - #for i in series.images.all(): + # image_array = [] + # for i in series.images.all(): # image_array.append(image_as_base64(i.image)) # #image_array.append(i.image.url) # We are still limited by the size of a series (although this should not be too big or computers will crash...) - images.append(["{}/{}".format(settings.REMOTE_URL, i.image.url) for i in series.images.all()]) - #image_array = [i.image.url for i in series.images.all()] - #images.append(url) + images.append( + [ + "{}/{}".format(settings.REMOTE_URL, i.image.url) + for i in series.images.all() + ] + ) + # image_array = [i.image.url for i in series.images.all()] + # images.append(url) image_titles.append(series.get_examination()) question_json = { "title": self.history, "images": images, "image_titles": image_titles, - #"feedback_image": [], - #"annotations": [str(q.image_annotations)], + # "feedback_image": [], + # "annotations": [str(q.image_annotations)], "type": "long", "cached": False, } return question_json - - #exam_order.append(q.id) - path= "{0}longs/questions/{1}.json".format(settings.MEDIA_ROOT, question_id) - url= "{0}longs/questions/{1}.json".format(settings.MEDIA_URL, question_id) + # exam_order.append(q.id) + path = "{0}longs/questions/{1}.json".format(settings.MEDIA_ROOT, question_id) + url = "{0}longs/questions/{1}.json".format(settings.MEDIA_URL, question_id) timestamp = timezone.now() @@ -250,32 +262,34 @@ class Long(models.Model): print("start writing file") # We manually create the json for long questions to reducem memroy usade - f.write("""{{ + f.write( + """{{ "title": "{}", "type": "long", "generated": "{}", "question_json_id": "{}", - """.format(self.history, timestamp.isoformat(), self.question_json_id) + """.format( + self.history, timestamp.isoformat(), self.question_json_id + ) ) - f.write('"images" : [') - #images = [] + # images = [] image_titles = [] question_series = self.series.all() # Loop through longimage associations for i, series in enumerate(question_series): print("gen series", i) - #image_array = [] - #for i in series.images.all(): + # image_array = [] + # for i in series.images.all(): # image_array.append(image_as_base64(i.image)) # #image_array.append(i.image.url) # We are still limited by the size of a series (although this should not be too big or computers will crash...) - #image_array = [image_as_base64(i.image) for i in series.images.all()] + # image_array = [image_as_base64(i.image) for i in series.images.all()] ##image_array = [i.image.url for i in series.images.all()] - #f.write(json.dumps(image_array)) + # f.write(json.dumps(image_array)) f.write('["') first = True @@ -286,12 +300,12 @@ class Long(models.Model): f.write(', "') f.write(image_as_base64(img.image)) # for debugging - #f.write(img.image.url) + # f.write(img.image.url) f.write('"') first = False - f.write(']') + f.write("]") - #images.append(url) + # images.append(url) if i != len(question_series) - 1: f.write(",") image_titles.append(series.get_examination()) @@ -299,11 +313,10 @@ class Long(models.Model): f.write('"image_titles" : {} }}'.format(json.dumps(image_titles))) - self.json_creation_time = timestamp self.save() - #exam_question = { + # exam_question = { # "title": q.history, # "images": images, # "image_titles": image_titles, @@ -311,12 +324,10 @@ class Long(models.Model): # #"annotations": [str(q.image_annotations)], # "type": "long", # "cached": False, - #} + # } return url - - # def GetNonFeedbackQuestionImages(self): # return self.get_images() @@ -324,6 +335,7 @@ class Long(models.Model): def test_image_validator(file): pass + class LongSeriesImage(models.Model): image = models.FileField(upload_to=image_directory_path) position = models.IntegerField(default=0) @@ -333,14 +345,14 @@ class LongSeriesImage(models.Model): ) class Meta: - ordering = ['position'] + ordering = ["position"] def get_dicom_info(self): try: info = pretty_print_dicom(pydicom.read_file(self.image)) except pydicom.errors.InvalidDicomError: info = "File is not a dicom." - return(info) + return info @reversion.register @@ -371,12 +383,12 @@ class LongSeries(models.Model): null=True, blank=True, ) - #long = models.ManyToManyField( + # long = models.ManyToManyField( # "Long", # help_text="The question(s) this series should be associated with", # related_name="series", # blank=True, - #) + # ) description = models.TextField( blank=True, help_text="Description of stack, for admin organisation, will not be visible when taking", @@ -392,15 +404,14 @@ class LongSeries(models.Model): help_text="If a question should be freely available to browse", default=True ) - def __str__(self): if self.long: long_id = ", ".format([long.pk for long in self.long.all()]) - #long_id = self.long.pk + # long_id = self.long.pk else: long_id = "None" - return "{}/{} : {} [{}]".format(self.pk, - self.get_examination(), self.description, long_id + return "{}/{} : {} [{}]".format( + self.pk, self.get_examination(), self.description, long_id ) def get_author_objects(self): @@ -440,18 +451,12 @@ class LongSeries(models.Model): return reverse("longs:long_series_detail", kwargs={"pk": self.pk}) def get_image_urls(self, feedback=False): - images = [ - f"{REMOTE_URL}{i.image.url}" - for i in self.images.all() - ] + images = [f"{REMOTE_URL}{i.image.url}" for i in self.images.all()] return ",".join(images) def get_image_url_array(self, feedback=False): - images = [ - f"{REMOTE_URL}{i.image.url}" - for i in self.images.all() - ] + images = [f"{REMOTE_URL}{i.image.url}" for i in self.images.all()] return json.dumps(images) @@ -466,7 +471,9 @@ class LongSeries(models.Model): thumbnailer = get_thumbnailer(img) thumbnail = thumbnailer["exam-list"] except InvalidImageFormatError: - return format_html('Invalid image url', img), len(images) + return format_html('Invalid image url', img), len( + images + ) return format_html('', thumbnail), len(images) def get_block(self): @@ -494,8 +501,6 @@ class LongSeries(models.Model): i.position = n i.save() n = n + 1 - - def order_by_dicom(self, field="SliceLocation"): images = self.images.all() @@ -505,7 +510,7 @@ class LongSeries(models.Model): for i in images: files.append((i, pydicom.dcmread(i.image.path))) - #print("file count: {}".format(len(files))) + # print("file count: {}".format(len(files))) # skip files with no SliceLocation (eg scout views) slices = [] @@ -514,7 +519,7 @@ class LongSeries(models.Model): for i, f in files: if hasattr(f, field): slices.append(f) - #map[f.SliceLocation] = i + # map[f.SliceLocation] = i map[f[field].value] = i else: skipcount = skipcount + 1 @@ -524,8 +529,7 @@ class LongSeries(models.Model): # ensure they are in the correct order slices = sorted(slices, key=lambda s: s[field].value) - - #print(slices) + # print(slices) n = 1 for f in slices: i = map[f[field].value] @@ -543,6 +547,7 @@ class LongSeries(models.Model): return size + # class LongImage(models.Model): # long = models.ForeignKey(Long, # related_name="images", @@ -589,14 +594,20 @@ class Exam(ExamBase): default=4500, ) - author = models.ManyToManyField(settings.AUTH_USER_MODEL, - blank=True, - help_text='Author of exam', - related_name="long_exam_author") + author = models.ManyToManyField( + settings.AUTH_USER_MODEL, + blank=True, + help_text="Author of exam", + related_name="long_exam_author", + ) - 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_cid_users = models.ManyToManyField(CidUser, blank=True, related_name="longs_exams") + valid_cid_users = models.ManyToManyField( + CidUser, blank=True, related_name="longs_exams" + ) valid_user_users = models.ManyToManyField( settings.AUTH_USER_MODEL, blank=True, related_name="user_longs_exams" @@ -606,49 +617,45 @@ class Exam(ExamBase): CidUserGroup, blank=True, help_text="These groups define which candidates are able to be added to the exams/collection.", - related_name="longs_cid_user_groups" - + related_name="longs_cid_user_groups", ) user_user_groups = models.ManyToManyField( UserUserGroup, blank=True, help_text="These groups define which candidates are able to be added to the exams/collection.", - related_name="longs_user_user_groups" + related_name="longs_user_user_groups", ) def get_exam_question_json(self, question_id): q = get_object_or_404(Long, pk=question_id) - #exam_order.append(q.id) + # exam_order.append(q.id) # Loop through longimage associations images = [] image_titles = [] for series in q.series.all(): - #image_array = [] - #for i in series.images.all(): + # image_array = [] + # for i in series.images.all(): # image_array.append(image_as_base64(i.image)) # #image_array.append(i.image.url) image_array = [image_as_base64(i.image) for i in series.images.all()] images.append(image_array) image_titles.append(series.get_examination()) - - exam_question = { "title": q.history, "images": images, "image_titles": image_titles, - #"feedback_image": [], - #"annotations": [str(q.image_annotations)], + # "feedback_image": [], + # "annotations": [str(q.image_annotations)], "type": "long", "images_json": True, } return exam_question - def get_exam_json(self, based=True): questions = self.exam_questions.all() @@ -661,7 +668,6 @@ class Exam(ExamBase): exam_order.append(q.id) if based: - # If it is a new question we need to for a question json refresh if q.json_creation_time is None: q.get_question_json() @@ -672,30 +678,31 @@ class Exam(ExamBase): images = [] image_titles = [] for series in q.series.all(): - #image_array = [] - #for i in series.images.all(): + # image_array = [] + # for i in series.images.all(): # image_array.append(image_as_base64(i.image)) # #image_array.append(i.image.url) - image_array = ["{}/{}".format(settings.REMOTE_URL, i.image.url) for i in series.images.all()] + image_array = [ + "{}/{}".format(settings.REMOTE_URL, i.image.url) + for i in series.images.all() + ] images.append(image_array) image_titles.append(series.get_examination()) - - #exam_questions[q.id] = n + # exam_questions[q.id] = n exam_questions[q.id] = { "title": q.history, "images": images, "image_titles": image_titles, - #"feedback_image": [], - #"annotations": [str(q.image_annotations)], + # "feedback_image": [], + # "annotations": [str(q.image_annotations)], "type": "long", } - #if feedback_images: + # if feedback_images: # exam_questions[q.id]["feedback_image"] = feedback_images - exam_json = { "eid": "long/{}".format(self.id), "cached": False, @@ -705,18 +712,18 @@ class Exam(ExamBase): "exam_order": exam_order, "questions": exam_questions, "based": based, - #"question_requests": exam_questions, + # "question_requests": exam_questions, } if based: exam_json["question_requests"] = exam_questions - if self.time_limit: exam_json["exam_time"] = self.time_limit return exam_json + @reversion.register class CidUserAnswer(models.Model): """User answers by candidate""" @@ -764,41 +771,48 @@ class CidUserAnswer(models.Model): EIGHT = 8, _("8") score = models.CharField( - max_length=3, choices=ScoreOptions.choices, default=ScoreOptions.UNMARKED, blank=True, help_text="The final score for the candidate" + max_length=3, + choices=ScoreOptions.choices, + default=ScoreOptions.UNMARKED, + blank=True, + help_text="The final score for the candidate", ) - #mark = models.ManyToManyField( + # mark = models.ManyToManyField( # "AnswerMarks", related_name="user_answer", blank=True - #) - #secondary_mark = models.ForeignKey( + # ) + # secondary_mark = models.ForeignKey( # "AnswerMarks", related_name="user_answer", on_delete=models.SET_NULL, null=True, blank=True - #) + # ) created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) class Meta: - ordering = ['cid'] + ordering = ["cid"] def __str__(self): try: exam = self.exam except (Exam.DoesNotExist, KeyError) as e: exam = "None" - return "{}/{}/{} ({})".format( - exam, self.cid, self.created, self.updated - ) + return "{}/{}/{} ({})".format(exam, self.cid, self.created, self.updated) def save(self, *args, **kwargs): self.clean() return super(CidUserAnswer, self).save(*args, **kwargs) def clean(self): - for ans in (self.answer_observations, self.answer_interpretation, self.answer_principle_diagnosis, self.answer_differential_diagnosis, self.answer_management): + for ans in ( + self.answer_observations, + self.answer_interpretation, + self.answer_principle_diagnosis, + self.answer_differential_diagnosis, + self.answer_management, + ): if ans: ans = ans.strip() - def is_marked(self): if self.score == "": return False @@ -813,10 +827,10 @@ class CidUserAnswer(models.Model): if count == 1: return f"Marked by {mark_objects.first().marker} (awaiting second marker)" - + s = ", ".join(f"{str(i.marker)} ({i.score})" for i in mark_objects) return f"Marked by {s}" - + def get_markers(self): mark_objects = self.mark.all() if not mark_objects: @@ -845,23 +859,28 @@ class CidUserAnswer(models.Model): return reverse("longs:user_answer_view", kwargs={"pk": self.pk}) - - - @reversion.register class AnswerMarks(models.Model): - score = models.CharField( - max_length=3, choices=CidUserAnswer.ScoreOptions.choices + score = models.CharField(max_length=3, choices=CidUserAnswer.ScoreOptions.choices) + + mark_reason = models.TextField( + null=True, + blank=True, + help_text="Reason for the given mark - not visible to candidates", ) - mark_reason = models.TextField(null=True, blank=True, help_text="Reason for the given mark - not visible to candidates") - - candidate_feedback = models.TextField(null=True, blank=True, help_text="Feedback for the candidate") + candidate_feedback = models.TextField( + null=True, blank=True, help_text="Feedback for the candidate" + ) marker = models.ForeignKey( settings.AUTH_USER_MODEL, related_name="marker", on_delete=models.CASCADE ) user_answer = models.ForeignKey( - CidUserAnswer, related_name="mark", on_delete=models.SET_NULL, null=True, blank=True - ) \ No newline at end of file + CidUserAnswer, + related_name="mark", + on_delete=models.SET_NULL, + null=True, + blank=True, + )