Coverage for apis_core / generic / helpers.py: 84%

81 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-27 05:15 +0000

1import functools 

2import logging 

3 

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 

8 

9logger = logging.getLogger(__name__) 

10 

11 

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 

35 

36 

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

48 

49 _fields_to_search = [ 

50 field.name for field in default_search_fields(model, fields_to_search) 

51 ] 

52 

53 q = Q() 

54 

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 

64 

65 

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(lambda x: x not in Model.mro(), model.mro()): 

74 paths.append(tuple([cls.__module__.split(".")[-2], cls.__name__])) 

75 paths.append(tuple(cls.__module__.split(".")[:-1] + [cls.__name__])) 

76 return [list(path) for path in list(dict.fromkeys(paths))] 

77 

78 

79def template_names_via_mro(model, folder="", prefix="", suffix=""): 

80 """ 

81 Use the MRO to generate a list of template names for a model 

82 """ 

83 results = [] 

84 for path in mro_paths(model): 

85 path = path.copy() # copy, so we don't modify the original list 

86 if folder: 

87 path.insert(len(path) - 1, folder) 

88 path[-1] = f"{prefix}{path[-1]}{suffix}" 

89 results.append("/".join(path).lower()) 

90 return results 

91 

92 

93@functools.lru_cache 

94def permission_fullname(action: str, model: object) -> str: 

95 permission_codename = get_permission_codename(action, model._meta) 

96 return f"{model._meta.app_label}.{permission_codename}" 

97 

98 

99@functools.lru_cache 

100def module_paths(model, path: str = "", suffix: str = "") -> tuple: 

101 paths = list(map(lambda x: x[:-1] + [path] + x[-1:], mro_paths(model))) 

102 prepends = [] 

103 for prepend in getattr(settings, "ADDITIONAL_MODULE_LOOKUP_PATHS", []): 

104 prepends.extend( 

105 list(map(lambda x: prepend.split(".") + [path] + x[-1:], mro_paths(model))) 

106 ) 

107 classes = tuple(".".join(prefix) + suffix for prefix in prepends + paths) 

108 return classes 

109 

110 

111@functools.lru_cache 

112def makeclassprefix(string: str) -> str: 

113 string = "".join([c if c.isidentifier() else " " for c in string]) 

114 string = "".join([word.strip().capitalize() for word in string.split(" ")]) 

115 return string 

116 

117 

118@functools.lru_cache 

119def import_string(dotted_path): 

120 try: 

121 return module_loading.import_string(dotted_path) 

122 except (ModuleNotFoundError, ImportError) as e: 

123 logger.debug("Could not load %s: %s", dotted_path, e) 

124 return False 

125 

126 

127@functools.lru_cache 

128def first_member_match(dotted_path_list: tuple[str], fallback=None) -> object: 

129 logger.debug("Looking for matching class in %s", dotted_path_list) 

130 pathgen = map(import_string, dotted_path_list) 

131 result = next(filter(bool, pathgen), None) 

132 if result: 

133 logger.debug("Found matching attribute/class in %s", result) 

134 else: 

135 logger.debug("Found nothing, returning fallback: %s", fallback) 

136 return result or fallback 

137 

138 

139def split_and_strip_parameter(params: list[str]) -> list[str]: 

140 """ 

141 Clean a URI param list type 

142 This method iterates through a list of strings. It looks if 

143 the items contain a comma separated list of items and then splits 

144 those and also runs strip on all those items. 

145 So out of ["foo.bar", "bar.faz, faz.foo"] it 

146 creates a list ["foo.bar", "bar.faz", "faz.foo"] 

147 """ 

148 newlist = [] 

149 for param in params: 

150 subparams = map(str.strip, param.split(",")) 

151 newlist.extend(subparams) 

152 return newlist 

153 

154 

155def string_to_bool(string: str = "false") -> bool: 

156 """ 

157 Convert a string to a boolean representing its semantic value 

158 """ 

159 return string.lower() == "true"