-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create generic Create, View, Update, Delete and List views that work with ContentTypes Closes: #509
- Loading branch information
Showing
19 changed files
with
561 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
20
apis_core/generic/templates/generic/generic_confirm_delete.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 %} |
Oops, something went wrong.