From cef06e4682818861dcf725f186b4ac57132f5eaf Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 24 Jun 2024 11:43:33 +0100 Subject: [PATCH] add question schema model and integrate most urls/views --- atlas/filters.py | 6 ++ atlas/forms.py | 18 +++- atlas/migrations/0055_questionschema.py | 24 +++++ atlas/models.py | 23 +++++ atlas/tables.py | 24 ++++- atlas/templates/atlas/base.html | 1 + .../atlas/collection_case_detail.html | 2 +- .../atlas/question_schema_detail.html | 24 +++++ .../atlas/question_schemas_preset.html | 6 +- .../templates/atlas/questionschema_form.html | 24 +++++ atlas/urls.py | 17 ++-- atlas/views.py | 88 ++++++++++++++++--- 12 files changed, 232 insertions(+), 25 deletions(-) create mode 100644 atlas/migrations/0055_questionschema.py create mode 100755 atlas/templates/atlas/question_schema_detail.html create mode 100755 atlas/templates/atlas/questionschema_form.html diff --git a/atlas/filters.py b/atlas/filters.py index 81f14d2e..608fb484 100755 --- a/atlas/filters.py +++ b/atlas/filters.py @@ -7,6 +7,7 @@ from .models import ( Finding, PathologicalProcess, Presentation, + QuestionSchema, Series, Structure, Subspecialty, @@ -282,3 +283,8 @@ class SubspecialtyFilter(django_filters.FilterSet): data=data, queryset=queryset, prefix=prefix, request=request ) pass + +class QuestionSchemaFilter(django_filters.FilterSet): + class Meta: + model = QuestionSchema + fields = ("name", "description") \ No newline at end of file diff --git a/atlas/forms.py b/atlas/forms.py index f7cf597d..471aaacf 100755 --- a/atlas/forms.py +++ b/atlas/forms.py @@ -27,6 +27,7 @@ from atlas.models import ( CidReportAnswer, Differential, Finding, + QuestionSchema, Resource, SelfReview, Series, @@ -528,10 +529,11 @@ class JsonAnswerForm(Form): class Meta: fields = ["json_answer"] - def __init__(self, *args, case_detail, **kwargs): + def __init__(self, *args, question_schema=None, **kwargs): super(JsonAnswerForm, self).__init__(*args, **kwargs) - if case_detail.question_schema is not None: - self.fields["json_answer"] = JSONSchemaField(schema=case_detail.question_schema, options='schema/options.json') + if question_schema is not None: + self.fields["json_answer"] = JSONSchemaField(schema=question_schema, options='schema/options.json') + self.fields["json_answer"].label = False class BaseReportAnswerForm(ModelForm): class Meta: @@ -768,4 +770,14 @@ class CaseDetailForm(ModelForm): widgets = { "question_schema": SvelteJSONEditorWidgetOverride(), "question_answers": SvelteJSONEditorWidgetOverride(), + } + + +class QuestionSchemaForm(ModelForm): + class Meta: + model = QuestionSchema + fields = ["name", "description", "schema"] + + widgets = { + "schema": SvelteJSONEditorWidgetOverride(), } \ No newline at end of file diff --git a/atlas/migrations/0055_questionschema.py b/atlas/migrations/0055_questionschema.py new file mode 100644 index 00000000..03918ced --- /dev/null +++ b/atlas/migrations/0055_questionschema.py @@ -0,0 +1,24 @@ +# Generated by Django 5.0.2 on 2024-06-24 09:27 + +import generic.mixins +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('atlas', '0054_casecollection_feedback_once_collection_complete_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='QuestionSchema', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('description', models.TextField(blank=True, null=True)), + ('schema', models.JSONField()), + ], + bases=(models.Model, generic.mixins.AuthorMixin), + ), + ] diff --git a/atlas/models.py b/atlas/models.py index 9475bd13..333fd6a8 100644 --- a/atlas/models.py +++ b/atlas/models.py @@ -74,6 +74,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator from loguru import logger + image_storage = FileSystemStorage( # Physical file location ROOT location="{0}atlas/".format(settings.MEDIA_ROOT), @@ -1272,3 +1273,25 @@ class CaseResource(models.Model): def __str__(self) -> str: return f"{self.resource} - {self.case} (Pre: {self.pre_review})" + + +class QuestionSchema(models.Model, AuthorMixin): + name = models.CharField(max_length=255) + description = models.TextField(blank=True, null=True) + schema = models.JSONField() + + def get_absolute_url(self): + return reverse("atlas:question_schema_detail", kwargs={"pk": self.pk}) + + def get_example_form(self): + from .forms import JsonAnswerForm + example_form = JsonAnswerForm( + question_schema=self.schema + ) + return example_form + + def get_schema_as_json(self) -> str: + return json.dumps(self.schema) + + def __str__(self) -> str: + return "{}".format(self.name) \ No newline at end of file diff --git a/atlas/tables.py b/atlas/tables.py index 8c4b36cb..29345ec2 100755 --- a/atlas/tables.py +++ b/atlas/tables.py @@ -9,6 +9,7 @@ from .models import ( Condition, PathologicalProcess, Presentation, + QuestionSchema, Series, Structure, Finding, @@ -337,4 +338,25 @@ class CaseCollectionTable(tables.Table): ), *args, **kwargs, - ) \ No newline at end of file + ) + +class QuestionSchemaTable(tables.Table): + name = tables.Column( + linkify=("atlas:question_schema_detail", {"pk": tables.A("pk")}), + verbose_name="QuestionSchema", + ) + + edit = tables.LinkColumn( + "atlas:question_schema_update", text="Edit", args=[A("pk")], orderable=False + ) + delete = tables.LinkColumn( + "atlas:question_schema_delete", text="Delete", args=[A("pk")], orderable=False + ) + + class Meta: + model = QuestionSchema + template_name = "django_tables2/bootstrap4.html" + fields = ("name", "description", "schema") + + def render_schema(self, value, record): + return format_html("
Schema{}
", value) \ No newline at end of file diff --git a/atlas/templates/atlas/base.html b/atlas/templates/atlas/base.html index 7ffb7b79..3f474304 100755 --- a/atlas/templates/atlas/base.html +++ b/atlas/templates/atlas/base.html @@ -16,6 +16,7 @@ Create Series / Resources / Uploads / + Question Schemas / Help {% endif %} {% comment %}
diff --git a/atlas/templates/atlas/collection_case_detail.html b/atlas/templates/atlas/collection_case_detail.html index ab5fc2d0..3c354061 100644 --- a/atlas/templates/atlas/collection_case_detail.html +++ b/atlas/templates/atlas/collection_case_detail.html @@ -29,7 +29,7 @@ hx-target=".offcanvas-body" >Collection questions diff --git a/atlas/templates/atlas/question_schema_detail.html b/atlas/templates/atlas/question_schema_detail.html new file mode 100755 index 00000000..a4aedec9 --- /dev/null +++ b/atlas/templates/atlas/question_schema_detail.html @@ -0,0 +1,24 @@ +{% extends 'atlas/base.html' %} + +{% block content %} + + + +
+

Name: {{question_schema.name}}

+

Description: {{question_schema.description}}

+ +
JSON:{{question_schema.schema}}
+ + {{example_form}} + + + + +
+ +{% endblock %} + +{% block js %} +{{example_form.media}} +{% endblock %} diff --git a/atlas/templates/atlas/question_schemas_preset.html b/atlas/templates/atlas/question_schemas_preset.html index 988e1721..82af3645 100644 --- a/atlas/templates/atlas/question_schemas_preset.html +++ b/atlas/templates/atlas/question_schemas_preset.html @@ -3,12 +3,12 @@ {% comment %} {% endcomment %}
    - {% for schema in schemas %} + {% for question_schema in question_schemas %}
  1. -
    Schema: -
    {{schema}}
    +
    {{question_schema.name}} +
    {{question_schema.schema}}
    diff --git a/atlas/templates/atlas/questionschema_form.html b/atlas/templates/atlas/questionschema_form.html new file mode 100755 index 00000000..72fb2063 --- /dev/null +++ b/atlas/templates/atlas/questionschema_form.html @@ -0,0 +1,24 @@ +{% extends "atlas/base.html" %} + + +{% block css %} +{% endblock %} +{% block js %} + + {{form.media}} + + + + +{% endblock %} +{% block content %} +

    Add/Edit Condition

    +

    Use this form to create or edit a question schema.

    +
    + {% csrf_token %} + + {{ form.as_div }} + +
    +{% endblock %} diff --git a/atlas/urls.py b/atlas/urls.py index efafb417..57aaf984 100755 --- a/atlas/urls.py +++ b/atlas/urls.py @@ -121,11 +121,11 @@ urlpatterns = [ views.collection_question_schemas, name="collection_question_schemas", ), - path( - "question_schemas_preset", - views.question_schemas_preset, - name="question_schemas_preset", - ), + #path( + # "question_schemas_preset", + # views.question_schemas_preset, + # name="question_schemas_preset", + #), path( "collection//cids//delete_answers", views.delete_collection_cid_answers, @@ -218,7 +218,12 @@ urlpatterns = [ name="series_dicom_json", ), path("uncategorised_dicoms/", views.uncategorised_dicoms, name="uncategorised_dicoms_view"), - + path("question_schema/", views.QuestionSchemaView.as_view(), name="question_schema_overview"), + path("question_schema/", views.question_schema_detail, name="question_schema_detail"), + path("question_schema//update", views.QuestionSchemaUpdate.as_view(), name="question_schema_update"), + path("question_schema//delete", views.QuestionSchemaDelete.as_view(), name="question_schema_delete"), + path("question_schema/create", views.QuestionSchemaCreate.as_view(), name="question_schema_create"), + path("question_schema/schemas", views.question_schema_schemas, name="question_schema_schemas"), path("condition/", views.ConditionView.as_view(), name="condition_view"), path("categories/", views.categories_list, name="categories_list"), path("condition/", views.condition_detail, name="condition_detail"), diff --git a/atlas/views.py b/atlas/views.py index c66012bc..8d038462 100755 --- a/atlas/views.py +++ b/atlas/views.py @@ -49,6 +49,7 @@ from .forms import ( ConditionForm, FindingForm, JsonAnswerForm, + QuestionSchemaForm, ResourceForm, SelfReviewForm, SeriesAuthorForm, @@ -70,6 +71,7 @@ from .models import ( Differential, PathologicalProcess, Presentation, + QuestionSchema, Resource, SelfReview, Series, @@ -89,6 +91,7 @@ from .tables import ( FindingTable, PathologicalProcessTable, PresentationTable, + QuestionSchemaTable, SeriesTable, StructureTable, SubspecialtyTable, @@ -100,6 +103,7 @@ from .filters import ( FindingFilter, PathologicalProcessFilter, PresentationFilter, + QuestionSchemaFilter, SeriesFilter, StructureFilter, SubspecialtyFilter, @@ -224,6 +228,33 @@ def series_detail(request, pk, finding_pk=None): ) +@login_required +# @user_is_atlas_editor +def question_schema_detail(request, pk: int): + question_schema = get_object_or_404(QuestionSchema, pk=pk) + + example_form = question_schema.get_example_form() + + # logging.debug(atlas.subspecialty.first().name.all()) + return render( + request, + "atlas/question_schema_detail.html", + {"question_schema": question_schema, "example_form": example_form}, + ) + +@login_required +# @user_is_atlas_editor +def question_schema_schemas(request): + question_schemas = QuestionSchema.objects.all() + + return render( + request, + "atlas/question_schemas_preset.html", + { + "question_schemas": question_schemas, + }, + ) + @login_required # @user_is_atlas_editor def condition_detail(request, pk): @@ -823,6 +854,18 @@ class ConditionCreate(RevisionMixin, LoginRequiredMixin, CreateView): form_class = ConditionForm +class QuestionSchemaCreate(LoginRequiredMixin, CreateView): + model = QuestionSchema + form_class = QuestionSchemaForm + +class QuestionSchemaUpdate(LoginRequiredMixin, UpdateView): + model = QuestionSchema + form_class = QuestionSchemaForm + +class QuestionSchemaDelete(AuthorOrCheckerRequiredMixin, DeleteView): + model = QuestionSchema + template_name = "confirm_delete.html" + class FindingUpdate(RevisionMixin, LoginRequiredMixin, UpdateView): model = Finding form_class = FindingForm @@ -1141,6 +1184,14 @@ class ConditionView(LoginRequiredMixin, SingleTableMixin, FilterView): filterset_class = ConditionFilter +class QuestionSchemaView(LoginRequiredMixin, SingleTableMixin, FilterView): + model = QuestionSchema + table_class = QuestionSchemaTable + template_name = "atlas/view.html" + + filterset_class = QuestionSchemaFilter + + class SubspecialtyView(LoginRequiredMixin, SingleTableMixin, FilterView): model = Subspecialty table_class = SubspecialtyTable @@ -1484,8 +1535,17 @@ class CollectionView(LoginRequiredMixin, SingleTableMixin, FilterView): def collection_viva(request, pk): collection = get_object_or_404(CaseCollection, pk=pk) - cases = collection.cases.all().order_by("casedetail__sort_order").prefetch_related("series", "series__images", "series__contrast", "series__plane", "series__examination") - + cases = ( + collection.cases.all() + .order_by("casedetail__sort_order") + .prefetch_related( + "series", + "series__images", + "series__contrast", + "series__plane", + "series__examination", + ) + ) return render( request, @@ -1588,7 +1648,9 @@ def collection_case_details(request, exam_id, case_id): # answers = request.POST case_detail.question_answers = json.loads(request.POST.get("json_answer")) case_detail.save() - example_form = JsonAnswerForm(request.POST, case_detail=case_detail) + example_form = JsonAnswerForm( + request.POST, question_schema=case_detail.question_schema + ) form = CaseDetailForm(instance=case_detail) # Called if the user saves the main form elif request.POST.get("submit") == "save": @@ -1596,7 +1658,7 @@ def collection_case_details(request, exam_id, case_id): if form.is_valid(): form.save() # Add any additional logic or redirection here - # example_form = JsonAnswerForm(request.POST, case_detail=case_detail) + # example_form = JsonAnswerForm(request.POST, question_schema=case_detail.question_schema) post_data = request.POST.copy() if ( case_detail.question_schema is not None @@ -1618,7 +1680,9 @@ def collection_case_details(request, exam_id, case_id): post_data["json_answer"] = json.dumps(answers) case_detail.question_answers = answers case_detail.save() - example_form = JsonAnswerForm(post_data, case_detail=case_detail) + example_form = JsonAnswerForm( + post_data, question_schema=case_detail.question_schema + ) form = CaseDetailForm(instance=case_detail) # This shouldn't happen @@ -1629,7 +1693,9 @@ def collection_case_details(request, exam_id, case_id): else: post_data = request.POST.copy() post_data["json_answer"] = json.dumps(case_detail.question_answers) - example_form = JsonAnswerForm(post_data, case_detail=case_detail) + example_form = JsonAnswerForm( + post_data, question_schema=case_detail.question_schema + ) form = CaseDetailForm(instance=case_detail) pass @@ -1652,17 +1718,18 @@ def collection_case_details(request, exam_id, case_id): # post_data = request.POST.copy() # post_data["json_answer"]= json.dumps(answers) # case_detail.question_answers = answers - # example_form = JsonAnswerForm(post_data, case_detail=case_detail) + # example_form = JsonAnswerForm(post_data, question_schema=case_detail.question_schema) # form = CaseDetailForm(instance=case_detail) - # blank_form = JsonAnswerForm(case_detail=case_detail) - case_number, case_count = collection.get_index_of_case(case_detail.case, case_count=True) + # blank_form = JsonAnswerForm(question_schema=case_detail.question_schema) + case_number, case_count = collection.get_index_of_case( + case_detail.case, case_count=True + ) previous = collection.get_previous_case(case_detail.case) next = collection.get_next_case(case_detail.case) - return render( request, "atlas/collection_case_detail.html", @@ -1983,7 +2050,6 @@ def collection_case_view_take( ).first() ReportAnswerForm = UserQuestionAnswerForm - if request.method == "POST": if collection.collection_type in ("REP", "QUE"): if not collection.publish_results: