Coverage for apis_core/relations/models.py: 66%
77 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-09-03 06:15 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2025-09-03 06:15 +0000
1import functools
2import logging
4from django.contrib.contenttypes.fields import GenericForeignKey
5from django.contrib.contenttypes.models import ContentType
6from django.core.exceptions import ValidationError
7from django.db import models
8from django.db.models.base import ModelBase
9from model_utils.managers import InheritanceManager
11from apis_core.generic.abc import GenericModel
13logger = logging.getLogger(__name__)
16# This ModelBase is simply there to check if the needed attributes
17# are set in the Relation child classes.
18class RelationModelBase(ModelBase):
19 def __new__(metacls, name, bases, attrs):
20 if name == "Relation":
21 return super().__new__(metacls, name, bases, attrs)
22 else:
23 new_class = super().__new__(metacls, name, bases, attrs)
24 if not (new_class._meta.abstract or new_class._meta.proxy):
25 if not hasattr(new_class, "subj_model"):
26 raise ValueError(
27 "%s inherits from Relation and must therefore specify subj_model"
28 % name
29 )
30 if not hasattr(new_class, "obj_model"):
31 raise ValueError(
32 "%s inherits from Relation and must therefore specify obj_model"
33 % name
34 )
36 # `subj_model` or `obj_model` being a list was supported in an earlier
37 # version of apis, but it is not anymore
38 if isinstance(getattr(new_class, "subj_model", None), list):
39 raise ValueError("%s.subj_model must not be a list" % name)
40 if isinstance(getattr(new_class, "obj_model", None), list):
41 raise ValueError("%s.obj_model mut not be a list" % name)
43 if not new_class._meta.ordering:
44 logger.warning(
45 f"{name} inherits from Relation but does not specify 'ordering' in its Meta class. "
46 "Empty ordering could result in inconsitent results with pagination. "
47 "Set a ordering or inherit the Meta class from Relation.",
48 )
50 return new_class
53@functools.cache
54def get_by_natural_key(natural_key: str):
55 app_label, name = natural_key.lower().split(".")
56 return ContentType.objects.get_by_natural_key(app_label, name).model_class()
59class Relation(GenericModel, models.Model, metaclass=RelationModelBase):
60 subj_content_type = models.ForeignKey(
61 ContentType, on_delete=models.CASCADE, related_name="relation_subj_set"
62 )
63 subj_object_id = models.PositiveIntegerField(null=True)
64 subj = GenericForeignKey("subj_content_type", "subj_object_id")
65 obj_content_type = models.ForeignKey(
66 ContentType, on_delete=models.CASCADE, related_name="relation_obj_set"
67 )
68 obj_object_id = models.PositiveIntegerField(null=True)
69 obj = GenericForeignKey("obj_content_type", "obj_object_id")
71 objects = InheritanceManager()
73 class Meta:
74 indexes = [
75 models.Index(
76 fields=["subj_content_type"], name="relations_r_subj_content_type"
77 ),
78 models.Index(fields=["subj_object_id"], name="relations_r_subj_object_id"),
79 models.Index(
80 fields=["obj_content_type"], name="relations_r_obj_content_type"
81 ),
82 models.Index(fields=["obj_object_id"], name="relations_r_obj_object_id"),
83 models.Index(
84 fields=["subj_content_type", "subj_object_id"],
85 name="relations_r_subj_c_t_o_i",
86 ),
87 models.Index(
88 fields=["obj_content_type", "obj_object_id"],
89 name="relations_r_obj_c_t_o_i",
90 ),
91 ]
93 def save(self, *args, **kwargs):
94 subj_model = getattr(self, "subj_model", None)
95 if subj_model and self.subj_content_type is subj_model:
96 raise ValidationError(f"{self.subj} is not of type {subj_model}")
97 obj_model = getattr(self, "obj_model", None)
98 if obj_model and self.obj_content_type is obj_model:
99 raise ValidationError(f"{self.obj} is not of type {obj_model}")
100 super().save(*args, **kwargs)
102 @property
103 def subj_to_obj_text(self) -> str:
104 if hasattr(self, "name"):
105 return f"{self.subj} {self.name()} {self.obj}"
106 return f"{self.subj} relation to {self.obj}"
108 @property
109 def obj_to_subj_text(self) -> str:
110 if hasattr(self, "reverse_name"):
111 return f"{self.obj} {self.reverse_name()} {self.subj}"
112 return f"{self.obj} relation to {self.subj}"
114 def __str__(self):
115 return self.subj_to_obj_text
117 @classmethod
118 def _get_models(cls, model):
119 models = model if isinstance(model, list) else [model]
120 return [
121 get_by_natural_key(model) if isinstance(model, str) else model
122 for model in models
123 ]
125 @classmethod
126 def subj_list(cls) -> list[models.Model]:
127 return cls._get_models(cls.subj_model)
129 @classmethod
130 def obj_list(cls) -> list[models.Model]:
131 return cls._get_models(cls.obj_model)
133 @classmethod
134 def name(cls) -> str:
135 return cls._meta.verbose_name
137 @classmethod
138 def reverse_name(cls) -> str:
139 return cls._meta.verbose_name + " reverse"