Coverage for apis_core/apis_entities/models.py: 44%
80 statements
« prev ^ index » next coverage.py v7.5.3, created at 2026-01-07 08:21 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2026-01-07 08:21 +0000
1import functools
2import logging
3from collections import defaultdict
5from django.apps import apps
6from django.conf import settings
7from django.contrib.contenttypes.models import ContentType
8from django.db.models import Case, CharField, F, Q, Value, When
9from django.db.models.base import ModelBase
10from django.db.models.functions import Concat
11from django.urls import NoReverseMatch, reverse
13from apis_core.apis_metainfo.models import RootObject
14from apis_core.relations.models import Relation
15from apis_core.utils.settings import apis_base_uri
17NEXT_PREV = getattr(settings, "APIS_NEXT_PREV", True)
19logger = logging.getLogger(__name__)
22class AbstractEntityModelBase(ModelBase):
23 def __new__(metacls, name, bases, attrs):
24 if name == "AbstractEntity":
25 return super().__new__(metacls, name, bases, attrs)
26 else:
27 new_class = super().__new__(metacls, name, bases, attrs)
28 if not new_class._meta.ordering:
29 logger.warning(
30 f"{name} inherits from AbstractEntity but does not specify 'ordering' in its Meta class. "
31 "Empty ordering could result in inconsitent results with pagination. "
32 "Set a ordering or inherit the Meta class from AbstractEntity.",
33 )
35 return new_class
38class AbstractEntity(RootObject, metaclass=AbstractEntityModelBase):
39 """
40 Abstract super class which encapsulates common logic between the
41 different entity kinds and provides various methods relating to either
42 all or one specific entity kind.
44 Most of the class methods are designed to be used in the subclass as they
45 are considering contexts which depend on the subclass entity type.
46 So they are to be understood in that dynamic context.
47 """
49 class Meta:
50 abstract = True
52 @functools.cached_property
53 def get_prev_id(self):
54 if NEXT_PREV:
55 prev_instance = (
56 type(self)
57 .objects.filter(id__lt=self.id)
58 .order_by("-id")
59 .only("id")
60 .first()
61 )
62 if prev_instance is not None:
63 return prev_instance.id
64 return False
66 @functools.cached_property
67 def get_next_id(self):
68 if NEXT_PREV:
69 next_instance = (
70 type(self)
71 .objects.filter(id__gt=self.id)
72 .order_by("id")
73 .only("id")
74 .first()
75 )
76 if next_instance is not None:
77 return next_instance.id
78 return False
80 def import_data(self, data):
81 super().import_data(data)
82 if "same_as" in data:
83 self._uris = data["same_as"]
84 self.save()
85 if "relations" in data:
86 self.create_relations_to_uris = data["relations"]
87 self.save()
89 def get_default_uri(self):
90 try:
91 route = reverse("GetEntityGenericRoot", kwargs={"pk": self.pk})
92 except NoReverseMatch:
93 route = reverse("apis_core:GetEntityGeneric", kwargs={"pk": self.pk})
94 base = apis_base_uri().strip("/")
95 return f"{base}{route}"
97 @classmethod
98 def get_facet_label(cls):
99 if "db_string" in cls._meta.get_fields():
100 return F("db_string")
101 return Concat(
102 Value(f"{cls._meta.verbose_name.title()} "),
103 F("pk"),
104 output_field=CharField(),
105 )
107 @classmethod
108 def get_facets(cls, queryset):
109 facets = defaultdict(dict)
110 if getattr(cls, "enable_facets", False):
111 my_content_type = ContentType.objects.get_for_model(queryset.model)
113 # we filter all the relations that are somehow connected to instances of this queryset
114 # we are only interested in the id and the type of the object the relations point to,
115 # so we store those in `target_id` and `target_content_type`
116 query_filter = Q(
117 subj_content_type=my_content_type, subj_object_id__in=queryset
118 ) | Q(
119 Q(obj_content_type=my_content_type, obj_object_id__in=queryset),
120 ~Q(subj_content_type=my_content_type),
121 )
122 rels = (
123 Relation.objects.to_content_type_with_targets(my_content_type).values(
124 "target_id", "target_content_type"
125 )
126 ).filter(query_filter)
128 # we use the ids to get a the labels of the targets
129 # and we store those in an id: label dict
130 target_ids = [rel["target_id"] for rel in rels]
131 entity_classes = list(
132 filter(lambda x: issubclass(x, AbstractEntity), apps.get_models())
133 )
135 # this should be moved to an EntityManager once the entites app is in apis_core
136 when_clauses_classes = [
137 When(
138 **{f"{cls.__name__.lower()}__isnull": False},
139 then=cls.get_facet_label(),
140 )
141 for cls in entity_classes
142 ]
143 identifiers = (
144 RootObject.objects_inheritance.filter(id__in=target_ids)
145 .select_subclasses()
146 .annotate(label=Case(*when_clauses_classes))
147 .values("pk", "label")
148 )
150 identifiers = {item["pk"]: item["label"] for item in identifiers}
152 for rel in rels:
153 _id = rel["target_id"]
154 content_type = rel["target_content_type"]
155 facetname = (
156 "relation_to_" + ContentType.objects.get_for_id(content_type).name
157 )
158 if _id in facets[facetname]:
159 facets[facetname][_id]["count"] += 1
160 else:
161 facets[facetname][_id] = {
162 "count": 1,
163 "name": identifiers.get(_id, "Unknown"),
164 }
165 return facets