Coverage for apis_core/generic/helpers.py: 84%
81 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-09-30 06:11 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-09-30 06:11 +0000
1import functools
2import logging
4from django.conf import settings
5from django.contrib.auth import get_permission_codename
6from django.db.models import CharField, Model, Q, TextField
7from django.utils import module_loading
9logger = logging.getLogger(__name__)
12def default_search_fields(model, field_names=None):
13 """
14 Retrieve the default model fields to use for a search operation
15 By default those are all the CharFields and TextFields of a model.
16 It is also possible to define those fields on the model using the
17 `_default_search_fields` attribute.
18 The method also takes a `field_names` argument to override the list
19 of fields.
20 """
21 default_types = (CharField, TextField)
22 fields = [
23 field for field in model._meta.get_fields() if isinstance(field, default_types)
24 ]
25 # check if the model has a `_default_search_fields`
26 # list and use that as searchfields
27 if isinstance(getattr(model, "_default_search_fields", None), list):
28 fields = [
29 model._meta.get_field(field) for field in model._default_search_fields
30 ]
31 # if `fields_to_search` is a list, use that
32 if isinstance(field_names, list):
33 fields = [model._meta.get_field(field) for field in field_names]
34 return fields
37def generate_search_filter(model, query, fields_to_search=None, prefix=""):
38 """
39 Generate a default search filter that searches for the `query`
40 This helper can be used by autocomplete querysets if nothing
41 fancier is needed.
42 If the `prefix` is set, the field names will be prefixed with that string -
43 this can be useful if you want to use the `generate_search_filter` in a
44 `Q` combined query while searching over multiple models.
45 """
46 if isinstance(query, str):
47 query = query.split()
49 _fields_to_search = [
50 field.name for field in default_search_fields(model, fields_to_search)
51 ]
53 q = Q()
55 for token in query:
56 q &= functools.reduce(
57 lambda acc, field_name: (
58 acc | Q(**{f"{prefix}{field_name}__icontains": token})
59 ),
60 _fields_to_search,
61 Q(),
62 )
63 return q
66@functools.lru_cache
67def mro_paths(model):
68 """
69 Create a list of MRO classes for a Django model
70 """
71 paths = []
72 if model is not None:
73 for cls in filter(
74 lambda x: x not in Model.mro() and "." in x.__module__, model.mro()
75 ):
76 paths.append(tuple([cls.__module__.split(".")[-2], cls.__name__]))
77 paths.append(tuple(cls.__module__.split(".")[:-1] + [cls.__name__]))
78 return [list(path) for path in list(dict.fromkeys(paths))]
81def template_names_via_mro(model, folder="", prefix="", suffix=""):
82 """
83 Use the MRO to generate a list of template names for a model
84 """
85 results = []
86 for path in mro_paths(model):
87 path = path.copy() # copy, so we don't modify the original list
88 if folder:
89 path.insert(len(path) - 1, folder)
90 path[-1] = f"{prefix}{path[-1]}{suffix}"
91 results.append("/".join(path).lower())
92 return results
95@functools.lru_cache
96def permission_fullname(action: str, model: object) -> str:
97 permission_codename = get_permission_codename(action, model._meta)
98 return f"{model._meta.app_label}.{permission_codename}"
101@functools.lru_cache
102def module_paths(model, path: str = "", suffix: str = "") -> tuple:
103 paths = list(map(lambda x: x[:-1] + [path] + x[-1:], mro_paths(model)))
104 prepends = []
105 for prepend in getattr(settings, "ADDITIONAL_MODULE_LOOKUP_PATHS", []):
106 prepends.extend(
107 list(map(lambda x: prepend.split(".") + [path] + x[-1:], mro_paths(model)))
108 )
109 classes = tuple(".".join(prefix) + suffix for prefix in prepends + paths)
110 return classes
113@functools.lru_cache
114def makeclassprefix(string: str) -> str:
115 string = "".join([c if c.isidentifier() else " " for c in string])
116 string = "".join([word.strip().capitalize() for word in string.split(" ")])
117 return string
120@functools.lru_cache
121def import_string(dotted_path):
122 try:
123 return module_loading.import_string(dotted_path)
124 except (ModuleNotFoundError, ImportError) as e:
125 logger.debug("Could not load %s: %s", dotted_path, e)
126 return False
129@functools.lru_cache
130def first_member_match(dotted_path_list: tuple[str], fallback=None) -> object:
131 logger.debug("Looking for matching class in %s", dotted_path_list)
132 pathgen = map(import_string, dotted_path_list)
133 result = next(filter(bool, pathgen), None)
134 if result:
135 logger.debug("Found matching attribute/class in %s", result)
136 else:
137 logger.debug("Found nothing, returning fallback: %s", fallback)
138 return result or fallback
141def split_and_strip_parameter(params: list[str]) -> list[str]:
142 """
143 Clean a URI param list type
144 This method iterates through a list of strings. It looks if
145 the items contain a comma separated list of items and then splits
146 those and also runs strip on all those items.
147 So out of ["foo.bar", "bar.faz, faz.foo"] it
148 creates a list ["foo.bar", "bar.faz", "faz.foo"]
149 """
150 newlist = []
151 for param in params:
152 subparams = map(str.strip, param.split(","))
153 newlist.extend(subparams)
154 return newlist
157def string_to_bool(string: str = "false") -> bool:
158 """
159 Convert a string to a boolean representing its semantic value
160 """
161 return string.lower() == "true"