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

129 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-09-16 07:42 +0000

1import difflib 

2import itertools 

3 

4from django.apps import apps 

5from django.contrib.contenttypes.models import ContentType 

6from django.core import serializers 

7from django.core.exceptions import ImproperlyConfigured 

8from django.db import DEFAULT_DB_ALIAS, router 

9from django.db.models import Q 

10from django_tables2 import RequestConfig 

11 

12from apis_core.apis_metainfo.models import Uri 

13from apis_core.apis_relations.models import Property, TempTriple 

14from apis_core.apis_relations.tables import get_generic_triple_table 

15from apis_core.generic.helpers import first_member_match, module_paths 

16from apis_core.utils.settings import get_entity_settings_by_modelname 

17 

18 

19def get_content_types_with_allowed_relation_from( 

20 content_type: ContentType, 

21) -> list[ContentType]: 

22 """Returns a list of ContentTypes to which the given ContentTypes may be related by a Property""" 

23 

24 # Find all the properties where the entity is either subject or object 

25 properties_with_entity_as_subject = Property.objects.filter( 

26 subj_class=content_type 

27 ).prefetch_related("obj_class") 

28 properties_with_entity_as_object = Property.objects.filter( 

29 obj_class=content_type 

30 ).prefetch_related("subj_class") 

31 

32 content_type_querysets = [] 

33 

34 # Where entity is subject, get all the object content_types 

35 for p in properties_with_entity_as_subject: 

36 objs = p.obj_class.all() 

37 content_type_querysets.append(objs) 

38 # Where entity is object, get all the subject content_types 

39 for p in properties_with_entity_as_object: 

40 subjs = p.subj_class.all() 

41 content_type_querysets.append(subjs) 

42 

43 # Join querysets with itertools.chain, call set to make unique, and extract the model class 

44 return set(itertools.chain(*content_type_querysets)) 

45 

46 

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

48 for model in models: 

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

50 DEFAULT_DB_ALIAS, model 

51 ): 

52 objects = model._default_manager 

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

54 yield from queryset.iterator() 

55 

56 

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

58 """ 

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

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

61 a serializer and natural foreign keys. 

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

63 """ 

64 

65 # get all APIS apps and all APIS models 

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

67 apis_app_models = [ 

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

69 ] 

70 

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

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

73 app_labels = set(apis_app_labels) 

74 app_labels |= set(additional_app_labels) 

75 

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

77 # app label to app_labels 

78 for model in apps.get_models(): 

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

80 app_labels.add(model._meta.app_label) 

81 

82 # now go through all app labels 

83 app_list = {} 

84 for app_label in app_labels: 

85 app_config = apps.get_app_config(app_label) 

86 app_list[app_config] = None 

87 

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

89 

90 yield from datadump_get_objects(models) 

91 

92 

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

94 return serializers.serialize( 

95 serialier_format, 

96 datadump_get_queryset(additional_app_labels), 

97 use_natural_foreign_keys=True, 

98 ) 

99 

100 

101def triple_sidebar(obj: object, request, detail=True): 

102 content_type = ContentType.objects.get_for_model(obj) 

103 side_bar = [] 

104 

105 triples_related_all = ( 

106 TempTriple.objects_inheritance.filter(Q(subj__pk=obj.pk) | Q(obj__pk=obj.pk)) 

107 .all() 

108 .select_subclasses() 

109 ) 

110 

111 for other_content_type in get_content_types_with_allowed_relation_from( 

112 content_type 

113 ): 

114 triples_related_by_entity = triples_related_all.filter( 

115 (Q(subj__self_contenttype=other_content_type) & Q(obj__pk=obj.pk)) 

116 | (Q(obj__self_contenttype=other_content_type) & Q(subj__pk=obj.pk)) 

117 ) 

118 

119 table_class = get_generic_triple_table( 

120 other_entity_class_name=other_content_type.model, 

121 entity_pk_self=obj.pk, 

122 detail=detail, 

123 ) 

124 

125 prefix = f"{other_content_type.model}" 

126 title_card = other_content_type.name 

127 tb_object = table_class(data=triples_related_by_entity, prefix=prefix) 

128 tb_object_open = request.GET.get(prefix + "page", None) 

129 entity_settings = get_entity_settings_by_modelname(content_type.model) 

130 per_page = entity_settings.get("relations_per_page", 10) 

131 RequestConfig(request, paginate={"per_page": per_page}).configure(tb_object) 

132 tab_id = f"triple_form_{content_type.model}_to_{other_content_type.model}" 

133 side_bar.append( 

134 ( 

135 title_card, 

136 tb_object, 

137 tab_id, 

138 tb_object_open, 

139 ) 

140 ) 

141 return side_bar 

142 

143 

144def get_importer_for_model(model: object): 

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

146 if importer := first_member_match(importer_paths): 

147 return importer 

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

149 

150 

151def create_object_from_uri(uri: str, model: object) -> object: 

152 if uri.startswith("http"): 

153 try: 

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

155 return uri.root_object 

156 except Uri.DoesNotExist: 

157 Importer = get_importer_for_model(model) 

158 importer = Importer(uri, model) 

159 instance = importer.create_instance() 

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

161 return instance 

162 return None 

163 

164 

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

166 """ 

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

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

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

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

171 """ 

172 

173 def style_remove(text): 

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

175 

176 def style_insert(text): 

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

178 

179 nones = ["", None] 

180 if a in nones and b in nones: 

181 result = "" 

182 elif a in nones: 

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

184 elif b in nones: 

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

186 else: 

187 result = "" 

188 a = str(a) 

189 b = str(b) 

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

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

192 match opcode: 

193 case "equal": 

194 equal = a[a_start:a_end] 

195 if shorten and len(equal) > shorten: 

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

197 result += equal 

198 case "delete": 

199 if show_a: 

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

201 case "insert": 

202 if show_b: 

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

204 case "replace": 

205 if show_b: 

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

207 if show_a: 

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

209 return result 

210 

211 

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

213 """ 

214 Helper method to parse input values and construct field lookups 

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

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

217 interpreted django lookup string and the trimmed value 

218 E.g. 

219 

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

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

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

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

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

225 

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

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

228 """ 

229 

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

231 value = value[1:] 

232 return "__iendswith", value 

233 

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

235 value = value[:-1] 

236 return "__istartswith", value 

237 

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

239 value = value[1:-1] 

240 return "__iexact", value 

241 

242 else: 

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

244 value = value[1:-1] 

245 return "__icontains", value