Coverage for apis_core/apis_entities/models.py: 69%
68 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-06-25 10:00 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2025-06-25 10:00 +0000
1import functools
2import logging
3import re
5from django.conf import settings
6from django.contrib.contenttypes.models import ContentType
7from django.db.models.base import ModelBase
8from django.db.models.signals import post_save
9from django.dispatch import receiver
10from django.urls import NoReverseMatch, reverse
12from apis_core.apis_metainfo.models import RootObject, Uri
13from apis_core.utils.settings import apis_base_uri
15NEXT_PREV = getattr(settings, "APIS_NEXT_PREV", True)
17logger = logging.getLogger(__name__)
20class AbstractEntityModelBase(ModelBase):
21 def __new__(metacls, name, bases, attrs):
22 if name == "AbstractEntity":
23 return super().__new__(metacls, name, bases, attrs)
24 else:
25 new_class = super().__new__(metacls, name, bases, attrs)
26 if not new_class._meta.ordering:
27 logger.warning(
28 f"{name} inherits from AbstractEntity but does not specify 'ordering' in its Meta class. "
29 "Empty ordering could result in inconsitent results with pagination. "
30 "Set a ordering or inherit the Meta class from AbstractEntity.",
31 )
33 return new_class
36class AbstractEntity(RootObject, metaclass=AbstractEntityModelBase):
37 """
38 Abstract super class which encapsulates common logic between the
39 different entity kinds and provides various methods relating to either
40 all or one specific entity kind.
42 Most of the class methods are designed to be used in the subclass as they
43 are considering contexts which depend on the subclass entity type.
44 So they are to be understood in that dynamic context.
45 """
47 class Meta:
48 abstract = True
50 @classmethod
51 def get_or_create_uri(cls, uri):
52 uri = str(uri)
53 try:
54 if re.match(r"^[0-9]*$", uri):
55 p = cls.objects.get(pk=uri)
56 else:
57 p = cls.objects.get(uri__uri=uri)
58 return p
59 except Exception as e:
60 print("Found no object corresponding to given uri." + e)
61 return False
63 # TODO
64 @classmethod
65 def get_entity_list_filter(cls):
66 return None
68 @functools.cached_property
69 def get_prev_id(self):
70 if NEXT_PREV:
71 prev_instance = (
72 type(self)
73 .objects.filter(id__lt=self.id)
74 .order_by("-id")
75 .only("id")
76 .first()
77 )
78 if prev_instance is not None:
79 return prev_instance.id
80 return False
82 @functools.cached_property
83 def get_next_id(self):
84 if NEXT_PREV:
85 next_instance = (
86 type(self)
87 .objects.filter(id__gt=self.id)
88 .order_by("id")
89 .only("id")
90 .first()
91 )
92 if next_instance is not None:
93 return next_instance.id
94 return False
97@receiver(post_save, dispatch_uid="create_default_uri")
98def create_default_uri(sender, instance, created, raw, using, update_fields, **kwargs):
99 # disable the handler during fixture loading
100 if raw:
101 return
102 create_default_uri = getattr(settings, "CREATE_DEFAULT_URI", True)
103 skip_default_uri = getattr(instance, "skip_default_uri", False)
104 if create_default_uri and not skip_default_uri:
105 if isinstance(instance, AbstractEntity) and created:
106 base = apis_base_uri().strip("/")
107 try:
108 route = reverse("GetEntityGenericRoot", kwargs={"pk": instance.pk})
109 except NoReverseMatch:
110 route = reverse(
111 "apis_core:GetEntityGeneric", kwargs={"pk": instance.pk}
112 )
113 uri = f"{base}{route}"
114 content_type = ContentType.objects.get_for_model(instance)
115 Uri.objects.create(
116 uri=uri,
117 content_type=content_type,
118 object_id=instance.id,
119 )