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

60 statements  

« prev     ^ index     » next       coverage.py v7.6.8, created at 2024-12-20 09:24 +0000

1import functools 

2import logging 

3 

4from django.contrib.auth import get_permission_codename 

5from django.db.models import CharField, Model, Q, TextField 

6from django.utils import module_loading 

7 

8logger = logging.getLogger(__name__) 

9 

10 

11def default_search_fields(model, field_names=None): 

12 """ 

13 Retrieve the default model fields to use for a search operation 

14 By default those are all the CharFields and TextFields of a model. 

15 It is also possible to define those fields on the model using the 

16 `_default_search_fields` attribute. 

17 The method also takes a `field_names` argument to override the list 

18 of fields. 

19 """ 

20 default_types = (CharField, TextField) 

21 fields = [ 

22 field for field in model._meta.get_fields() if isinstance(field, default_types) 

23 ] 

24 # check if the model has a `_default_search_fields` 

25 # list and use that as searchfields 

26 if isinstance(getattr(model, "_default_search_fields", None), list): 

27 fields = [ 

28 model._meta.get_field(field) for field in model._default_search_fields 

29 ] 

30 # if `fields_to_search` is a list, use that 

31 if isinstance(field_names, list): 

32 fields = [model._meta.get_field(field) for field in field_names] 

33 return fields 

34 

35 

36def generate_search_filter(model, query, fields_to_search=None, prefix=""): 

37 """ 

38 Generate a default search filter that searches for the `query` 

39 This helper can be used by autocomplete querysets if nothing 

40 fancier is needed. 

41 If the `prefix` is set, the field names will be prefixed with that string - 

42 this can be useful if you want to use the `generate_search_filter` in a 

43 `Q` combined query while searching over multiple models. 

44 """ 

45 query = query.split() 

46 

47 _fields_to_search = [ 

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

49 ] 

50 

51 q = Q() 

52 

53 for token in query: 

54 q &= functools.reduce( 

55 lambda acc, field_name: acc 

56 | Q(**{f"{prefix}{field_name}__icontains": token}), 

57 _fields_to_search, 

58 Q(), 

59 ) 

60 return q 

61 

62 

63@functools.lru_cache 

64def mro_paths(model): 

65 """ 

66 Create a list of MRO classes for a Django model 

67 """ 

68 paths = [] 

69 for cls in filter(lambda x: x not in Model.mro(), model.mro()): 

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

71 return paths 

72 

73 

74@functools.lru_cache 

75def template_names_via_mro(model, suffix=""): 

76 """ 

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

78 """ 

79 mro_prefix_list = ["/".join(prefix) for prefix in mro_paths(model)] 

80 return [f"{prefix.lower()}{suffix}" for prefix in mro_prefix_list] 

81 

82 

83@functools.lru_cache 

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

85 permission_codename = get_permission_codename(action, model._meta) 

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

87 

88 

89@functools.lru_cache 

90def module_paths(model, path: str = "", suffix: str = "") -> list: 

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

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

93 return classes 

94 

95 

96@functools.lru_cache 

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

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

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

100 return string 

101 

102 

103@functools.lru_cache 

104def import_string(dotted_path): 

105 try: 

106 return module_loading.import_string(dotted_path) 

107 except (ModuleNotFoundError, ImportError): 

108 return False 

109 

110 

111@functools.lru_cache 

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

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

114 pathgen = map(import_string, dotted_path_list) 

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

116 if result: 

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

118 else: 

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

120 return result or fallback