Coverage for apis_core/generic/templatetags/generic.py: 70%

135 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-09-30 06:11 +0000

1import functools 

2from collections import defaultdict 

3 

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 

11 

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 

15 

16register = template.Library() 

17 

18 

19@register.filter 

20def contenttype(model): 

21 return ContentType.objects.get_for_model(model) 

22 

23 

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 

58 

59 

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() 

66 

67 

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) 

71 

72 

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 

86 

87 

88@register.filter 

89def get_attribute(obj, attribute): 

90 return getattr(obj, attribute, None) 

91 

92 

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() 

99 

100 

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 ) 

106 

107 

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]) 

112 

113 

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 

126 

127 

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 

137 

138 

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))) 

146 

147 

148@register.simple_tag 

149def model_field_template_lookup_list(model, field, suffix="") -> list[str]: 

150 """ 

151 generate a template path based on the modelname, the fieldname and 

152 the suffix. return a list with this template path and a fallback 

153 path. 

154 """ 

155 content_type = ContentType.objects.get_for_model(model) 

156 path = f"{content_type.app_label}/partials/{content_type.model}_{field.name}_{suffix}.html" 

157 return [path, f"generic/partials/default_model_field_{suffix}.html"] 

158 

159 

160def template_exists(value): 

161 try: 

162 template.loader.get_template(value) 

163 return True 

164 except template.TemplateDoesNotExist: 

165 return False 

166 

167 

168@register.simple_tag 

169def app_templates(prefix: str = "", suffix: str = ""): 

170 """ 

171 List templates found in the installed apps template folder. 

172 The template path is prefixed with `prefix` and suffixed with 

173 `suffix` 

174 """ 

175 labels = [app.label for app in apps.get_app_configs()] 

176 templates = [f"{prefix}{label}{suffix}" for label in labels] 

177 existing = [template for template in templates if template_exists(template)] 

178 return existing 

179 

180 

181@register.simple_tag 

182def get_genericmodels() -> list[GenericModel]: 

183 """ 

184 Return a list of `GenericModel` models 

185 """ 

186 return list(filter(lambda x: issubclass(x, GenericModel), apps.get_models())) 

187 

188 

189@register.simple_tag 

190def regroup_filter_empty( 

191 models: list[models.Model], attribute: str 

192) -> list[models.Model]: 

193 """ 

194 Group a list of models based on the value of `attribute`. This is similar to the 

195 [regroup](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#regroup) 

196 tag, but it filters out `None` values before grouping instead of breaking on them. 

197 """ 

198 models = filter( 

199 lambda x: functools.reduce(getattr, attribute.split("."), x), models 

200 ) 

201 groups = defaultdict(list) 

202 for model in models: 

203 groups[functools.reduce(getattr, attribute.split("."), model)].append(model) 

204 return dict(groups) 

205 

206 

207@register.simple_tag 

208def get_pure_genericmodels() -> list[GenericModel]: 

209 parents = [] 

210 if apps.is_installed("apis_core.collections"): 

211 collections = apps.get_app_config("collections") 

212 parents.append(collections.models_module.SkosCollection) 

213 parents.append(collections.models_module.SkosCollectionContentObject) 

214 if apps.is_installed("apis_core.relations"): 

215 relations = apps.get_app_config("relations") 

216 parents.append(relations.models_module.Relation) 

217 if apps.is_installed("apis_core.history"): 

218 history = apps.get_app_config("history") 

219 parents.append(history.models_module.APISHistoryTableBase) 

220 if apps.is_installed("apis_core.entities"): 

221 entities = apps.get_app_config("entities") 

222 parents.append(entities.module.abc.Entity) 

223 genericmodels = [ 

224 model 

225 for model in set(get_genericmodels()) 

226 if not issubclass(model, tuple(parents)) 

227 ] 

228 return genericmodels 

229 

230 

231@register.simple_tag 

232def sort_object_on_attribute(objects: list[object], attribute: str) -> list[object]: 

233 """ 

234 Sort a list of objects based on an attribute. The attribute can be nested, like 

235 `some.nested.attribute` 

236 """ 

237 return sorted( 

238 objects, key=lambda x: functools.reduce(getattr, attribute.split("."), x) 

239 )