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 25, 2023
1 parent 3fcd5d8 commit bafcf2e
Show file tree
Hide file tree
Showing 19 changed files with 561 additions and 0 deletions.
35 changes: 35 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,35 @@ 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)
if getattr(field, "m2m_field_name", False):
values = getattr(instance, field.name).all()
data[field] = ", ".join([str(value) for value in values])
return data


@register.simple_tag
def contenttypes(app_labels=None):
if app_labels:
app_labels = app_labels.split(",")
return ContentType.objects.filter(app_label__in=app_labels)
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)
30 changes: 30 additions & 0 deletions apis_core/generic/filtersets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
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, filterset=GenericFilterSet, fields="__all__"):
meta = type(
str("Meta"),
(object,),
{"model": model, "fields": fields, "form": GenericFilterSetForm},
)
filterset = type(
str("%sFilterSet" % model._meta.object_name),
(filterset,),
{"Meta": meta},
)
return filterset
52 changes: 52 additions & 0 deletions apis_core/generic/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from django import forms
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit
from dal import autocomplete


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", None)
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"))

# override the fields pointing to other models,
# to make them use the autocomplete widgets
override_fieldtypes = {
"ModelMultipleChoiceField": autocomplete.ModelSelect2Multiple,
"ModelChoiceField": autocomplete.ModelSelect2,
}
for field in self.fields:
clsname = self.fields[field].__class__.__name__
if clsname in override_fieldtypes.keys():
ct = ContentType.objects.get_for_model(
self.fields[field]._queryset.model
)
url = reverse("apis_core:generic:autocomplete", args=[ct])
self.fields[field].widget = override_fieldtypes[clsname](url)
self.fields[field].widget.choices = self.fields[field].choices
53 changes: 53 additions & 0 deletions apis_core/generic/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import functools
import inspect
import importlib

from django.db.models import CharField, TextField, Q, Model


def generate_search_filter(model, query):
fields_to_search = [
field.name
for field in model._meta.fields
if isinstance(field, (CharField, TextField))
]
q = Q()

for token in query:
q &= functools.reduce(
lambda acc, field_name: acc | Q(**{f"{field_name}__icontains": token}),
fields_to_search,
Q(),
)

return model.objects.filter(q)


def mro_paths(model):
paths = []
for cls in filter(lambda x: x not in Model.mro(), model.mro()):
paths.append(cls.__module__.split(".")[:-1] + [cls.__name__])
return paths


def template_names_via_mro(model, suffix=""):
mro_prefix_list = ["/".join(prefix) for prefix in mro_paths(model)]
return [f"{prefix.lower()}{suffix}" for prefix in mro_prefix_list]


def class_from_path(classpath):
module, cls = classpath.rsplit(".", 1)
try:
members = inspect.getmembers(importlib.import_module(module))
members = list(filter(lambda c: c[0] == cls, members))
except ModuleNotFoundError:
return False
if members:
return members[0][1]
return False


def first_match_via_mro(model, path: str = "", suffix: str = ""):
paths = list(map(lambda x: x[:-1] + [path] + x[-1:], mro_paths(model)))
classes = [".".join(prefix) + suffix for prefix in paths]
return next(map(class_from_path, classes), None)
11 changes: 11 additions & 0 deletions apis_core/generic/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from rest_framework.serializers import ModelSerializer


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


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

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


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


class EditColumn(CustomTemplateColumn):
template_name = "columns/edit.html"
orderable = False
exclude_from_export = True
verbose_name = ""


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


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

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

def __init__(self, *args, **kwargs):
# if there is no custom sequence set, move `edit` and `delete` to the back
if "sequence" not in kwargs:
kwargs["sequence"] = ["...", "edit", "delete"]

super().__init__(*args, **kwargs)
3 changes: 3 additions & 0 deletions apis_core/generic/templates/columns/delete.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{% 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>
3 changes: 3 additions & 0 deletions apis_core/generic/templates/columns/edit.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{% load apiscore %}
<a href="{% url 'apis_core:generic:update' record|contenttype record.id %}"
class="text-warning"></a>
20 changes: 20 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,20 @@
{% extends basetemplate|default:"base.html" %}

{% block content %}
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-body">
<form action="" method="post">
{% csrf_token %}
<h4>
Confirm deletion of:
<div class="text-center">
<strong>{{ object }}</strong>
</div>
</h4>
<input class="btn btn-danger" type="submit" value="Yes, delete" />
</form>
</div>
</div>
</div>
{% endblock content %}
18 changes: 18 additions & 0 deletions apis_core/generic/templates/generic/generic_content.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{% 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 %}
9 changes: 9 additions & 0 deletions apis_core/generic/templates/generic/generic_form.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% 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 %}
Loading

0 comments on commit bafcf2e

Please sign in to comment.