Coverage for apis_core/generic/templatetags/generic.py: 70%
137 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-10-28 13:07 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-10-28 13:07 +0000
1import functools
2from collections import defaultdict
4from django import template
5from django.apps import apps
6from django.contrib.contenttypes.fields import GenericForeignKey
7from django.contrib.contenttypes.models import ContentType
8from django.core.exceptions import ObjectDoesNotExist
9from django.db import models
10from django.shortcuts import get_object_or_404
12from apis_core.core.templatetags.core import get_model_fields
13from apis_core.generic.abc import GenericModel
14from apis_core.generic.helpers import template_names_via_mro
16register = template.Library()
19@register.filter
20def contenttype(model):
21 return ContentType.objects.get_for_model(model)
24@register.simple_tag
25def modeldict(instance, fields=None, exclude=None, exclude_noneditable=True):
26 data = {}
27 for f in get_model_fields(instance):
28 if not getattr(f, "editable", False) and exclude_noneditable:
29 continue
30 if fields is not None and f.name not in fields:
31 continue
32 if exclude and f.name in exclude:
33 continue
34 field = instance._meta.get_field(f.name)
35 try:
36 data[field] = getattr(instance, field.name)
37 except ObjectDoesNotExist as e:
38 data[field] = f"{field.value_from_object(instance)} ({e})"
39 if fn := getattr(instance, f"get_{field.name}_display", False):
40 data[field] = fn()
41 if getattr(field, "m2m_field_name", False):
42 values = getattr(instance, field.name).all()
43 data[field] = ", ".join([str(value) for value in values])
44 # if there are generic foreign keys defined in the model,
45 # return the object they point to and remove the
46 # individual attributes they are composed of. if the
47 # generic fk does **not** resolve, keep everything as is
48 for field in filter(
49 lambda x: isinstance(x, GenericForeignKey), get_model_fields(instance)
50 ):
51 if getattr(instance, field.name):
52 data[field] = getattr(instance, field.name)
53 ct_field = instance._meta.get_field(field.ct_field)
54 del data[ct_field]
55 fk_field = instance._meta.get_field(field.fk_field)
56 del data[fk_field]
57 return data
60@register.simple_tag
61def contenttypes(app_labels=None):
62 if app_labels:
63 app_labels = app_labels.split(",")
64 return ContentType.objects.filter(app_label__in=app_labels)
65 return ContentType.objects.all()
68def is_genericmodel(content_type: ContentType):
69 model_class = content_type.model_class()
70 return model_class is not None and issubclass(model_class, GenericModel)
73@register.simple_tag
74def genericmodel_content_types():
75 """
76 Retrieve all models which inherit from GenericModel class
77 and return their ContentType.
78 """
79 genericmodels = list(
80 filter(
81 lambda content_type: is_genericmodel(content_type),
82 ContentType.objects.all(),
83 )
84 )
85 return genericmodels
88@register.filter
89def get_attribute(obj, attribute):
90 return getattr(obj, attribute, None)
93@register.filter
94def content_type_count(content_type):
95 """
96 Return the number of objects having a specific content type
97 """
98 return content_type.model_class().objects.count()
101@register.simple_tag
102def model_mro_templates(obj, folder="", prefix="", suffix=""):
103 return template_names_via_mro(
104 type(obj), folder=folder, prefix=prefix, suffix=suffix
105 )
108@register.simple_tag(takes_context=True)
109def any_view_permission(context, models):
110 user = context.request.user
111 return any([user.has_perm(model.get_view_permission()) for model in models])
114@register.simple_tag
115def content_types_by_natural_keys(natural_keys: tuple = ()) -> list[ContentType]:
116 """
117 Convert a list of natural keys to a list of ContentType models
118 If any of the natural keys does not refer to an existing model, raise a 404
119 """
120 content_types = []
121 for key in natural_keys:
122 app_label, model = key.split(".")
123 content_type = get_object_or_404(ContentType, app_label=app_label, model=model)
124 content_types.append(content_type)
125 return content_types
128@register.simple_tag
129def natural_keys_by_content_types(content_types: tuple = ()) -> list[str]:
130 """
131 Convert a list of ContentType models to their natural key
132 """
133 natural_keys = []
134 for content_type in content_types:
135 natural_keys.append(content_type.app_label + "." + content_type.model)
136 return natural_keys
139@register.filter
140def split(string: str = "", delimiter=",") -> list[str]:
141 """
142 Split a string by a specific delimiter and also strip the string of
143 leading and trailing whitespaces.
144 """
145 return list(map(str.strip, string.split(delimiter)))
148@register.simple_tag
149def model_field_template_lookup_list(model, field, suffix="") -> list[str]:
150 """
151 Generate a list of template paths based on the modelname, the field and
152 the suffix. The list contains a template based on the field type and the
153 suffix, a template based on the model name, the field name and the suffix
154 and a generic fallback template that is only based on the suffix.
155 """
156 content_type = ContentType.objects.get_for_model(model)
157 path = f"{content_type.app_label}/partials/{content_type.model}_{field.name}_{suffix}.html"
158 return [
159 path,
160 f"generic/partials/default_{field.get_internal_type().lower()}_{suffix}.html",
161 f"generic/partials/default_model_field_{suffix}.html",
162 ]
165def template_exists(value):
166 try:
167 template.loader.get_template(value)
168 return True
169 except template.TemplateDoesNotExist:
170 return False
173@register.simple_tag
174def app_templates(prefix: str = "", suffix: str = "", reverse=True):
175 """
176 List templates found in the installed apps template folder.
177 The template path is prefixed with `prefix` and suffixed with
178 `suffix`
179 """
180 labels = [app.label for app in apps.get_app_configs()]
181 templates = [f"{prefix}{label}{suffix}" for label in labels]
182 existing = [template for template in templates if template_exists(template)]
183 if reverse:
184 existing.reverse()
185 return existing
188@register.simple_tag
189def get_genericmodels() -> list[GenericModel]:
190 """
191 Return a list of `GenericModel` models
192 """
193 return list(filter(lambda x: issubclass(x, GenericModel), apps.get_models()))
196@register.simple_tag
197def regroup_filter_empty(
198 models: list[models.Model], attribute: str
199) -> list[models.Model]:
200 """
201 Group a list of models based on the value of `attribute`. This is similar to the
202 [regroup](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#regroup)
203 tag, but it filters out `None` values before grouping instead of breaking on them.
204 """
205 models = filter(
206 lambda x: functools.reduce(getattr, attribute.split("."), x), models
207 )
208 groups = defaultdict(list)
209 for model in models:
210 groups[functools.reduce(getattr, attribute.split("."), model)].append(model)
211 return dict(groups)
214@register.simple_tag
215def get_pure_genericmodels() -> list[GenericModel]:
216 parents = []
217 if apps.is_installed("apis_core.collections"):
218 collections = apps.get_app_config("collections")
219 parents.append(collections.models_module.SkosCollection)
220 parents.append(collections.models_module.SkosCollectionContentObject)
221 if apps.is_installed("apis_core.relations"):
222 relations = apps.get_app_config("relations")
223 parents.append(relations.models_module.Relation)
224 if apps.is_installed("apis_core.history"):
225 history = apps.get_app_config("history")
226 parents.append(history.models_module.APISHistoryTableBase)
227 if apps.is_installed("apis_core.entities"):
228 entities = apps.get_app_config("entities")
229 parents.append(entities.module.abc.Entity)
230 genericmodels = [
231 model
232 for model in set(get_genericmodels())
233 if not issubclass(model, tuple(parents))
234 ]
235 return genericmodels
238@register.simple_tag
239def sort_object_on_attribute(objects: list[object], attribute: str) -> list[object]:
240 """
241 Sort a list of objects based on an attribute. The attribute can be nested, like
242 `some.nested.attribute`
243 """
244 return sorted(
245 objects, key=lambda x: functools.reduce(getattr, attribute.split("."), x)
246 )