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

98 statements  

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

1import difflib 

2 

3from django.apps import apps 

4from django.contrib.contenttypes.models import ContentType 

5from django.core import serializers 

6from django.core.exceptions import ImproperlyConfigured 

7from django.db import DEFAULT_DB_ALIAS, router 

8 

9from apis_core.apis_metainfo.models import Uri 

10from apis_core.generic.helpers import first_member_match, module_paths 

11 

12 

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

14 for model in models: 

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

16 DEFAULT_DB_ALIAS, model 

17 ): 

18 objects = model._default_manager 

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

20 yield from queryset.iterator() 

21 

22 

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

24 """ 

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

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

27 a serializer and natural foreign keys. 

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

29 """ 

30 

31 # get all APIS apps and all APIS models 

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

33 apis_app_models = [ 

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

35 ] 

36 

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

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

39 app_labels = set(apis_app_labels) 

40 app_labels |= set(additional_app_labels) 

41 

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

43 # app label to app_labels 

44 for model in apps.get_models(): 

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

46 app_labels.add(model._meta.app_label) 

47 

48 # now go through all app labels 

49 app_list = {} 

50 for app_label in app_labels: 

51 app_config = apps.get_app_config(app_label) 

52 app_list[app_config] = None 

53 

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

55 

56 yield from datadump_get_objects(models) 

57 

58 

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

60 return serializers.serialize( 

61 serialier_format, 

62 datadump_get_queryset(additional_app_labels), 

63 use_natural_foreign_keys=True, 

64 ) 

65 

66 

67def get_importer_for_model(model: object): 

68 importer_paths = module_paths(model, path="importers", suffix="Importer") 

69 if importer := first_member_match(importer_paths): 

70 return importer 

71 raise ImproperlyConfigured(f"No suitable importer found for {model}") 

72 

73 

74def create_object_from_uri(uri: str, model: object, raise_on_fail=False) -> object: 

75 if uri.startswith("http"): 

76 try: 

77 uri = Uri.objects.get(uri=uri) 

78 return uri.root_object 

79 except Uri.DoesNotExist: 

80 Importer = get_importer_for_model(model) 

81 importer = Importer(uri, model) 

82 instance = importer.create_instance() 

83 uri = Uri.objects.create(uri=importer.get_uri, root_object=instance) 

84 return instance 

85 if raise_on_fail: 

86 content_type = ContentType.objects.get_for_model(model) 

87 raise ImproperlyConfigured( 

88 f'Could not create {content_type.name} from string "{uri}"' 

89 ) 

90 return False 

91 

92 

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

94 """ 

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

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

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

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

99 """ 

100 

101 def style_remove(text): 

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

103 

104 def style_insert(text): 

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

106 

107 nones = ["", None] 

108 if a in nones and b in nones: 

109 result = "" 

110 elif a in nones: 

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

112 elif b in nones: 

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

114 else: 

115 result = "" 

116 a = str(a) 

117 b = str(b) 

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

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

120 match opcode: 

121 case "equal": 

122 equal = a[a_start:a_end] 

123 if shorten and len(equal) > shorten: 

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

125 result += equal 

126 case "delete": 

127 if show_a: 

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

129 case "insert": 

130 if show_b: 

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

132 case "replace": 

133 if show_b: 

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

135 if show_a: 

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

137 return result 

138 

139 

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

141 """ 

142 Helper method to parse input values and construct field lookups 

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

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

145 interpreted django lookup string and the trimmed value 

146 E.g. 

147 

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

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

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

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

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

153 

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

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

156 """ 

157 

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

159 value = value[1:] 

160 return "__iendswith", value 

161 

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

163 value = value[:-1] 

164 return "__istartswith", value 

165 

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

167 value = value[1:-1] 

168 return "__iexact", value 

169 

170 else: 

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

172 value = value[1:-1] 

173 return "__icontains", value