diff --git a/.gitignore b/.gitignore index d614eec2..c776f1f5 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,6 @@ __pycache__ rad/settings_local.py venv .venv/ -.env \ No newline at end of file +.env + +temp/ \ No newline at end of file diff --git a/rad/settings.py b/rad/settings.py index e0a7b916..eb094eb3 100644 --- a/rad/settings.py +++ b/rad/settings.py @@ -77,7 +77,9 @@ INSTALLED_APPS = [ "django_htmx", "crispy_forms", "crispy_bootstrap4", - "autocomplete" + "autocomplete", + 'markdownify.apps.MarkdownifyConfig', + ] MIDDLEWARE = [ diff --git a/rcr/admin.py b/rcr/admin.py index cc551b62..d8c2e4e5 100644 --- a/rcr/admin.py +++ b/rcr/admin.py @@ -1,11 +1,17 @@ from django.contrib import admin # Register your models here. +from reversion.admin import VersionAdmin -from rcr.models import Item, Category, Level, Outcome, OutcomeReason +from rcr.models import Item, RadiologyCategory, OncologyCategory, Level, Outcome, OutcomeReason -admin.site.register(Item) -admin.site.register(Category) +@admin.register(Item) +class ItemAdmin(VersionAdmin): + pass + +#admin.site.register(Item, ItemAdmin) +admin.site.register(RadiologyCategory) +admin.site.register(OncologyCategory) admin.site.register(Level) admin.site.register(Outcome) admin.site.register(OutcomeReason) \ No newline at end of file diff --git a/rcr/filters.py b/rcr/filters.py new file mode 100755 index 00000000..bbc2b275 --- /dev/null +++ b/rcr/filters.py @@ -0,0 +1,81 @@ +import django_filters + +from .models import Item +from django.contrib.auth.models import User + +from django.db.models import Q + + + +def get_rad(request): + if request is None: + return User.objects.none() + + queryset = User.objects.filter(groups__name="rcr_radiology_assessor") + + return queryset + +def get_onc(request): + if request is None: + return User.objects.none() + + queryset = User.objects.filter(groups__name="rcr_oncology_assessor") + + return queryset + +def get_assessor(request): + #if request is None: + # return User.objects.none() + + queryset = User.objects.filter(Q(groups__name="rcr_oncology_assessor") | Q(groups__name="rcr_radiology_assessor") ) + + return queryset + +class ItemFilter(django_filters.FilterSet): + assigned_to_radiology = django_filters.ModelMultipleChoiceFilter( + queryset=get_rad, null_label="No user" + ) + assigned_to_oncology = django_filters.ModelMultipleChoiceFilter( + queryset=get_onc, null_label="No user" + ) + assessed_by = django_filters.ModelMultipleChoiceFilter( + queryset=get_assessor, null_label="No user" + ) + specialty = django_filters.MultipleChoiceFilter(choices=Item.SPECIALTY_CHOICES + ) + + class Meta: + model = Item + fields = { + "name": ["icontains"], + "description": ["icontains"], + "content_type": ["icontains"], + "created_on": ["exact"], + "specialty": ["exact"], + "targeted_to_group": ["exact"], + "assigned_to_radiology": ["exact"], + "assigned_to_oncology": ["exact"], + "assessed_by": ["exact"], + "completed_radiology": ["exact"], + "completed_oncology": ["exact"], + "needs_review": ["exact"], + + } + + #def __init__( + # self, + # data=None, + # queryset=None, + # prefix=None, + # strict=None, + # user=None, + # request=None, + #): + # if not request.user.groups.filter(name="rapid_checker").exists(): + # queryset = queryset.filter(open_access=True) | queryset.filter( + # author__id=request.user.id + # ) + # super(RapidFilter, self).__init__( + # data=data, queryset=queryset, prefix=prefix, request=request + # ) + # pass diff --git a/rcr/forms.py b/rcr/forms.py index c9e4223c..820d2f51 100644 --- a/rcr/forms.py +++ b/rcr/forms.py @@ -1,4 +1,3 @@ - from django import forms from django.contrib.admin import widgets from django.forms import ( @@ -12,17 +11,35 @@ from django.forms import ( CharField, ValidationError, modelformset_factory, - CheckboxSelectMultiple + CheckboxSelectMultiple, ) -from rcr.models import Item, Category, Level +from rcr.models import Item, RadiologyCategory, OncologyCategory, Level from django.contrib.auth.models import User + class ItemForm(ModelForm): class Meta: model = Item - exclude = ["rcr_platform_id", "name", "assessed_by", "assessed_date", "assigned_to"] - #fields = ["category", "level"] + exclude = [ + "rcr_platform_id", + "name", + "description", + "content_type", + "created_on", + "number_of_items", + "specialty", + "access_count", + "targeted_to_group", + "assessed_by", + "assessed_date", + "assigned_to_radiology", + "assigned_to_oncology", + "review_link", + "review_link_password", + ] + # fields = ["category", "level"] + class AssessorAssignmentForm(Form): - items = ModelMultipleChoiceField(queryset=Item.objects.filter(assigned_to=None)) \ No newline at end of file + items = ModelMultipleChoiceField(queryset=Item.objects.all(), widget=forms.SelectMultiple(attrs={'size': 20})) \ No newline at end of file diff --git a/rcr/management/__init__.py b/rcr/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rcr/management/commands/__init__.py b/rcr/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rcr/management/commands/import.py b/rcr/management/commands/import.py new file mode 100644 index 00000000..7880296a --- /dev/null +++ b/rcr/management/commands/import.py @@ -0,0 +1,81 @@ +import csv +from pathlib import Path +from django.core.management.base import BaseCommand, CommandError +from rcr.models import Item +import argparse + +import csv + +class DictReaderStrip(csv.DictReader): + @property + def fieldnames(self): + if self._fieldnames is None: + # Initialize self._fieldnames + # Note: DictReader is an old-style class, so can't use super() + csv.DictReader.fieldnames.fget(self) + if self._fieldnames is not None: + self._fieldnames = [name.strip() for name in self._fieldnames] + return self._fieldnames + + +class Command(BaseCommand): + help = "Imports the RCR spreadsheet" + + def add_arguments(self, parser): + # parser.add_argument("poll_ids", nargs="+", type=int) + parser.add_argument('--file', type=argparse.FileType('r')) + + + def handle(self, *args, **options): + #print(options) + #path = Path(options["file"]) + #with open(path) as f: + reader = DictReaderStrip(options["file"]) + #reader = csv.DictReader(f) + for row in reader: + if row["Access count (all time)"] == "": + row["Access count (all time)"] = None + if row["Number of items"] == "": + row["Number of items"] = None + if row["Number of items"] == "Unknown": + row["Number of items"] = None + + try: + item = Item.objects.get(rcr_platform_id=row["Unique Identifier"]) + + item.name=row["Name"] + item.description=row["Description"] + item.content_type=row["Content type"] + item.created_on=row["Created on"] + item.number_of_items=row["Number of items"] + item.specialty=row["Speciality"] + item.access_count=row["Access count (all time)"] + item.targeted_to_group=row["Targeted to group"] + item.review_link=row["Review link (if applicable)"] + item.review_link_password=row["Review link password (if applicable)"] + + item.save() + + self.stdout.write( + self.style.SUCCESS(f"Item updated: {item.pk}/{item.name}") + ) + except Item.DoesNotExist: + + item = Item.objects.create( + rcr_platform_id=row["Unique Identifier"], + name=row["Name"], + description=row["Description"], + content_type=row["Content type"], + created_on=row["Created on"], + number_of_items=row["Number of items"], + specialty=row["Speciality"], + access_count=row["Access count (all time)"], + targeted_to_group=row["Targeted to group"], + review_link=row["Review link (if applicable)"], + review_link_password=row["Review link password (if applicable)"], + ) + + + self.stdout.write( + self.style.SUCCESS(f"Item created: {item.pk}/{item.name}") + ) diff --git a/rcr/migrations/0010_alter_item_number_of_items_and_more.py b/rcr/migrations/0010_alter_item_number_of_items_and_more.py new file mode 100644 index 00000000..746d0af6 --- /dev/null +++ b/rcr/migrations/0010_alter_item_number_of_items_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.4 on 2024-01-16 22:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rcr', '0009_item_content_type_item_created_on_item_description_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='item', + name='number_of_items', + field=models.IntegerField(null=True), + ), + migrations.AlterField( + model_name='item', + name='rcr_platform_id', + field=models.CharField(max_length=255, unique=True), + ), + ] diff --git a/rcr/migrations/0011_alter_item_number_of_items_and_more.py b/rcr/migrations/0011_alter_item_number_of_items_and_more.py new file mode 100644 index 00000000..4aa5b0a1 --- /dev/null +++ b/rcr/migrations/0011_alter_item_number_of_items_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.4 on 2024-01-16 22:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rcr', '0010_alter_item_number_of_items_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='item', + name='number_of_items', + field=models.IntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name='item', + name='unique_views_2023', + field=models.IntegerField(blank=True, null=True), + ), + ] diff --git a/rcr/migrations/0012_item_comments_item_needs_review_and_more.py b/rcr/migrations/0012_item_comments_item_needs_review_and_more.py new file mode 100644 index 00000000..7980024e --- /dev/null +++ b/rcr/migrations/0012_item_comments_item_needs_review_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.1.4 on 2024-01-17 23:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rcr', '0011_alter_item_number_of_items_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='item', + name='comments', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='item', + name='needs_review', + field=models.BooleanField(default=False, help_text='Check if this item needs review.'), + ), + migrations.AlterField( + model_name='item', + name='targeted_to_group', + field=models.CharField(blank=True, default='Targeted', max_length=255), + ), + ] diff --git a/rcr/migrations/0013_rename_completed_item_completed_radiology.py b/rcr/migrations/0013_rename_completed_item_completed_radiology.py new file mode 100644 index 00000000..0515c98a --- /dev/null +++ b/rcr/migrations/0013_rename_completed_item_completed_radiology.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.4 on 2024-01-19 15:31 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('rcr', '0012_item_comments_item_needs_review_and_more'), + ] + + operations = [ + migrations.RenameField( + model_name='item', + old_name='completed', + new_name='completed_radiology', + ), + ] diff --git a/rcr/migrations/0014_item_completed_oncology.py b/rcr/migrations/0014_item_completed_oncology.py new file mode 100644 index 00000000..d2d3baf1 --- /dev/null +++ b/rcr/migrations/0014_item_completed_oncology.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.4 on 2024-01-19 15:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rcr', '0013_rename_completed_item_completed_radiology'), + ] + + operations = [ + migrations.AddField( + model_name='item', + name='completed_oncology', + field=models.BooleanField(default=False), + ), + ] diff --git a/rcr/migrations/0015_rename_unique_views_2023_item_access_count_and_more.py b/rcr/migrations/0015_rename_unique_views_2023_item_access_count_and_more.py new file mode 100644 index 00000000..88252b38 --- /dev/null +++ b/rcr/migrations/0015_rename_unique_views_2023_item_access_count_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.1.4 on 2024-01-22 09:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rcr', '0014_item_completed_oncology'), + ] + + operations = [ + migrations.RenameField( + model_name='item', + old_name='unique_views_2023', + new_name='access_count', + ), + migrations.AddField( + model_name='item', + name='review_link', + field=models.CharField(blank=True, default='', max_length=255), + ), + migrations.AddField( + model_name='item', + name='review_link_password', + field=models.CharField(blank=True, default='', max_length=255), + ), + ] diff --git a/rcr/migrations/0016_remove_item_assigned_to_item_assigned_to_oncology_and_more.py b/rcr/migrations/0016_remove_item_assigned_to_item_assigned_to_oncology_and_more.py new file mode 100644 index 00000000..6993a162 --- /dev/null +++ b/rcr/migrations/0016_remove_item_assigned_to_item_assigned_to_oncology_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 4.1.4 on 2024-01-22 11:02 + +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), + ('rcr', '0015_rename_unique_views_2023_item_access_count_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='item', + name='assigned_to', + ), + migrations.AddField( + model_name='item', + name='assigned_to_oncology', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned_items_oncology', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='item', + name='assigned_to_radiology', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned_items_radiology', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/rcr/migrations/0017_rename_category_radiologycategory.py b/rcr/migrations/0017_rename_category_radiologycategory.py new file mode 100644 index 00000000..befa095c --- /dev/null +++ b/rcr/migrations/0017_rename_category_radiologycategory.py @@ -0,0 +1,17 @@ +# Generated by Django 4.1.4 on 2024-01-22 20:51 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('rcr', '0016_remove_item_assigned_to_item_assigned_to_oncology_and_more'), + ] + + operations = [ + migrations.RenameModel( + old_name='Category', + new_name='RadiologyCategory', + ), + ] diff --git a/rcr/migrations/0018_oncologycategory_and_more.py b/rcr/migrations/0018_oncologycategory_and_more.py new file mode 100644 index 00000000..a113679e --- /dev/null +++ b/rcr/migrations/0018_oncologycategory_and_more.py @@ -0,0 +1,31 @@ +# Generated by Django 4.1.4 on 2024-01-22 20:54 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('rcr', '0017_rename_category_radiologycategory'), + ] + + operations = [ + migrations.CreateModel( + name='OncologyCategory', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ], + ), + migrations.RenameField( + model_name='item', + old_name='category', + new_name='radiology_category', + ), + migrations.AddField( + model_name='item', + name='oncology_category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='rcr.oncologycategory'), + ), + ] diff --git a/rcr/models.py b/rcr/models.py index 4269cedb..c657133f 100644 --- a/rcr/models.py +++ b/rcr/models.py @@ -2,9 +2,11 @@ from django.conf import settings from django.db import models from django.urls import reverse from django.utils.translation import gettext_lazy as _ +import reversion # Create your models here. +@reversion.register() class Item(models.Model): class LevelChoices(models.TextChoices): NONE = "", _("None") @@ -13,20 +15,29 @@ class Item(models.Model): POST_GEN = "GEN", _("Post FRCR Generalist") POST_SPEC = "SPEC", _("Post FRCR Specialist") + SPECIALTY_CHOICES = ( + ("Radiology", "Radiology"), + ("Oncology", "Oncology"), + ("Cross-faculty", "Cross-faculty"), + ) + name = models.CharField(max_length=200, blank=True, default="") description = models.TextField(blank=True, default="") content_type = models.CharField(max_length=200, blank=True, default="") created_on = models.DateTimeField(null=True, blank=True) - number_of_items = models.IntegerField(default=1) + number_of_items = models.IntegerField(null=True, blank=True) specialty = models.CharField(max_length=50,blank=True, default="") - unique_views_2023 = models.IntegerField(null=True) - targeted_to_group = models.CharField(max_length=255, default="Targeted") + access_count = models.IntegerField(null=True, blank=True) + targeted_to_group = models.CharField(max_length=255, default="Targeted", blank=True) + review_link = models.CharField(max_length=255, blank=True, default="") + review_link_password = models.CharField(max_length=255, blank=True, default="") rcr_platform_id = models.CharField(max_length=255, unique=True) - category = models.ForeignKey("Category", on_delete=models.SET_NULL, null=True, blank=True) + radiology_category = models.ForeignKey("RadiologyCategory", on_delete=models.SET_NULL, null=True, blank=True, related_name="item_radiology") + oncology_category = models.ForeignKey("OncologyCategory", on_delete=models.SET_NULL, null=True, blank=True, related_name="item_oncology") level = models.ManyToManyField("Level", blank=True) @@ -34,26 +45,44 @@ class Item(models.Model): assessed_by = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True, related_name="assessed_items") - assigned_to = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL, related_name="assigned_items") + assigned_to_radiology = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL, related_name="assigned_items_radiology") + assigned_to_oncology = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL, related_name="assigned_items_oncology") outcome = models.ForeignKey("Outcome", on_delete=models.SET_NULL, blank=True, null=True) outcome_reason = models.ForeignKey("OutcomeReason", on_delete=models.SET_NULL, blank=True, null=True) outcome_free_text = models.CharField(max_length=255, help_text="Reason for outcome (if not available above)", default="", blank=True) - completed = models.BooleanField(default=False) + + comments = models.TextField(blank=True) + needs_review = models.BooleanField(default=False, help_text="Check if this item needs review.") + + completed_radiology = models.BooleanField(default=False) + completed_oncology = models.BooleanField(default=False) class Meta: ordering = ("pk",) def __str__(self): - return self.name + completed = "" + if self.completed_oncology: + completed = " [Completed ONC]" + if self.completed_radiology: + completed = completed + " [Completed RAD]" + + return f"{self.specialty} / {self.content_type} / {self.name}{completed}" def get_absolute_url(self): return reverse("rcr:radiology_detail_view", kwargs={"pk": self.pk}) -class Category(models.Model): +class RadiologyCategory(models.Model): + name = models.CharField(max_length=255) + + def __str__(self): + return self.name + +class OncologyCategory(models.Model): name = models.CharField(max_length=255) def __str__(self): diff --git a/rcr/templates/rcr/assessor_assignment_form.html b/rcr/templates/rcr/assessor_assignment_form.html index 9fce2aa8..2ddd6ae1 100644 --- a/rcr/templates/rcr/assessor_assignment_form.html +++ b/rcr/templates/rcr/assessor_assignment_form.html @@ -1,10 +1,26 @@ {% extends 'rcr/base.html' %} +{% load auth_extras %} {% block content %}

Assign items to {{user_to_assign}}

+
+ Filter +
+ {{ item_filter.form.as_p }} + +
+
+ + {% if request.user|has_group:"rcr_radiology_assessor" %} + Radiology + {% elif request.user|has_group:"rcr_oncology_assessor" %} + Oncology + {% endif %} + assessor.
+
{% csrf_token %} {{assessor_assignment_form}} diff --git a/rcr/templates/rcr/assessors.html b/rcr/templates/rcr/assessors.html index 3d5c5768..4f438988 100644 --- a/rcr/templates/rcr/assessors.html +++ b/rcr/templates/rcr/assessors.html @@ -5,13 +5,22 @@

Assessors:

+

Radiology

+

Oncology

+ {% endblock %} \ No newline at end of file diff --git a/rcr/templates/rcr/base.html b/rcr/templates/rcr/base.html index e35d6783..589f8fa0 100644 --- a/rcr/templates/rcr/base.html +++ b/rcr/templates/rcr/base.html @@ -76,6 +76,9 @@