From 508934f94c5a0bfb2b72dea73f28f5b4e6b12d10 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 18 Jan 2021 11:18:07 +0000 Subject: [PATCH] merge rapids commit --- rad/urls.py | 2 +- rapids/__init__.py | 0 rapids/admin.py | 57 +++ rapids/apps.py | 0 rapids/decorators.py | 13 + rapids/filters.py | 10 + rapids/forms.py | 102 ++++ rapids/models.py | 388 ++++++++++++++- rapids/tables.py | 26 + rapids/templates/admin/base_site.html | 5 + rapids/templates/rapids/author_list.html | 8 + rapids/templates/rapids/base.html | 54 +++ rapids/templates/rapids/category_detail.html | 13 + rapids/templates/rapids/create_simple.html | 5 + rapids/templates/rapids/index.html | 23 + rapids/templates/rapids/note_form.html | 12 + rapids/templates/rapids/rapid_detail.html | 11 + .../templates/rapids/rapid_display_block.html | 32 ++ rapids/templates/rapids/rapid_form.html | 251 ++++++++++ .../rapids/rapidcreationdefault_form.html | 10 + rapids/templates/rapids/view.html | 18 + rapids/tests.py | 0 rapids/urls.py | 46 ++ rapids/views.py | 451 +++++++++++++++++- 24 files changed, 1533 insertions(+), 4 deletions(-) mode change 100644 => 100755 rapids/__init__.py mode change 100644 => 100755 rapids/admin.py mode change 100644 => 100755 rapids/apps.py create mode 100755 rapids/decorators.py create mode 100755 rapids/filters.py create mode 100755 rapids/forms.py create mode 100755 rapids/tables.py create mode 100755 rapids/templates/admin/base_site.html create mode 100755 rapids/templates/rapids/author_list.html create mode 100755 rapids/templates/rapids/base.html create mode 100755 rapids/templates/rapids/category_detail.html create mode 100755 rapids/templates/rapids/create_simple.html create mode 100755 rapids/templates/rapids/index.html create mode 100755 rapids/templates/rapids/note_form.html create mode 100755 rapids/templates/rapids/rapid_detail.html create mode 100755 rapids/templates/rapids/rapid_display_block.html create mode 100755 rapids/templates/rapids/rapid_form.html create mode 100755 rapids/templates/rapids/rapidcreationdefault_form.html create mode 100755 rapids/templates/rapids/view.html mode change 100644 => 100755 rapids/tests.py create mode 100755 rapids/urls.py mode change 100644 => 100755 rapids/views.py diff --git a/rad/urls.py b/rad/urls.py index 83b113f2..8b520e69 100644 --- a/rad/urls.py +++ b/rad/urls.py @@ -27,7 +27,7 @@ urlpatterns = [ path("anatomy/", include("anatomy.urls"), name="anatomy"), path("physics/", include("physics.urls"), name="physics"), #path("sbas/", include("sbas.urls"), name="sbas"), - #path("rapids/", include("rapids.urls"), name="rapids"), + path("rapids/", include("rapids.urls"), name="rapids"), path("accounts/", include("django.contrib.auth.urls")), path("accounts/profile", views.profile, name="profile"), path("", TemplateView.as_view(template_name="index.html"), name="home"), diff --git a/rapids/__init__.py b/rapids/__init__.py old mode 100644 new mode 100755 diff --git a/rapids/admin.py b/rapids/admin.py old mode 100644 new mode 100755 index 8c38f3f3..2a2cd8b5 --- a/rapids/admin.py +++ b/rapids/admin.py @@ -1,3 +1,60 @@ from django.contrib import admin +from .models import Rapid, RapidImage, Examination, Site, Abnormality, Region, Note, RapidCreationDefault +import tagulous.admin + +from django.forms import ModelForm + +from reversion.admin import VersionAdmin +from django.forms.widgets import RadioSelect # Register your models here. +admin.site.register(Examination) +admin.site.register(Site) +admin.site.register(Abnormality) +admin.site.register(Region) +admin.site.register(Note) +admin.site.register(RapidCreationDefault) + + +class RapidImageInline(admin.TabularInline): + model = RapidImage + extra = 1 + readonly_fields = [ + "image_tag", + ] + + +class RapidAdminForm(ModelForm): + class Meta: + model = Rapid + + fields = [ + "normal", "abnormality", "region", "laterality", "examination", + "condition", "sign", "site", "feedback", "author" + ] + + widgets = { + "laterality": RadioSelect(attrs={'style': 'display: inline-flex'}), + } + + +class RapidAdmin(VersionAdmin): + form = RapidAdminForm + + filter_horizontal = ( + "abnormality", + "region", + "examination", + ) + save_on_top = True + save_as = True + view_on_site = True + + inlines = [ + RapidImageInline, + ] + + +admin.site.register(Rapid, RapidAdmin) + +#tagulous.admin.register(Rapid.condition) diff --git a/rapids/apps.py b/rapids/apps.py old mode 100644 new mode 100755 diff --git a/rapids/decorators.py b/rapids/decorators.py new file mode 100755 index 00000000..72c1c36b --- /dev/null +++ b/rapids/decorators.py @@ -0,0 +1,13 @@ +from django.core.exceptions import PermissionDenied +from .models import Rapid + +def user_is_author_or_rapid_checker(function): + def wrap(request, *args, **kwargs): + rapid = Rapid.objects.get(pk=kwargs['pk']) + if request.user in rapid.author.all() or request.user.groups.filter(name='rapid_checker').exists(): + return function(request, *args, **kwargs) + else: + raise PermissionDenied + wrap.__doc__ = function.__doc__ + wrap.__name__ = function.__name__ + return wrap diff --git a/rapids/filters.py b/rapids/filters.py new file mode 100755 index 00000000..3f10b4f6 --- /dev/null +++ b/rapids/filters.py @@ -0,0 +1,10 @@ +import django_filters + +from .models import Rapid + + +class RapidFilter(django_filters.FilterSet): + class Meta: + model = Rapid + fields = ("normal", "abnormality", "region", "examination", + "laterality", "site", "created_date", "author") diff --git a/rapids/forms.py b/rapids/forms.py new file mode 100755 index 00000000..85c8278a --- /dev/null +++ b/rapids/forms.py @@ -0,0 +1,102 @@ +from django.forms import ModelForm, ModelMultipleChoiceField, ModelChoiceField, ChoiceField +from django.forms import inlineformset_factory + +from rapids.models import Abnormality, Examination, Note, Rapid, RapidCreationDefault, RapidImage, Region + +from django.contrib.admin.widgets import FilteredSelectMultiple +from django.forms.widgets import RadioSelect + + +class ExaminationForm(ModelForm): + class Meta: + model = Examination + fields = ['examination'] + + +class NoteForm(ModelForm): + class Meta: + model = Note + fields = ['note'] + + +class RegionForm(ModelForm): + class Meta: + model = Region + fields = ['name'] + + +class AbnormalityForm(ModelForm): + class Meta: + model = Abnormality + fields = ['name'] + + +class RapidCreationDefaultForm(ModelForm): + class Meta: + model = RapidCreationDefault + fields = ['site'] + exclude = ["author"] + + +class RapidForm(ModelForm): + class Media: + # Django also includes a few javascript files necessary + # for the operation of this form element. You need to + # include + # in the template. + css = { + 'all': ['css/widgets.css'], + } + # Adding this javascript is crucial + js = ['jsi18n.js', "tesseract.min.js"] + + def __init__(self, *args, **kwargs): + super(RapidForm, self).__init__(*args, **kwargs) + #self.fields['question'].widget.attrs = {'class': 'question-form', 'rows': 10, 'cols': 100} + #self.fields['feedback'].widget.attrs = {'class': 'feedback-form', 'rows': 10, 'cols': 100} + self.fields['abnormality'] = ModelMultipleChoiceField( + required=False, + queryset=Abnormality.objects.all(), + widget=FilteredSelectMultiple(verbose_name="Abnormality", + is_stacked=False)) + + self.fields['region'] = ModelMultipleChoiceField( + required=False, + queryset=Region.objects.all(), + widget=FilteredSelectMultiple(verbose_name="Region", + is_stacked=False)) + + self.fields['examination'] = ModelMultipleChoiceField( + queryset=Examination.objects.all(), + widget=FilteredSelectMultiple(verbose_name="Examination", + is_stacked=False)) + + self.fields['laterality'] = ChoiceField( + choices=Rapid.LATERALITY_CHOICES, + required=True, + widget=RadioSelect()) + + class Meta: + model = Rapid + #fields = ['due_back'] + #fields = '__all__' + fields = [ + "normal", "abnormality", "region", "laterality", "examination", + "condition", "sign", "site", "feedback" + ] + #fields = ['question', 'feedback', 'subspecialty', 'references'] + widgets = { + # "normal": RadioSelect( + # choices=[(True, 'Yes'), + # (False, 'No')]) + } + + +ImageFormSet = inlineformset_factory(Rapid, + RapidImage, + fields=['image', 'feedback_image'], + exclude=[], + can_delete=True, + extra=4, + max_num=10, + field_classes="testing") diff --git a/rapids/models.py b/rapids/models.py index 71a83623..ddd997d0 100644 --- a/rapids/models.py +++ b/rapids/models.py @@ -1,3 +1,389 @@ from django.db import models +from django.utils import timezone -# Create your models here. +from django.core.files.storage import FileSystemStorage +from django.conf import settings +from django.utils.html import format_html + +from django.urls import reverse + +from django.utils.translation import ugettext_lazy as _ + +from sortedm2m.fields import SortedManyToManyField + +import string + +image_storage = FileSystemStorage( + # Physical file location ROOT + location=u"{0}/rapids/".format(settings.MEDIA_ROOT), + # Url for file + base_url=u"{0}rapids/".format(settings.MEDIA_URL), +) + +def image_directory_path(instance, filename): + return u"picture/{0}".format(filename) + + +class AnswerStrings(models.Model): + text = models.CharField(max_length=255, unique=True) + + def __str__(self): + return self.text + + +class Abnormality(models.Model): + 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', ) + + +class Region(models.Model): + name = models.CharField( + max_length=200, + unique=True, + help_text="Region of the abnormality (not including lateralitiy)", + blank=True) + + def __str__(self): + return self.name + + class Meta: + ordering = ('name', ) + + +class Examination(models.Model): + examination = models.CharField(max_length=200) + + def __str__(self): + return self.examination + + class Meta: + ordering = ('examination', ) + + +class Site(models.Model): + site = models.CharField(max_length=200) + initials = models.CharField(max_length=200) + + def __str__(self): + return self.site + + +#class Condition(tagulous.models.TagModel): +# pass + +class Answer(models.Model): + question = models.ForeignKey( + Rapid, related_name="answers", on_delete=models.CASCADE + ) + answer = models.TextField(max_length=500) + answer_compare = models.TextField(max_length=500) + + class MarkOptions(models.TextChoices): + UNMARKED = "", _("Unmarked") + INCORRECT = "0", _("Incorrect") + HALF_MARK = "1", _("Half mark") + CORRECT = "2", _("Correct") + + status = models.CharField( + max_length=1, choices=MarkOptions.choices, default=MarkOptions.UNMARKED + ) + + def __str__(self): + return self.answer + + def save(self, *args, **kwargs): + self.clean() + return super(Answer, self).save(*args, **kwargs) + + def clean(self): + if self.answer: + self.answer = self.answer.strip() + + s = self.answer.lower() + s = s.translate(str.maketrans('', '', string.punctuation)) + + self.answer_compare = s + + +class Rapid(models.Model): + #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) + + normal = models.BooleanField(default=False, help_text="Tick if true") + + NONE = "NONE" + LEFT = "LEFT" + RIGHT = "RIGHT" + BILATERAL = "BILAT" + + LATERALITY_CHOICES = ( + (NONE, "None"), + (LEFT, "Left"), + (RIGHT, "Right"), + (BILATERAL, "Bilateral"), + ) + + abnormality = models.ManyToManyField( + Abnormality, + blank=True, + help_text="The abnormality (laterality and region independent)", + ) + region = models.ManyToManyField( + Region, + blank=True, + help_text="Region of the abnormality (laterality independent)") + examination = models.ManyToManyField( + 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( + 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( + to=Sign, blank=True, help_text='Radiological signs in the question') + + DEFAULT_SITE_ID = 1 + site = models.ManyToManyField( + Site, + blank=True, + default=DEFAULT_SITE_ID, + help_text= + "If we know the source of the image (should possibly be removed?)") + 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") + + scrapped = models.BooleanField( + default=False, + help_text='Question has been scrapped and will not be shown') + + def get_absolute_url(self): + return reverse('rapids:rapid_detail', kwargs={'pk': self.pk}) + + def get_authors(self): + """Returns a comma seperated text list of authors""" + authors = ", ".join([i.username for i in self.author.all()]) + return authors + + def get_regions(self): + """Returns a comma seperated text list of regions""" + regions = ", ".join([i.name for i in self.region.all()]) + return regions + + def get_abnormalities(self): + """Returns a comma seperated text list of regions""" + abnormalities = ", ".join([i.name for i in self.abnormality.all()]) + return abnormalities + + def __str__(self): + n = "Normal" + if not self.normal: + #n = self.answers.first() + n = "{} / {}".format(self.abnormality.first(), self.region.first()) + exams = ", ".join([str(i) for i in self.examination.all()]) + lat = "" + if self.laterality != "NONE": + lat = self.laterality + return "{} / {} {}".format(exams, lat, n) + + +class RapidImage(models.Model): + rapid = models.ForeignKey(Rapid, + related_name="images", + on_delete=models.CASCADE) + image = models.ImageField(upload_to=image_directory_path, storage=image_storage) + + feedback_image = models.BooleanField(default=False) + + def image_tag(self): + if self.image: + return mark_safe( + 'Click and hold to zoom' + .format(settings.MEDIA_URL, self.image)) + else: + return "" + + image_tag.short_description = 'Image' + + +class Note(models.Model): + rapid = models.ForeignKey(Rapid, + related_name="rapid_notes", + on_delete=models.CASCADE, + null=True) + author = models.ForeignKey(User, + related_name="rapid_notes", + on_delete=models.CASCADE) + note = models.TextField() + created_on = models.DateTimeField(auto_now_add=True) + + def get_absolute_url(self): + self.pk = self.rapid_id + return reverse('rapids:rapid_detail', kwargs={'pk': self.pk}) + + def __str__(self): + return "{} [{}] {}".format(self.rapid, self.author, self.created_on) + + +class RapidCreationDefault(models.Model): + author = models.OneToOneField(User, + related_name="rapid_default", + on_delete=models.CASCADE) + + site = models.ManyToManyField( + Site, + blank=True, + default=1, + help_text="Default site to use when creating a new rapid") + + def get_absolute_url(self): + return reverse('rapids:rapid_create') + + +class AnatomyQuestion(models.Model): + # author = models.ForeignKey('auth.User', on_delete=models.CASCADE) + # image = models.ImageField() + # feedback = models.TextField(null=True) + question_type = models.ForeignKey( + QuestionType, on_delete=models.SET_NULL, null=True + ) + + image = models.ImageField(upload_to=image_directory_path, storage=image_storage) + + image_annotations = models.TextField( + blank=True, + help_text="Stores a JSON representation of annotations to be applied by cornerstonetools", + ) + + description = models.CharField( + max_length=400, + help_text="Short description of the image e.g. 'Sagittal CT Chest, Abdomen & Pelvis', will be displayed as the title.", + ) + + examination = models.ForeignKey( + Examination, on_delete=models.SET_NULL, null=True, blank=True + ) + modality = models.ForeignKey( + Modality, + on_delete=models.SET_NULL, + null=True, + help_text="Modality of the image", + ) + region = models.ForeignKey( + Region, + on_delete=models.SET_NULL, + null=True, + blank=True, + help_text="Region the image covers", + ) + body_part = models.ForeignKey( + BodyPart, on_delete=models.SET_NULL, null=True, blank=True + ) + structure = models.ForeignKey( + Structure, on_delete=models.SET_NULL, null=True, blank=True + ) + created_date = models.DateTimeField(default=timezone.now) + + 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(s) of question", + related_name="anatomy_authored_questions", + ) + + def __str__(self): + # Get first answer + return "{} [{}, {}]".format( + self.GetPrimaryAnswer(), self.modality, self.structure + ) + # Get associated exams + e = self.exams.all().values_list("name", flat=True) + exams = ", ".join(e) + + if a is None: + return "({})".format(exams) + else: + return "{} ({}) [{}, {}]".format( + a.answer, exams, self.modality, self.structure + ) + + def get_absolute_url(self): + return reverse("anatomy:question_detail", kwargs={"pk": self.pk}) + + def GetPrimaryAnswer(self): + if len(self.answers.all()) > 0: + return self.answers.all().first().answer + else: + return "None yet..." + + def GetExams(self): + e = self.exams.all().values_list("name", flat=True) + exams = ", ".join(e) + return exams + + def GetUnmarkedAnswersString(self): + unmarked_answers = self.GetUnmarkedAnswers() + + if not unmarked_answers: + return "No answers to mark" + return format_html( + "{} answer unmarked: {}".format( + len(unmarked_answers), ", ".join(unmarked_answers) + ) + ) + + def GetUnmarkedAnswers(self): + user_answers = set( + [i.answer_compare for i in self.cid_user_answers.all()] + ) + unmarked_answers = user_answers - self.GetMarkedAnswers() + + return unmarked_answers + + def GetUnmarkedAnswerCount(self): + return len(self.GetUnmarkedAnswers()) + + def GetMarkedAnswers(self): + return set( + [ + i.answer_compare + for i in self.answers.all() + if i.status != i.MarkOptions.UNMARKED + ] + ) + correct_answers = set( + [i.answer.get_compare_string() for i in self.answers.all()] + ) + half_mark_answers = set( + [i.answer.get_compare_string() for i in self.half_mark_answers.all()] + ) + incorrect_answers = set( + [i.answer.get_compare_string() for i in self.incorrect_answers.all()] + ) + + marked_answers = correct_answers | half_mark_answers | incorrect_answers + return marked_answers \ No newline at end of file diff --git a/rapids/tables.py b/rapids/tables.py new file mode 100755 index 00000000..a11c49ce --- /dev/null +++ b/rapids/tables.py @@ -0,0 +1,26 @@ +import django_tables2 as tables +from django_tables2.utils import A + +from .models import Rapid + + +class RapidTable(tables.Table): + edit = tables.LinkColumn('rapids:rapid_update', + text='Edit', + args=[A('pk')], + orderable=False) + view = tables.LinkColumn('rapids:rapid_detail', + text='View', + args=[A('pk')], + orderable=False) + clone = tables.LinkColumn('rapids:rapid_clone', + text='Clone', + args=[A('pk')], + orderable=False) + + class Meta: + model = Rapid + template_name = "django_tables2/bootstrap4.html" + fields = ("normal", "abnormality", "region", "examination", + "laterality", "site", "created_date", "author") + sequence = ("view", ) \ No newline at end of file diff --git a/rapids/templates/admin/base_site.html b/rapids/templates/admin/base_site.html new file mode 100755 index 00000000..792249f6 --- /dev/null +++ b/rapids/templates/admin/base_site.html @@ -0,0 +1,5 @@ +{% extends "admin/base_site.html" %} +{% load static %} +{% block extrahead %} + +{% endblock %} \ No newline at end of file diff --git a/rapids/templates/rapids/author_list.html b/rapids/templates/rapids/author_list.html new file mode 100755 index 00000000..82564136 --- /dev/null +++ b/rapids/templates/rapids/author_list.html @@ -0,0 +1,8 @@ +{% extends 'rapids/base.html' %} + +{% block content %} + +{% for author in authors %} +

Author: {{author.username}},

+{% endfor %} +{% endblock %} \ No newline at end of file diff --git a/rapids/templates/rapids/base.html b/rapids/templates/rapids/base.html new file mode 100755 index 00000000..e762e551 --- /dev/null +++ b/rapids/templates/rapids/base.html @@ -0,0 +1,54 @@ +{% load static %} + + + + + {% block css %} + {% endblock %} + + + + + + + + + + + + + {% block js %} + {% endblock %} + + + + + Rapids + + + +
+ {% block navigation %} + {% endblock %} +
+
+ {% block content %} + {% endblock %} +
+
+
+ + + \ No newline at end of file diff --git a/rapids/templates/rapids/category_detail.html b/rapids/templates/rapids/category_detail.html new file mode 100755 index 00000000..0d8e840e --- /dev/null +++ b/rapids/templates/rapids/category_detail.html @@ -0,0 +1,13 @@ +{% extends 'rapids/base.html' %} + +{% block content %} +{{category}}: + + +{% endblock %} \ No newline at end of file diff --git a/rapids/templates/rapids/create_simple.html b/rapids/templates/rapids/create_simple.html new file mode 100755 index 00000000..14e906af --- /dev/null +++ b/rapids/templates/rapids/create_simple.html @@ -0,0 +1,5 @@ +

Add new {{name}}

+
{% csrf_token %} + {{ form.as_p }} + +
\ No newline at end of file diff --git a/rapids/templates/rapids/index.html b/rapids/templates/rapids/index.html new file mode 100755 index 00000000..8bc76cd7 --- /dev/null +++ b/rapids/templates/rapids/index.html @@ -0,0 +1,23 @@ +{% extends 'rapids/base.html' %} + +{% block content %} +My Questions: + + +Other Questions: + + +{% endblock %} \ No newline at end of file diff --git a/rapids/templates/rapids/note_form.html b/rapids/templates/rapids/note_form.html new file mode 100755 index 00000000..89dcd1ae --- /dev/null +++ b/rapids/templates/rapids/note_form.html @@ -0,0 +1,12 @@ +{% extends "rapids/base.html" %} + +{% block content %} +

Add Note

+
+ {% csrf_token %} + + {{ form.as_table }} +
+ +
+{% endblock %} diff --git a/rapids/templates/rapids/rapid_detail.html b/rapids/templates/rapids/rapid_detail.html new file mode 100755 index 00000000..ffb52dc9 --- /dev/null +++ b/rapids/templates/rapids/rapid_detail.html @@ -0,0 +1,11 @@ +{% extends 'rapids/base.html' %} + +{% block content %} +Edit +Clone + Add Note +{% if request.user.is_superuser %} +Admin Edit +{% endif %} +{% include 'rapids/rapid_display_block.html' %} +{% endblock %} \ No newline at end of file diff --git a/rapids/templates/rapids/rapid_display_block.html b/rapids/templates/rapids/rapid_display_block.html new file mode 100755 index 00000000..3f0f2b58 --- /dev/null +++ b/rapids/templates/rapids/rapid_display_block.html @@ -0,0 +1,32 @@ +
+
+ {{ rapid.created_date }} +
+ +

Rapid: {{ rapid }}

+

Region: {{ rapid.get_regions }}

+

Abnormality: {{ rapid.get_abnormalities }}

+

Images: + {% for image in rapid.images.all %} + Image {{ forloop.counter }}{% if image.feedback_image %} [feedback image]{% endif %}: + + {% endfor %}

+

Feedback: {{ rapid.feedback }}

+

Author(s): {% for author in rapid.author.all %} {{author}}, {% endfor %}

+

Checked by: {% for verified in rapid.verified.all %} {{verified}}, {% endfor %}

+

Scrapped: {{ rapid.scrapped }} (toggle) +

+
+
+ Notes: +
    + {% for note in rapid.rapid_notes.all %} +
  • + {{ note.created_on }} by {{ note.author }}: {{ note.note }} +
  • + {% endfor %} +
+ +
\ No newline at end of file diff --git a/rapids/templates/rapids/rapid_form.html b/rapids/templates/rapids/rapid_form.html new file mode 100755 index 00000000..73097adb --- /dev/null +++ b/rapids/templates/rapids/rapid_form.html @@ -0,0 +1,251 @@ +{% extends "rapids/base.html" %} +{% load static from staticfiles %} + +{% block js %} + + + + +{{ form.media }} +{% endblock %} +{% block content %} +

Submit Rapid

+Edit defaults +
+ {% csrf_token %} + + + + + + {{ form.as_table }} +
+

Images:

+ +
Drop images here (or use the buttons below)
Feedback image?
drop those here
+
+
+
+ {% for form in image_formset %} +
    + {{form.non_field_errors}} + {{form.errors}} + {{ form.as_ul }} +
+ {% endfor %} +
+ {{ image_formset.management_form }} + + +
+ +{% endblock %} \ No newline at end of file diff --git a/rapids/templates/rapids/rapidcreationdefault_form.html b/rapids/templates/rapids/rapidcreationdefault_form.html new file mode 100755 index 00000000..d5ec635d --- /dev/null +++ b/rapids/templates/rapids/rapidcreationdefault_form.html @@ -0,0 +1,10 @@ +{% extends "rapids/base.html" %} + + +{% block content %} +

Defaults for creating new rapids

+
{% csrf_token %} + {{ form.as_p }} + +
+{% endblock %} \ No newline at end of file diff --git a/rapids/templates/rapids/view.html b/rapids/templates/rapids/view.html new file mode 100755 index 00000000..2d6dddd3 --- /dev/null +++ b/rapids/templates/rapids/view.html @@ -0,0 +1,18 @@ +{% extends 'rapids/base.html' %} + +{% load render_table from django_tables2 %} +{% block css %} +{% endblock %} + +{% block content %} + +
+

Filter Rapids

+
+ {{ filter.form }} + +
+
+{% render_table table %} + +{% endblock %} \ No newline at end of file diff --git a/rapids/tests.py b/rapids/tests.py old mode 100644 new mode 100755 diff --git a/rapids/urls.py b/rapids/urls.py new file mode 100755 index 00000000..ea47966a --- /dev/null +++ b/rapids/urls.py @@ -0,0 +1,46 @@ +from django.urls import path, include +from . import views + +app_name = "rapids" + +urlpatterns = [ + # path('', views.question_list, name='question_list'), + path("", views.index, name="index"), + path("author//", views.author_detail, name="author_detail"), + path("author/", views.author_list, name="author_list"), + #path("unchecked/", views.unchecked_list, name="unchecked_list"), + #path("verified//", views.verified_detail, name="verified_detail"), + path("/", views.rapid_detail, name="rapid_detail"), + path("/split", views.rapid_split, name="rapid_split"), + path("/clone", views.RapidClone.as_view(), name="rapid_clone"), + #path("verified/", views.verified, name="verified"), + #path("all_questions/", views.all_questions, name="all_questions"), + path("/scrap", views.rapid_scrap, name="rapid_scrap"), + path("create/", views.RapidCreate.as_view(), name="rapid_create"), + path("create/defaults", + views.RapidCreationDefaultView.as_view(), + name="rapid_create_defaults"), + path("view/", views.RapidView.as_view(), name="rapid_view"), + path("region/create/", views.create_region, name="create_region"), + path("region/ajax/get_region_id", + views.get_region_id, + name="get_region_id"), + path("abnormality/create/", + views.create_abnormality, + name="create_abnormality"), + path("abnormality/ajax/get_abnormality_id", + views.get_abnormality_id, + name="get_abnormality_id"), + path("examination/create/", + views.create_examination, + name="create_examination"), + path("examination/ajax/get_examination_id", + views.get_examination_id, + name="get_examination_id"), + path( + "/update", + views.RapidUpdate.as_view(), + name="rapid_update", + ), + path("/add_note", views.AddNote.as_view(), name="rapid_add_note"), +] diff --git a/rapids/views.py b/rapids/views.py old mode 100644 new mode 100755 index 91ea44a2..c4d94676 --- a/rapids/views.py +++ b/rapids/views.py @@ -1,3 +1,450 @@ -from django.shortcuts import render +from django.shortcuts import render, get_object_or_404, redirect +from django import forms +#from django.contrib.auth.models import User +from django.contrib.auth.decorators import login_required, user_passes_test +from django.contrib.auth.models import User +from django.core.exceptions import PermissionDenied -# Create your views here. +from django.contrib.auth.mixins import LoginRequiredMixin + +from django.views.generic.edit import CreateView, UpdateView, DeleteView +from django.views.generic import ListView +from django.views.decorators.csrf import csrf_exempt + +from django.urls import reverse_lazy, reverse + +from django.http import Http404, JsonResponse +from django.http import HttpResponseRedirect, HttpResponse + +from .forms import RapidForm, ImageFormSet, NoteForm, RegionForm, AbnormalityForm, ExaminationForm +from .models import Rapid, Note, Abnormality, Region, Examination +from .tables import RapidTable +from .filters import RapidFilter + +from django_tables2 import SingleTableView, SingleTableMixin +from django_filters.views import FilterView + +from .decorators import user_is_author_or_rapid_checker + +from collections import defaultdict + +import logging +from copy import deepcopy +from django.forms.models import model_to_dict +from rapids.forms import RapidCreationDefaultForm +from rapids.models import RapidCreationDefault +logger = logging.getLogger(__name__) + + +class AuthorOrCheckerRequiredMixin(object): + def get_object(self, *args, **kwargs): + obj = super(UpdateView, self).get_object(*args, **kwargs) + if self.request.user.groups.filter(name='rapid_checker').exists(): + return obj + if self.request.user not in obj.author.all(): + raise PermissionDenied() #or Http404 + return obj + + +@login_required +def index(request): + other_rapids = Rapid.objects.exclude(author=request.user.pk) + user_rapids = Rapid.objects.filter(author=request.user.pk) + return render(request, "rapids/index.html", { + "other_rapids": other_rapids, + "user_rapids": user_rapids + }) + + +@login_required +def rapid_detail(request, pk): + rapid = get_object_or_404(Rapid, pk=pk) + + #if request.user not in rapid.author.all(): + # raise PermissionDenied + + #logging.debug(rapid.rapid_notes.first()) + #logging.debug(rapid.subspecialty.first().name.all()) + return render(request, 'rapids/rapid_detail.html', {'rapid': rapid}) + + +@login_required +def rapid_split(request, pk): + rapid = get_object_or_404(Rapid, pk=pk) + + images = rapid.images.all() + + old_abnormality = rapid.abnormality.all() + old_region = rapid.region.all() + old_examination = rapid.examination.all() + old_site = rapid.site.all() + old_author = rapid.author.all() + + if not images: + raise Http404 + + for n in range(len(images) - 1): + rapid.pk = None + + rapid.save() + + rapid.abnormality.set(old_abnormality) + rapid.region.set(old_region) + rapid.examination.set(old_examination) + rapid.site.set(old_site) + rapid.author.set(old_author) + images[n].rapid = rapid + images[n].save() + + #images[-1].rapid + + #if request.user not in rapid.author.all(): + # raise PermissionDenied + + #logging.debug(rapid.rapid_notes.first()) + #logging.debug(rapid.subspecialty.first().name.all()) + return render(request, 'rapids/rapid_detail.html', {'rapid': rapid}) + + +@login_required +def author_detail(request, pk): + #logging.debug(Author.objects.all()) + #author = get_object_or_404(Author, pk=pk) + author = User.objects.get(pk=pk) + + rapids = Rapid.objects.filter(author=pk) + + return render(request, "rapids/category_detail.html", { + "category": author, + "rapids": rapids + }) + + +def author_list(request): + authors = User.objects.all() + + return render(request, 'rapids/author_list.html', {'authors': authors}) + + +class RapidCreationDefaultView(LoginRequiredMixin, UpdateView): + model = RapidCreationDefault + form_class = RapidCreationDefaultForm + + # fields = '__all__' + # #fields = [ 'condition' ] + # #initial = {'date_of_death': '05/01/2018'} + # exclude = [ 'created_date', 'published_date' ] + # def dispatch(self, request, *args, **kwargs): + # """ + # Overridden so we can make sure the `Rapid` instance exists + # before going any further. + # """ + # self.pk = get_object_or_404(Rapid, pk=kwargs['pk']) + # return super().dispatch(request, *args, **kwargs) + def get_object(self, queryset=None): + obj, create = RapidCreationDefault.objects.get_or_create( + author=self.request.user) + + return obj + + def form_valid(self, form): + + model = form.save(commit=False) + + model.author = self.request.user + model.save() + + response = super().form_valid(form) + + return response + + #form.instance.author.add(self.request.user.id) + + +class AddNote(LoginRequiredMixin, CreateView): + model = Note + form_class = NoteForm + + # fields = '__all__' + # #fields = [ 'condition' ] + # #initial = {'date_of_death': '05/01/2018'} + # exclude = [ 'created_date', 'published_date' ] + def dispatch(self, request, *args, **kwargs): + """ + Overridden so we can make sure the `Rapid` instance exists + before going any further. + """ + self.pk = get_object_or_404(Rapid, pk=kwargs['pk']) + return super().dispatch(request, *args, **kwargs) + + def form_valid(self, form): + + note = form.save(commit=False) + + note.rapid = self.pk + + note.author = self.request.user + note.save() + + response = super().form_valid(form) + + return response + + #form.instance.author.add(self.request.user.id) + + +@login_required +def rapid_clone(request, pk): + new_item = get_object_or_404(Rapid, pk=pk) + new_item.pk = None #autogen a new pk (item_id) + #new_item.name = "Copy of " + new_item.name #need to change uniques + + form = RapidForm(request.POST or None, instance=new_item) + + formset = ImageFormSet() + + if form.is_valid(): + form.instance.author.add(request.user.id) + + #logger.debug(formset.is_valid()) + if formset.is_valid(): + response = super().form_valid(form) + formset.instance = obj + formset.save() + return response + else: + return super().form_invalid(form) + + context = { + "form": form, + "image_formset": formset + #other context + } + + return render(request, "rapids/rapid_form.html", context) + + +class RapidCreateBase(LoginRequiredMixin, CreateView): + model = Rapid + form_class = RapidForm + + def get_context_data(self, **kwargs): + context = super(RapidCreateBase, self).get_context_data(**kwargs) + if self.request.POST: + context['image_formset'] = ImageFormSet(self.request.POST, + self.request.FILES) + else: + context['image_formset'] = ImageFormSet() + return context + + def form_valid(self, form): + + self.object = form.save(commit=False) + self.object.save() + + form.instance.author.add(self.request.user.id) + + context = self.get_context_data(form=form) + formset = context['image_formset'] + if formset.is_valid(): + response = super().form_valid(form) + formset.instance = self.object + formset.save() + # If the normal submit button is pressed we save as normal + if "submit" in self.request.POST: + return response + # else we redirect to the clone url + else: + return redirect('rapids:rapid_clone', pk=self.object.pk) + + else: + return super().form_invalid(form) + + +#@login_required +class RapidCreate(RapidCreateBase): + + initial = {'laterality': Rapid.NONE} + + def get_initial(self): + # There has to be a better way... + try: + s = (i.pk for i in self.request.user.rapid_default.site.all()) + self.initial.update({'site': s}) + except AttributeError: + pass + return self.initial + + # fields = '__all__' + # #fields = [ 'condition' ] + # #initial = {'date_of_death': '05/01/2018'} + # exclude = [ 'created_date', 'published_date' ] + + #self.object = form.save(commit=False) + #self.object.save() + + #form.instance.author.add(self.request.user.id) + #return super().form_valid(form) + + +class RapidUpdate(LoginRequiredMixin, AuthorOrCheckerRequiredMixin, + UpdateView): + model = Rapid + form_class = RapidForm + + # fields = '__all__' + # #fields = [ 'condition' ] + # #initial = {'date_of_death': '05/01/2018'} + # exclude = [ 'created_date', 'published_date' ] + def get_context_data(self, **kwargs): + context = super(RapidUpdate, self).get_context_data(**kwargs) + if self.request.POST: + context['image_formset'] = ImageFormSet(self.request.POST, + self.request.FILES, + instance=self.object) + context['image_formset'].full_clean() + else: + context['image_formset'] = ImageFormSet(instance=self.object) + return context + + def form_valid(self, form): + + self.object = form.save(commit=False) + self.object.save() + + form.instance.author.add(self.request.user.id) + + context = self.get_context_data(form=form) + formset = context['image_formset'] + #logger.debug(formset.is_valid()) + if formset.is_valid(): + response = super().form_valid(form) + formset.instance = self.object + formset.save() + return response + else: + return super().form_invalid(form) + + +class RapidClone(RapidCreateBase): + """Clones a existing rapid""" + + # fields = '__all__' + # #fields = [ 'condition' ] + # #initial = {'date_of_death': '05/01/2018'} + # exclude = [ 'created_date', 'published_date' ] + def get_initial(self): + #print(self.request) + old_object = get_object_or_404(Rapid, pk=self.kwargs['pk']) + initial_data = model_to_dict(old_object, exclude=['id']) + + return initial_data + + +@login_required +@user_is_author_or_rapid_checker +def rapid_scrap(request, pk): + try: + rapid = Rapid.objects.get(pk=pk) + except Rapid.DoesNotExist: + raise Http404("Rapid does not exist") + + rapid.scrapped = not rapid.scrapped + rapid.save() + return HttpResponseRedirect(reverse('rapids:rapid_detail', args=(pk, ))) + + +# @login_required +# def edit_abnormality_popup(request): +# instance = get_object_or_404(Abnormality, pk = pk) +# form = AbnormalityForm(request.POST or None) +# if form.is_valid(): +# instance = form.save() +# return HttpResponse('' % (instance.pk, instance)) +# return render(request, "rapids/create_simple.html", {'form': form}) + + +@login_required +def create_abnormality(request): + form = AbnormalityForm(request.POST or None) + if form.is_valid(): + instance = form.save() + return HttpResponse( + '' + % (instance.pk, instance)) + return render(request, "rapids/create_simple.html", { + 'form': form, + 'name': "Abnormality" + }) + + +@csrf_exempt +def get_abnormality_id(request): + if request.is_ajax(): + abnormality_name = request.GET['abnormality_name'] + abnormality_id = Abnormality.objects.get(name=abnormality_name).id + data = { + 'abnormality_id': abnormality_id, + } + return HttpResponse(json.dumps(data), content_type='application/json') + return HttpResponse("/") + + +@login_required +def create_examination(request): + form = ExaminationForm(request.POST or None) + if form.is_valid(): + instance = form.save() + return HttpResponse( + '' + % (instance.pk, instance)) + return render(request, "rapids/create_simple.html", { + 'form': form, + 'name': "Examination" + }) + + +@csrf_exempt +def get_examination_id(request): + if request.is_ajax(): + examination_name = request.GET['examination_name'] + examination_id = Examination.objects.get(name=examination_name).id + data = { + 'examination_id': examination_id, + } + return HttpResponse(json.dumps(data), content_type='application/json') + return HttpResponse("/") + + +@login_required +def create_region(request): + form = RegionForm(request.POST or None) + if form.is_valid(): + instance = form.save() + return HttpResponse( + '' + % (instance.pk, instance)) + return render(request, "rapids/create_simple.html", { + 'form': form, + 'name': "Region" + }) + + +@csrf_exempt +def get_region_id(request): + if request.is_ajax(): + region_name = request.GET['region_name'] + region_id = Region.objects.get(name=region_name).id + data = { + 'region_id': region_id, + } + return HttpResponse(json.dumps(data), content_type='application/json') + return HttpResponse("/") + + +class RapidView(SingleTableMixin, FilterView): + model = Rapid + table_class = RapidTable + template_name = "rapids/view.html" + + filterset_class = RapidFilter \ No newline at end of file