diff --git a/anatomy/migrations/0028_auto_20210206_0905.py b/anatomy/migrations/0028_auto_20210206_0905.py new file mode 100644 index 00000000..941fb6d0 --- /dev/null +++ b/anatomy/migrations/0028_auto_20210206_0905.py @@ -0,0 +1,24 @@ +# Generated by Django 3.1.3 on 2021-02-06 09:05 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('anatomy', '0027_anatomyquestion_examination'), + ] + + operations = [ + migrations.AlterField( + model_name='anatomyquestion', + name='open_access', + field=models.BooleanField(default=True, help_text='If a question should be freely available to browse'), + ), + migrations.AlterField( + model_name='anatomyquestion', + name='question_type', + field=models.ForeignKey(default=1, null=True, on_delete=django.db.models.deletion.SET_NULL, to='anatomy.questiontype'), + ), + ] diff --git a/anatomy/models.py b/anatomy/models.py index 1b45daca..0e0cd9ea 100644 --- a/anatomy/models.py +++ b/anatomy/models.py @@ -68,7 +68,7 @@ class AnatomyQuestion(models.Model): # image = models.ImageField() # feedback = models.TextField(null=True) question_type = models.ForeignKey( - QuestionType, on_delete=models.SET_NULL, null=True + QuestionType, on_delete=models.SET_NULL, null=True, default=1 ) image = models.ImageField(upload_to=image_directory_path, storage=image_storage) @@ -108,7 +108,7 @@ class AnatomyQuestion(models.Model): created_date = models.DateTimeField(default=timezone.now) open_access = models.BooleanField( - help_text="If an question should be freely available to browse", default=True + help_text="If a question should be freely available to browse", default=True ) author = models.ManyToManyField( diff --git a/longs/migrations/0010_auto_20210206_0905.py b/longs/migrations/0010_auto_20210206_0905.py new file mode 100644 index 00000000..11ffd999 --- /dev/null +++ b/longs/migrations/0010_auto_20210206_0905.py @@ -0,0 +1,26 @@ +# Generated by Django 3.1.3 on 2021-02-06 09:05 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('longs', '0009_auto_20210205_1146'), + ] + + operations = [ + migrations.AddField( + model_name='longseries', + name='author', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='long_series', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='long', + name='open_access', + field=models.BooleanField(default=True, help_text='If a question should be freely available to browse'), + ), + ] diff --git a/longs/models.py b/longs/models.py index 7881884f..59a26178 100644 --- a/longs/models.py +++ b/longs/models.py @@ -22,9 +22,11 @@ import string from anatomy.models import Modality from generic.models import Examination, Condition, Sign -#from generic.models import Examination, Site, Condition, Sign + +# from generic.models import Examination, Site, Condition, Sign from easy_thumbnails.files import get_thumbnailer +from easy_thumbnails.exceptions import InvalidImageFormatError image_storage = FileSystemStorage( # Physical file location ROOT @@ -33,21 +35,23 @@ image_storage = FileSystemStorage( base_url=u"{0}longs/".format(settings.MEDIA_URL), ) + def image_directory_path(instance, filename): return u"picture/{0}".format(filename) def findMiddle(input_list): - middle = float(len(input_list))/2 + middle = float(len(input_list)) / 2 if middle % 2 != 0: - return input_list[int(middle - .5)] + return input_list[int(middle - 0.5)] else: - return (input_list[int(middle)]) - return (input_list[int(middle)], input_list[int(middle-1)]) + return input_list[int(middle)] + return (input_list[int(middle)], input_list[int(middle - 1)]) + class Long(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() history = models.TextField(null=True, blank=True) feedback = models.TextField(null=True, blank=True) @@ -55,12 +59,12 @@ class Long(models.Model): 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 \"...\"" + 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( - to=Sign, blank=True, help_text='Radiological signs in the question') + to=Sign, blank=True, help_text="Radiological signs in the question" + ) # Model answers model_observations = models.TextField(null=True, blank=True) @@ -69,9 +73,8 @@ class Long(models.Model): model_differential_diagnosis = models.TextField(null=True, blank=True) model_management = models.TextField(null=True, blank=True) - DEFAULT_SITE_ID = 1 - #site = models.ManyToManyField( + # site = models.ManyToManyField( # Site, # blank=True, # default=DEFAULT_SITE_ID, @@ -80,22 +83,23 @@ class Long(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="long_authored_questions") - - scrapped = models.BooleanField( - default=False, - help_text='Question has been scrapped and will not be shown') - - open_access = models.BooleanField( - help_text="If an question should be freely available to browse", default=True + author = models.ManyToManyField( + settings.AUTH_USER_MODEL, + blank=True, + help_text="Author of question", + related_name="long_authored_questions", ) + scrapped = models.BooleanField( + 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 + ) def get_absolute_url(self): - return reverse('longs:long_detail', kwargs={'pk': self.pk}) + return reverse("longs:long_detail", kwargs={"pk": self.pk}) def get_authors(self): """Returns a comma seperated text list of authors""" @@ -116,7 +120,6 @@ class Long(models.Model): exams = ", ".join(e) return exams - def GetImages(self, feedback=False): qs = self.images.all() @@ -128,28 +131,64 @@ class Long(models.Model): return images def GetImageUrls(self): - return ",".join(["http://penracourses.org.uk{}".format(i.url) for i in self.GetImages()]) - #def GetNonFeedbackQuestionImages(self): - #return self.GetImages() + return ",".join( + ["http://penracourses.org.uk{}".format(i.url) for i in self.GetImages()] + ) + + # def GetNonFeedbackQuestionImages(self): + # return self.GetImages() + class LongSeriesImage(models.Model): image = models.ImageField(upload_to=image_directory_path, storage=image_storage) position = models.IntegerField(default=0) - series = models.ForeignKey("LongSeries", related_name="images", on_delete=models.SET_NULL, null=True) + series = models.ForeignKey( + "LongSeries", related_name="images", on_delete=models.SET_NULL, null=True + ) class LongSeries(models.Model): - modality = models.ForeignKey(Modality, related_name="series_modality", on_delete=models.SET_NULL, null=True) + modality = models.ForeignKey( + Modality, related_name="series_modality", on_delete=models.SET_NULL, null=True + ) examination = models.ForeignKey( - Examination, help_text="Name of the examination", related_name="series_examination", on_delete=models.SET_NULL, null=True) - long = models.ForeignKey("Long", help_text="The question this series should be associated with", related_name="series", on_delete=models.SET_NULL, null=True) - description = models.TextField(blank=True, help_text="Description of stack, for admin organisation, will not be visible when taking") + Examination, + help_text="Name of the examination", + related_name="series_examination", + on_delete=models.SET_NULL, + null=True, + ) + long = models.ForeignKey( + "Long", + help_text="The question this series should be associated with", + related_name="series", + on_delete=models.SET_NULL, + null=True, + ) + description = models.TextField( + blank=True, + help_text="Description of stack, for admin organisation, will not be visible when taking", + ) + + author = models.ForeignKey( + settings.AUTH_USER_MODEL, + blank=True, + null=True, + related_name="long_series", + on_delete=models.CASCADE, + ) def __str__(self): - return "{} : {} [{}]".format(self.get_examination(), self.description, self.long.pk) + return "{} : {} [{}]".format( + self.get_examination(), self.description, self.long.pk + ) def get_author_objects(self): """Returns a comma seperated text list of authors""" + if self.author: + return self.author.all() + else: + return ["None"] if self.long: authors = [i for i in self.long.author.all()] else: @@ -157,15 +196,21 @@ class LongSeries(models.Model): return authors + def get_author_display(self): + return ", ".join(self.get_author_objects()) + def get_examination(self): """Returns a comma seperated text list of regions""" return str(self.examination) def get_absolute_url(self): - return reverse('longs:long_series_detail', kwargs={'pk': self.pk}) + return reverse("longs:long_series_detail", kwargs={"pk": self.pk}) def get_image_urls(self, feedback=False): - images = ["http://penracourses.org.uk{}".format(i.image.url) for i in self.images.all()] + images = [ + "http://penracourses.org.uk{}".format(i.image.url) + for i in self.images.all() + ] return ",".join(images) @@ -173,21 +218,25 @@ class LongSeries(models.Model): images = self.images.all() if len(images) < 1: - return 'No images', 0 + return "No images", 0 img = findMiddle(images).image - thumbnailer = get_thumbnailer(img) + try: + thumbnailer = get_thumbnailer(img) + except InvalidImageFormatError: + return format_html('Invalid image url', image_object) thumbnail = thumbnailer["exam-list"] return ''.format(thumbnail), len(images) - - def get_block(self): examination = self.get_examination() thumb, image_number = self.get_thumbnail() - return format_html('
{}
{}
Images: {}
', examination, thumb, image_number ) + return format_html( + "
{}
{}
Images: {}
", examination, thumb, image_number + ) -#class LongImage(models.Model): + +# class LongImage(models.Model): # long = models.ForeignKey(Long, # related_name="images", # on_delete=models.CASCADE) @@ -207,47 +256,43 @@ class LongSeries(models.Model): class Note(models.Model): - long = models.ForeignKey(Long, - related_name="long_notes", - on_delete=models.CASCADE, - null=True) - #author = models.ForeignKey(User, - author = models.ForeignKey( settings.AUTH_USER_MODEL, - related_name="long_notes", - on_delete=models.CASCADE) + long = models.ForeignKey( + Long, related_name="long_notes", on_delete=models.CASCADE, null=True + ) + # author = models.ForeignKey(User, + author = models.ForeignKey( + settings.AUTH_USER_MODEL, related_name="long_notes", on_delete=models.CASCADE + ) note = models.TextField() created_on = models.DateTimeField(auto_now_add=True) def get_absolute_url(self): self.pk = self.long_id - return reverse('longs:long_detail', kwargs={'pk': self.pk}) + return reverse("longs:long_detail", kwargs={"pk": self.pk}) def __str__(self): return "{} [{}] {}".format(self.long, self.author, self.created_on) class LongCreationDefault(models.Model): - #author = models.OneToOneField(User, + # author = models.OneToOneField(User, author = models.OneToOneField( - settings.AUTH_USER_MODEL, - related_name="long_default", - on_delete=models.CASCADE) + settings.AUTH_USER_MODEL, related_name="long_default", on_delete=models.CASCADE + ) - #site = models.ManyToManyField( + # site = models.ManyToManyField( # Site, # blank=True, # default=1, # help_text="Default site to use when creating a new long") def get_absolute_url(self): - return reverse('longs:long_create') + return reverse("longs:long_create") class Exam(models.Model): name = models.CharField(max_length=200) - exam_questions = SortedManyToManyField( - Long, related_name="exams", blank="true" - ) + exam_questions = SortedManyToManyField(Long, related_name="exams", blank="true") active = models.BooleanField( help_text="If an exam should be available", default=True @@ -262,7 +307,8 @@ class Exam(models.Model): ) 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, ) def __str__(self): @@ -280,6 +326,7 @@ class Exam(models.Model): def get_question_index(self, question): return list(self.exam_questions.all()).index(question) + class CidUserAnswer(models.Model): """User answers by candidate""" @@ -287,7 +334,6 @@ class CidUserAnswer(models.Model): Long, related_name="cid_user_answers", on_delete=models.CASCADE ) - # Answers answer_observations = models.TextField(null=True, blank=True) answer_interpretation = models.TextField(null=True, blank=True) @@ -295,7 +341,11 @@ class CidUserAnswer(models.Model): answer_differential_diagnosis = models.TextField(null=True, blank=True) answer_management = models.TextField(null=True, 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( @@ -323,7 +373,7 @@ class CidUserAnswer(models.Model): self.answer = self.answer.strip() s = self.answer.lower() - s = s.translate(str.maketrans('', '', string.punctuation)) + s = s.translate(str.maketrans("", "", string.punctuation)) self.answer_compare = s diff --git a/longs/tables.py b/longs/tables.py index 030c0036..6d704c85 100755 --- a/longs/tables.py +++ b/longs/tables.py @@ -17,12 +17,14 @@ class ImageColumn(tables.Column): if obj is None: return format_html('No image') - image_object = obj.image - try: - thumbnailer = get_thumbnailer(image_object) - return format_html('', thumbnailer["exam-list"]) - except InvalidImageFormatError: - return format_html('Invalid image url', image_object) + return obj.get_thumbnail()[0] + + #image_object = obj.image + #try: + # thumbnailer = get_thumbnailer(image_object) + # return format_html('', thumbnailer["exam-list"]) + #except InvalidImageFormatError: + # return format_html('Invalid image url', image_object) class LongTable(tables.Table): @@ -38,11 +40,11 @@ class LongTable(tables.Table): text='Clone', args=[A('pk')], orderable=False) - images = ImageColumn("images", orderable=False) + series = ImageColumn("images", orderable=False) class Meta: model = Long template_name = "django_tables2/bootstrap4.html" fields = ("normal", "abnormality", "region", "examination", "laterality", "site", "created_date", "author") - sequence = ("view", "images") \ No newline at end of file + sequence = ("view", "series") \ No newline at end of file diff --git a/longs/templates/longs/base.html b/longs/templates/longs/base.html index 1d87cea9..1d8e2670 100755 --- a/longs/templates/longs/base.html +++ b/longs/templates/longs/base.html @@ -19,7 +19,7 @@ Longs: {% if request.user.is_authenticated %} Exams / Questions / -Create Question +Create Case / Create Series {% endif %} {% comment %}
diff --git a/longs/templates/longs/long_display_block.html b/longs/templates/longs/long_display_block.html index 28106b49..e3d4d0a2 100755 --- a/longs/templates/longs/long_display_block.html +++ b/longs/templates/longs/long_display_block.html @@ -18,7 +18,8 @@ >Popup
{% endfor %} - Add series + Add new series
+ Add existing series

Feedback: {{ question.feedback }}

Author(s): {% for author in question.author.all %} Submit Long Case -Use this form to create a long case. Once the case has been created it can be associated with image sets (series) +Use this form to create a long case. Associated image sets can be added using this form however the images will have to be uploaded once the case has been created.

{% csrf_token %} diff --git a/longs/templates/longs/long_series_viewer.html b/longs/templates/longs/long_series_viewer.html index fae68ced..78bfa7dc 100755 --- a/longs/templates/longs/long_series_viewer.html +++ b/longs/templates/longs/long_series_viewer.html @@ -1,4 +1,6 @@
{{ series.modality}}, {{ series.examination }}
-
\ No newline at end of file +
+ +
Author: {{ series.get_author_display }}
\ No newline at end of file diff --git a/longs/views.py b/longs/views.py index 936ac981..a4a3e5dc 100755 --- a/longs/views.py +++ b/longs/views.py @@ -294,7 +294,7 @@ class LongSeriesCreate(LoginRequiredMixin, CreateView): self.object = form.save(commit=False) self.object.save() - #form.instance.author.add(self.request.user.id) + form.instance.author.add(self.request.user.id) context = self.get_context_data(form=form) series_formset = context["image_formset"] @@ -336,6 +336,8 @@ class LongSeriesUpdate(LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateV #self.object.exams.clear() #self.object.exams.add(*new_exams) + form.instance.author.add(self.request.user.id) + context = self.get_context_data(form=form) image_formset = context["image_formset"] # logger.debug(formset.is_valid()) diff --git a/rapids/migrations/0012_auto_20210206_0905.py b/rapids/migrations/0012_auto_20210206_0905.py new file mode 100644 index 00000000..64063734 --- /dev/null +++ b/rapids/migrations/0012_auto_20210206_0905.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.3 on 2021-02-06 09:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rapids', '0011_auto_20210205_1056'), + ] + + operations = [ + migrations.AlterField( + model_name='rapid', + name='open_access', + field=models.BooleanField(default=True, help_text='If a question should be freely available to browse'), + ), + ] diff --git a/rapids/models.py b/rapids/models.py index 9c080996..7dadb27a 100644 --- a/rapids/models.py +++ b/rapids/models.py @@ -168,7 +168,7 @@ class Rapid(models.Model): help_text='Question has been scrapped and will not be shown') open_access = models.BooleanField( - help_text="If an question should be freely available to browse", default=True + help_text="If a question should be freely available to browse", default=True )