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

135 statements  

« prev     ^ index     » next       coverage.py v7.15.0, created at 2026-07-28 06:34 +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, content_types): 

141 user = context.request.user 

142 return any( 

143 [user.has_perm(ct.model_class().get_view_permission()) for ct in content_types] 

144 ) 

145 

146 

147@register.simple_tag 

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

149 """ 

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

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

152 """ 

153 content_types = [] 

154 for key in natural_keys: 

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

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

157 content_types.append(content_type) 

158 return content_types 

159 

160 

161@register.simple_tag 

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

163 """ 

164 Convert a list of ContentType models to their natural key 

165 """ 

166 natural_keys = [] 

167 for content_type in content_types: 

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

169 return natural_keys 

170 

171 

172@register.filter 

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

174 """ 

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

176 leading and trailing whitespaces. 

177 """ 

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

179 

180 

181@register.simple_tag 

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

183 """ 

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

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

186 path. 

187 """ 

188 content_type = ContentType.objects.get_for_model(model) 

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

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

191 

192 

193def template_exists(value): 

194 try: 

195 template.loader.get_template(value) 

196 return True 

197 except template.TemplateDoesNotExist: 

198 return False 

199 

200 

201@register.simple_tag 

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

203 """ 

204 List templates found in the installed apps template folder. 

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

206 `suffix` 

207 """ 

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

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

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

211 return existing 

212 

213 

214@register.simple_tag 

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

216 """ 

217 Return a list of `GenericModel` models 

218 """ 

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

220 

221 

222@register.simple_tag 

223def regroup_filter_empty( 

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

225) -> list[models.Model]: 

226 """ 

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

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

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

230 """ 

231 models = filter( 

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

233 ) 

234 groups = defaultdict(list) 

235 for model in models: 

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

237 return dict(groups)