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

159 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-09-14 10:55 +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.simple_tag 

89def pure_genericmodel_content_types(): 

90 """ 

91 Retrieve all models which inherit from GenericModel class 

92 but are not Collections, Entities, Relations or History models 

93 """ 

94 parents = [] 

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

96 collections = apps.get_app_config("collections") 

97 parents.append(collections.models_module.SkosCollection) 

98 parents.append(collections.models_module.SkosCollectionContentObject) 

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

100 relations = apps.get_app_config("relations") 

101 parents.append(relations.models_module.Relation) 

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

103 history = apps.get_app_config("history") 

104 parents.append(history.models_module.APISHistoryTableBase) 

105 if apps.is_installed("apis_core.apis_entities"): 

106 entities = apps.get_app_config("apis_entities") 

107 parents.append(entities.models_module.AbstractEntity) 

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

109 entities = apps.get_app_config("entities") 

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

111 genericmodels = [ 

112 ct 

113 for ct in set(genericmodel_content_types()) 

114 if not issubclass(ct.model_class(), tuple(parents)) 

115 ] 

116 return genericmodels 

117 

118 

119@register.filter 

120def get_attribute(obj, attribute): 

121 return getattr(obj, attribute, None) 

122 

123 

124@register.filter 

125def content_type_count(content_type): 

126 """ 

127 Return the number of objects having a specific content type 

128 """ 

129 return content_type.model_class().objects.count() 

130 

131 

132@register.simple_tag 

133def model_mro_templates(obj, folder="", prefix="", suffix=""): 

134 return template_names_via_mro( 

135 type(obj), folder=folder, prefix=prefix, suffix=suffix 

136 ) 

137 

138 

139@register.simple_tag(takes_context=True) 

140def any_view_permission(context, models): 

141 user = context.request.user 

142 return any([user.has_perm(model.get_view_permission()) for model in models]) 

143 

144 

145@register.simple_tag 

146def content_types_by_natural_keys(natural_keys: tuple = ()) -> list[ContentType]: 

147 """ 

148 Convert a list of natural keys to a list of ContentType models 

149 If any of the natural keys does not refer to an existing model, raise a 404 

150 """ 

151 content_types = [] 

152 for key in natural_keys: 

153 app_label, model = key.split(".") 

154 content_type = get_object_or_404(ContentType, app_label=app_label, model=model) 

155 content_types.append(content_type) 

156 return content_types 

157 

158 

159@register.simple_tag 

160def natural_keys_by_content_types(content_types: tuple = ()) -> list[str]: 

161 """ 

162 Convert a list of ContentType models to their natural key 

163 """ 

164 natural_keys = [] 

165 for content_type in content_types: 

166 natural_keys.append(content_type.app_label + "." + content_type.model) 

167 return natural_keys 

168 

169 

170@register.filter 

171def split(string: str = "", delimiter=",") -> list[str]: 

172 """ 

173 Split a string by a specific delimiter and also strip the string of 

174 leading and trailing whitespaces. 

175 """ 

176 return list(map(str.strip, string.split(delimiter))) 

177 

178 

179@register.simple_tag 

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

181 """ 

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

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

184 path. 

185 """ 

186 content_type = ContentType.objects.get_for_model(model) 

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

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

189 

190 

191def template_exists(value): 

192 try: 

193 template.loader.get_template(value) 

194 return True 

195 except template.TemplateDoesNotExist: 

196 return False 

197 

198 

199@register.simple_tag 

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

201 """ 

202 List templates found in the installed apps template folder. 

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

204 `suffix` 

205 """ 

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

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

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

209 return existing 

210 

211 

212@register.simple_tag 

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

214 """ 

215 Return a list of `GenericModel` models 

216 """ 

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

218 

219 

220@register.simple_tag 

221def regroup_filter_empty( 

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

223) -> list[models.Model]: 

224 """ 

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

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

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

228 """ 

229 models = filter( 

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

231 ) 

232 groups = defaultdict(list) 

233 for model in models: 

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

235 return dict(groups) 

236 

237 

238@register.simple_tag 

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

240 parents = [] 

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

242 collections = apps.get_app_config("collections") 

243 parents.append(collections.models_module.SkosCollection) 

244 parents.append(collections.models_module.SkosCollectionContentObject) 

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

246 relations = apps.get_app_config("relations") 

247 parents.append(relations.models_module.Relation) 

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

249 history = apps.get_app_config("history") 

250 parents.append(history.models_module.APISHistoryTableBase) 

251 if apps.is_installed("apis_core.apis_entities"): 

252 entities = apps.get_app_config("apis_entities") 

253 parents.append(entities.models_module.AbstractEntity) 

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

255 entities = apps.get_app_config("entities") 

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

257 genericmodels = [ 

258 model 

259 for model in set(get_genericmodels()) 

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

261 ] 

262 return genericmodels 

263 

264 

265@register.simple_tag 

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

267 """ 

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

269 `some.nested.attribute` 

270 """ 

271 return sorted( 

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

273 )