Skip to content

Commit

Permalink
feat: make generic crudl views
Browse files Browse the repository at this point in the history
Create generic Create, View, Update, Delete and List views that work
with ContentTypes

Closes: #509
  • Loading branch information
b1rger committed Dec 24, 2023
1 parent 3fcd5d8 commit 1532e9f
Show file tree
Hide file tree
Showing 17 changed files with 413 additions and 0 deletions.
31 changes: 31 additions & 0 deletions apis_core/core/templatetags/apiscore.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from itertools import chain

from django import template
from django.conf import settings
from django.contrib.contenttypes.models import ContentType


register = template.Library()
Expand All @@ -13,3 +16,31 @@ 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)


@register.simple_tag
def modeldict(instance, fields=None, exclude=None):
opts = instance._meta
data = {}
for f in chain(opts.concrete_fields, opts.private_fields, opts.many_to_many):
if not getattr(f, "editable", False):
continue
if fields is not None and f.name not in fields:
continue
if exclude and f.name in exclude:
continue
field = instance._meta.get_field(f.name)
data[field] = instance._get_FIELD_display(field)
return data


@register.simple_tag
def contenttypes(app_label=None):
if app_label:
return ContentType.objects.filter(app_label=app_label)
return ContentType.objects.all()
14 changes: 14 additions & 0 deletions apis_core/generic/api_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from rest_framework import viewsets
from .serializers import serializer_factory


class GenericViewSet(viewsets.ModelViewSet):
def dispatch(self, *args, **kwargs):
self.model = kwargs.get("contenttype").model_class()
return super().dispatch(*args, **kwargs)

def get_queryset(self):
return self.model.objects.all()

def get_serializer_class(self):
return serializer_factory(self.model)
23 changes: 23 additions & 0 deletions apis_core/generic/filtersets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from django_filters.filterset import FilterSet
from .forms import GenericFilterSetForm


class GenericFilterSet(FilterSet):

@property
def form(self):
if not hasattr(self, "_form"):
Form = self.get_form_class()
if self.is_bound:
self._form = Form(self.data, prefix=self.form_prefix, model=self._meta.model)
else:
self._form = Form(prefix=self.form_prefix, model=self._meta.model)
return self._form


def filterset_factory(model, fields="__all__"):
meta = type(str("Meta"), (object,), {"model": model, "fields": fields, "form": GenericFilterSetForm})
filterset = type(
str("%sFilterSet" % model._meta.object_name), (GenericFilterSet,), {"Meta": meta}
)
return filterset
31 changes: 31 additions & 0 deletions apis_core/generic/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from django import forms
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit


class GenericFilterSetForm(forms.Form):
columns = forms.MultipleChoiceField(required=False)

def __init__(self, *args, **kwargs):
model = kwargs.pop("model")
super().__init__(*args, **kwargs)
self.fields["columns"].choices = [(field.name, field.verbose_name) for field in model._meta.fields]

self.helper = FormHelper()
self.helper.form_method = "GET"
self.helper.add_input(Submit('submit', 'Submit'))

def clean(self):
self.cleaned_data = super().clean()
self.cleaned_data.pop("columns")
return self.cleaned_data


class GenericModelForm(forms.ModelForm):
class Meta:
fields = "__all__"

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper(self)
self.helper.add_input(Submit('submit', 'Submit'))
9 changes: 9 additions & 0 deletions apis_core/generic/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from rest_framework.serializers import ModelSerializer


def serializer_factory(model, fields="__all__", **kwargs):
meta = type(str("Meta"), (object,), {"model": model, "fields": fields})
serializer = type(
str("%sModelSerializer" % model._meta.object_name), (ModelSerializer,), {"Meta": meta}
)
return serializer
38 changes: 38 additions & 0 deletions apis_core/generic/tables.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import django_tables2 as tables


class CustomTemplateColumn(tables.TemplateColumn):
template_name = None
orderable = None

def __init__(self, *args, **kwargs):
super().__init__(template_name=self.template_name, orderable=self.orderable, *args, **kwargs)


class DeleteColumn(CustomTemplateColumn):
template_name = "columns/delete.html"
orderable = False


class DescriptionColumn(CustomTemplateColumn):
template_name = "columns/description.html"
orderable = False


class GenericTable(tables.Table):
desc = DescriptionColumn()
delete = DeleteColumn()

class Meta:
fields = ['id', 'desc']

# TODO: refactor
def __init__(self, *args, **kwargs):
extra_columns = kwargs.get("extra_columns", [])
fields = [field for field in self._meta.model._meta.fields if field.name in kwargs.get("columns", [])]
for field in fields:
extra_columns.append((field.name, tables.columns.library.column_for_field(field, accessor=field.name)))
del kwargs["columns"]
kwargs["extra_columns"] = extra_columns

super().__init__(*args, **kwargs)
2 changes: 2 additions & 0 deletions apis_core/generic/templates/columns/delete.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{% load apiscore %}
<a href="{% url 'apis_core:generic:delete' record|contenttype record.id %}" class="text-danger"></a>
2 changes: 2 additions & 0 deletions apis_core/generic/templates/columns/description.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{% load apiscore %}
<a href="{% url 'apis_core:generic:detail' record|contenttype record.id %}">{{ record }}</a>
32 changes: 32 additions & 0 deletions apis_core/generic/templates/generic/generic_confirm_delete.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{% extends basetemplate|default:"base.html" %}

{% block content %}
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">Confirm delete</h3>
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<div class="modal-body">
<form action="" method="post">
{% csrf_token %}
<h4>
Are you sure you want to delete: <strong>{{ object }}</strong> ?
</h4>
<input class="btn btn-danger" type="submit" value="Yes, I want to delete" />
</form>
</div>
<div class="modal-footer">
<input class="btn"
type="submit"
value="No, bring me back"
onclick="goBack()" />
</div>
</div>
</div>
<script>
function goBack() {
window.history.back();
}
</script>
{% endblock content %}
14 changes: 14 additions & 0 deletions apis_core/generic/templates/generic/generic_content.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{% extends basetemplate|default:"base.html" %}

{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col">
{% block col %}
{% endblock col %}
</div>
{% block additionalcols %}
{% endblock additionalcols %}
</div>
</div>
{% endblock content %}
23 changes: 23 additions & 0 deletions apis_core/generic/templates/generic/generic_detail.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{% extends "generic/generic_content.html" %}
{% load apiscore %}

{% if object %}
{% block col %}
<div class="card">
<div class="card-header">
{{ object }}
</div>
<div class="card-body">
<table class="table table-hover">
{% modeldict object as d %}
{% for key, value in d.items %}
<tr>
<th>{{ key.verbose_name }}</th>
<td>{{ value }}</td>
</tr>
{% endfor %}
</table>
</div>
</div>
{% endblock col %}
{% endif %}
13 changes: 13 additions & 0 deletions apis_core/generic/templates/generic/generic_form.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{% extends "generic/generic_content.html" %}
{% load crispy_forms_tags %}

{% block col %}
<div class="card">
<div class="card-header">
Edit {{ object }}
</div>
<div class="card-body">
{% crispy form form.helper %}
</div>
</div>
{% endblock col %}
43 changes: 43 additions & 0 deletions apis_core/generic/templates/generic/generic_list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{% extends "generic/generic_content.html" %}
{% load render_table from django_tables2 %}
{% load crispy_forms_tags %}
{% load apiscore %}

{% if filter %}
{% block col %}
<div class="card">
<div class="card-header">
<div class="row">
<div class="col">{{ object_list.model|contenttype }}</div>
<div class="col"><a class="btn btn-outline-success float-right btn-sm" href="{% url 'apis_core:generic:create' object_list.model|contenttype %}">Create</a></div>
</div>
</div>
<div class="card-body">
{% block filter %}
{% crispy filter.form filter.form.helper %}
{% endblock filter %}
</div>
<div class="card-footer text-muted">
<a class="btn btn-outline-secondary" href=".">Reset filter</a>
</div>
</div>
{% endblock col %}
{% endif %}


{% if table %}
{% block additionalcols %}
<div class="col-8">
<div class="card">
<div class="card-header">
{{ table.paginator.count }} results
</div>
<div class="card-body">
{% block table %}
{% render_table table %}
{% endblock table %}
</div>
</div>
</div>
{% endblock additionalcols %}
{% endif %}
16 changes: 16 additions & 0 deletions apis_core/generic/templates/generic/overview.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{% extends "generic/generic_content.html" %}
{% load apiscore %}

{% block col %}
<div class="card">
<div class="card-header">
Overview
</div>
<div class="card-body text-center">
{% contenttypes "apis_ontology" as contenttypes %}
{% for contenttype in contenttypes %}
<a href="{% url 'apis_core:generic:list' contenttype %}"><button type="button" class="btn btn-outline-dark m-2">{{ contenttype }}</button></a>
{% endfor %}
</div>
</div>
{% endblock col %}
42 changes: 42 additions & 0 deletions apis_core/generic/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import get_object_or_404
from django.urls import include, path, register_converter
from rest_framework import routers

from apis_core.generic import views, api_views

router = routers.DefaultRouter()
router.register(r"", api_views.GenericViewSet, basename="foo")


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("overview/", views.Overview.as_view(), name="overview"),
path(
"<ccc:contenttype>/",
include(
[
path("", views.ListCC.as_view(), name="list"),
path("<int:pk>", views.DetailCC.as_view(), name="detail"),
path("create", views.CreateCC.as_view(), name="create"),
path("delete/<int:pk>", views.DeleteCC.as_view(), name="delete"),
path("update/<int:pk>", views.UpdateCC.as_view(), name="update"),
]
),
),
path("api/<ccc:contenttype>/", include(router.urls)),
]
Loading

0 comments on commit 1532e9f

Please sign in to comment.