From c4b719356ecd625db6111806599aa0aa8174b3f0 Mon Sep 17 00:00:00 2001 From: Birger Schacht Date: Thu, 21 Dec 2023 15:33:45 +0100 Subject: [PATCH] feat: make generic crudl views Create generic Create, View, Update, Delete and List views that work with ContentTypes Closes: #509 --- apis_core/core/templatetags/apiscore.py | 6 ++ apis_core/generic/tables.py | 13 +++++ .../generic/generic_confirm_delete.html | 32 +++++++++++ .../templates/generic/generic_detail.html | 6 ++ .../templates/generic/generic_filter.html | 44 +++++++++++++++ .../templates/generic/generic_form.html | 6 ++ apis_core/generic/urls.py | 36 ++++++++++++ apis_core/generic/views.py | 55 +++++++++++++++++++ apis_core/urls.py | 1 + 9 files changed, 199 insertions(+) create mode 100644 apis_core/generic/tables.py create mode 100644 apis_core/generic/templates/generic/generic_confirm_delete.html create mode 100644 apis_core/generic/templates/generic/generic_detail.html create mode 100644 apis_core/generic/templates/generic/generic_filter.html create mode 100644 apis_core/generic/templates/generic/generic_form.html create mode 100644 apis_core/generic/urls.py create mode 100644 apis_core/generic/views.py diff --git a/apis_core/core/templatetags/apiscore.py b/apis_core/core/templatetags/apiscore.py index e001c1a30..3663ee880 100644 --- a/apis_core/core/templatetags/apiscore.py +++ b/apis_core/core/templatetags/apiscore.py @@ -1,5 +1,6 @@ from django import template from django.conf import settings +from django.contrib.contenttypes.models import ContentType register = template.Library() @@ -13,3 +14,8 @@ def shared_url(): @register.simple_tag def page_range(paginator, number): return paginator.get_elided_page_range(number=number) + + +@register.filter +def contenttype(model): + return ContentType.objects.get_for_model(model) diff --git a/apis_core/generic/tables.py b/apis_core/generic/tables.py new file mode 100644 index 000000000..3a2ebf625 --- /dev/null +++ b/apis_core/generic/tables.py @@ -0,0 +1,13 @@ +import django_tables2 as tables +from django.urls import reverse +from django.contrib.contenttypes.models import ContentType + + +class GenericTable(tables.Table): + desc = tables.Column(empty_values=(), linkify=lambda record: reverse("apis_core:generic:genericdetail", args=[ContentType.objects.get_for_model(record), record.pk])) + + class Meta: + fields = ['id', 'desc'] + + def render_desc(self, record): + return str(record) diff --git a/apis_core/generic/templates/generic/generic_confirm_delete.html b/apis_core/generic/templates/generic/generic_confirm_delete.html new file mode 100644 index 000000000..6cf5240c5 --- /dev/null +++ b/apis_core/generic/templates/generic/generic_confirm_delete.html @@ -0,0 +1,32 @@ +{% extends basetemplate|default:"base.html" %} + +{% block content %} + + +{% endblock content %} diff --git a/apis_core/generic/templates/generic/generic_detail.html b/apis_core/generic/templates/generic/generic_detail.html new file mode 100644 index 000000000..1fde32852 --- /dev/null +++ b/apis_core/generic/templates/generic/generic_detail.html @@ -0,0 +1,6 @@ +{% extends basetemplate|default:"base.html" %} +{% load render_table from django_tables2 %} + +{% block content %} + {{ object }} +{% endblock content %} diff --git a/apis_core/generic/templates/generic/generic_filter.html b/apis_core/generic/templates/generic/generic_filter.html new file mode 100644 index 000000000..0da9bc8e9 --- /dev/null +++ b/apis_core/generic/templates/generic/generic_filter.html @@ -0,0 +1,44 @@ +{% extends basetemplate|default:"base.html" %} +{% load render_table from django_tables2 %} +{% load crispy_forms_tags %} +{% load apiscore %} + +{% block content %} +
+
+ {% if filter %} +
+
+
+
+
{{ object_list.model|contenttype }}
+ +
+
+
+ {% block filter %} + {% crispy filter.form filter.form.helper %} + {% endblock filter %} +
+ +
+
+ {% endif %} + {% if table %} +
+
+
+
+
+ {% block table %} + {% render_table table %} + {% endblock table %} +
+
+
+ {% endif %} +
+
+{% endblock content %} diff --git a/apis_core/generic/templates/generic/generic_form.html b/apis_core/generic/templates/generic/generic_form.html new file mode 100644 index 000000000..1befa0bde --- /dev/null +++ b/apis_core/generic/templates/generic/generic_form.html @@ -0,0 +1,6 @@ +{% extends basetemplate|default:"base.html" %} +{% load crispy_forms_tags %} + +{% block content %} + {% crispy form %} +{% endblock content %} diff --git a/apis_core/generic/urls.py b/apis_core/generic/urls.py new file mode 100644 index 000000000..18d0fe924 --- /dev/null +++ b/apis_core/generic/urls.py @@ -0,0 +1,36 @@ +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import get_object_or_404 +from django.urls import include, path, register_converter + +from apis_core.generic import views + + +class ContenttypeConverter: + regex = r"\w+\.\w+" + + def to_python(self, value): + app_label, model = value.split(".") + return get_object_or_404(ContentType, app_label=app_label, model=model) + + def to_url(self, value): + return f"{value.app_label}.{value.model}" + + +register_converter(ContenttypeConverter, "ccc") + +app_name = "generic" + +urlpatterns = [ + path( + "/", + include( + [ + path("", views.ListCC.as_view(), name="genericlist"), + path("", views.DetailCC.as_view(), name="genericdetail"), + path("create", views.CreateCC.as_view(), name="create"), + path("delete/", views.DeleteCC.as_view()), + path("update/", views.UpdateCC.as_view()), + ] + ), + ) +] diff --git a/apis_core/generic/views.py b/apis_core/generic/views.py new file mode 100644 index 000000000..d55cb684d --- /dev/null +++ b/apis_core/generic/views.py @@ -0,0 +1,55 @@ +from django.views.generic import DetailView +from django.views.generic.edit import CreateView, UpdateView, DeleteView +from django.urls import reverse + +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Submit +from django_filters.views import FilterView +from django_tables2 import SingleTableMixin +from django_tables2.tables import table_factory + +from .tables import GenericTable + +class CCMixin: + def setup(self, *args, **kwargs): + super().setup(*args, **kwargs) + self.model = kwargs.get("contenttype").model_class() + self.queryset = self.model.objects.all() + + def get_template_names(self): + return super().get_template_names() + [ + f"generic/generic{self.template_name_suffix}.html" + ] + + +class ListCC(CCMixin, SingleTableMixin, FilterView): + def get_table_class(self): + return table_factory(self.kwargs.get("contenttype").model_class(), GenericTable) + + def get_filterset(self, filterset_class): + filterset = super().get_filterset(filterset_class) + filterset.form.helper = FormHelper() + filterset.form.helper.form_method = "GET" + filterset.form.helper.add_input(Submit('submit', 'Submit')) + return filterset + + +class DetailCC(CCMixin, DetailView): + pass + + +class CreateCC(CCMixin, CreateView): + fields = "__all__" + template_name = "generic/generic_form.html" + + +class DeleteCC(CCMixin, DeleteView): + def get_success_url(self): + return reverse( + "apis:generic:genericlist", + args=[self.request.resolver_match.kwargs["contenttype"]], + ) + + +class UpdateCC(CCMixin, UpdateView): + fields = "__all__" diff --git a/apis_core/urls.py b/apis_core/urls.py index 5b9a2cbf6..095e7ea08 100644 --- a/apis_core/urls.py +++ b/apis_core/urls.py @@ -168,6 +168,7 @@ def build_apis_mock_request(method, path, view, original_request, **kwargs): name="GetEntityGeneric", ), path("api/dumpdata", Dumpdata.as_view()), + path("", include("apis_core.generic.urls", namespace="generic")), ] if "apis_fulltext_download" in settings.INSTALLED_APPS: