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
« prev ^ index » next coverage.py v7.6.1, created at 2024-09-16 07:42 +0000
1import difflib
2import itertools
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
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
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"""
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")
32 content_type_querysets = []
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)
43 # Join querysets with itertools.chain, call set to make unique, and extract the model class
44 return set(itertools.chain(*content_type_querysets))
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()
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 """
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 ]
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)
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)
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
88 models = serializers.sort_dependencies(app_list.items(), allow_cycles=True)
90 yield from datadump_get_objects(models)
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 )
101def triple_sidebar(obj: object, request, detail=True):
102 content_type = ContentType.objects.get_for_model(obj)
103 side_bar = []
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 )
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 )
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 )
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
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}")
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
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 """
173 def style_remove(text):
174 return f"<span class='diff-remove'>{text}</span>"
176 def style_insert(text):
177 return f"<span class='diff-insert'>{text}</span>"
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
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.
220 - ``example`` -> ``('__icontains', 'example')``
221 - ``*example*`` -> ``('__icontains', 'example')``
222 - ``*example`` -> ``('__iendswith', 'example')``
223 - ``example*``-> ``('__istartswith', 'example')``
224 - ``"example"`` -> ``('__iexact', 'example')``
226 :param str value: text to be parsed for ``*``
227 :return: a tuple containing the lookup type and the value without modifiers
228 """
230 if value.startswith("*") and not value.endswith("*"):
231 value = value[1:]
232 return "__iendswith", value
234 elif not value.startswith("*") and value.endswith("*"):
235 value = value[:-1]
236 return "__istartswith", value
238 elif value.startswith('"') and value.endswith('"'):
239 value = value[1:-1]
240 return "__iexact", value
242 else:
243 if value.startswith("*") and value.endswith("*"):
244 value = value[1:-1]
245 return "__icontains", value