Coverage for apis_core/utils/helpers.py: 56%

78 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-10-30 12:03 +0000

1import difflib 

2 

3from django.apps import apps 

4from django.core import serializers 

5from django.db import DEFAULT_DB_ALIAS, router 

6 

7 

8def datadump_get_objects(models: list = [], *args, **kwargs): 

9 for model in models: 

10 if not model._meta.proxy and router.allow_migrate_model( 

11 DEFAULT_DB_ALIAS, model 

12 ): 

13 objects = model._default_manager 

14 queryset = objects.using(DEFAULT_DB_ALIAS).order_by(model._meta.pk.name) 

15 yield from queryset.iterator() 

16 

17 

18def datadump_get_queryset(additional_app_labels: list = []): 

19 """ 

20 This method is loosely based on the `dumpdata` admin command. 

21 It iterates throug the relevant app models and exports them using 

22 a serializer and natural foreign keys. 

23 Data exported this way can be reimported into a newly created Django APIS app 

24 """ 

25 

26 # get all APIS apps and all APIS models 

27 apis_app_labels = ["apis_relations", "apis_metainfo"] 

28 apis_app_models = [ 

29 model for model in apps.get_models() if model._meta.app_label in apis_app_labels 

30 ] 

31 

32 # create a list of app labels we want to iterate 

33 # this allows to extend the apps via the ?app_labels= parameter 

34 app_labels = set(apis_app_labels) 

35 app_labels |= set(additional_app_labels) 

36 

37 # look for models that inherit from APIS models and add their 

38 # app label to app_labels 

39 for model in apps.get_models(): 

40 if any(map(lambda x: issubclass(model, x), apis_app_models)): 

41 app_labels.add(model._meta.app_label) 

42 

43 # now go through all app labels 

44 app_list = {} 

45 for app_label in app_labels: 

46 app_config = apps.get_app_config(app_label) 

47 app_list[app_config] = None 

48 

49 models = serializers.sort_dependencies(app_list.items(), allow_cycles=True) 

50 

51 yield from datadump_get_objects(models) 

52 

53 

54def datadump_serializer(additional_app_labels: list = [], serialier_format="json"): 

55 return serializers.serialize( 

56 serialier_format, 

57 datadump_get_queryset(additional_app_labels), 

58 use_natural_foreign_keys=True, 

59 ) 

60 

61 

62def get_html_diff(a, b, show_a=True, show_b=True, shorten=0): 

63 """ 

64 Create an colorized html represenation of the difference of two values a and b 

65 If `show_a` is True, colorize deletions in `a` 

66 If `show_b` is True, colorize insertions in `b` 

67 The value of `shorten` defines if long parts of strings that contains no change should be shortened 

68 """ 

69 

70 def style_remove(text): 

71 return f"<span class='diff-remove'>{text}</span>" 

72 

73 def style_insert(text): 

74 return f"<span class='diff-insert'>{text}</span>" 

75 

76 nones = ["", None] 

77 if a in nones and b in nones: 

78 result = "" 

79 elif a in nones: 

80 result = style_insert(b) if show_b else "" 

81 elif b in nones: 

82 result = style_remove(a) if show_a else "" 

83 else: 

84 result = "" 

85 a = str(a) 

86 b = str(b) 

87 codes = difflib.SequenceMatcher(None, a, b).get_opcodes() 

88 for opcode, a_start, a_end, b_start, b_end in codes: 

89 match opcode: 

90 case "equal": 

91 equal = a[a_start:a_end] 

92 if shorten and len(equal) > shorten: 

93 equal = equal[:5] + " ... " + equal[-10:] 

94 result += equal 

95 case "delete": 

96 if show_a: 

97 result += style_remove(a[a_start:a_end]) 

98 case "insert": 

99 if show_b: 

100 result += style_insert(b[b_start:b_end]) 

101 case "replace": 

102 if show_b: 

103 result += style_insert(b[b_start:b_end]) 

104 if show_a: 

105 result += style_remove(a[a_start:a_end]) 

106 return result 

107 

108 

109def construct_lookup(value: str) -> tuple[str, str]: 

110 """ 

111 Helper method to parse input values and construct field lookups 

112 (https://docs.djangoproject.com/en/4.2/ref/models/querysets/#field-lookups) 

113 Parses user input for wildcards and returns a tuple containing the 

114 interpreted django lookup string and the trimmed value 

115 E.g. 

116 

117 - ``example`` -> ``('__icontains', 'example')`` 

118 - ``*example*`` -> ``('__icontains', 'example')`` 

119 - ``*example`` -> ``('__iendswith', 'example')`` 

120 - ``example*``-> ``('__istartswith', 'example')`` 

121 - ``"example"`` -> ``('__iexact', 'example')`` 

122 

123 :param str value: text to be parsed for ``*`` 

124 :return: a tuple containing the lookup type and the value without modifiers 

125 """ 

126 

127 if value.startswith("*") and not value.endswith("*"): 

128 value = value[1:] 

129 return "__iendswith", value 

130 

131 elif not value.startswith("*") and value.endswith("*"): 

132 value = value[:-1] 

133 return "__istartswith", value 

134 

135 elif value.startswith('"') and value.endswith('"'): 

136 value = value[1:-1] 

137 return "__iexact", value 

138 

139 else: 

140 if value.startswith("*") and value.endswith("*"): 

141 value = value[1:-1] 

142 return "__icontains", value 

143 

144 

145def flatten_if_single(value: list): 

146 if len(value) == 1: 

147 return value[0] 

148 return value