Coverage for apis_core/relations/models.py: 64%
66 statements
« prev ^ index » next coverage.py v7.6.4, created at 2024-11-22 07:51 +0000
« prev ^ index » next coverage.py v7.6.4, created at 2024-11-22 07:51 +0000
1import functools
3from django.contrib.contenttypes.fields import GenericForeignKey
4from django.contrib.contenttypes.models import ContentType
5from django.core.exceptions import ValidationError
6from django.db import models
7from django.db.models.base import ModelBase
8from model_utils.managers import InheritanceManager
10from apis_core.generic.abc import GenericModel
13# This ModelBase is simply there to check if the needed attributes
14# are set in the Relation child classes.
15class RelationModelBase(ModelBase):
16 def __new__(metacls, name, bases, attrs):
17 if name == "Relation":
18 return super().__new__(metacls, name, bases, attrs)
19 else:
20 new_class = super().__new__(metacls, name, bases, attrs)
21 if not hasattr(new_class, "subj_model"):
22 raise ValueError(
23 "%s inherits from Relation and must therefore specify subj_model"
24 % name
25 )
26 if not hasattr(new_class, "obj_model"):
27 raise ValueError(
28 "%s inherits from Relation and must therefore specify obj_model"
29 % name
30 )
32 return new_class
35@functools.cache
36def get_by_natural_key(natural_key: str):
37 app_label, name = natural_key.lower().split(".")
38 return ContentType.objects.get_by_natural_key(app_label, name).model_class()
41class Relation(models.Model, GenericModel, metaclass=RelationModelBase):
42 subj_content_type = models.ForeignKey(
43 ContentType, on_delete=models.CASCADE, related_name="relation_subj_set"
44 )
45 subj_object_id = models.PositiveIntegerField()
46 subj = GenericForeignKey("subj_content_type", "subj_object_id")
47 obj_content_type = models.ForeignKey(
48 ContentType, on_delete=models.CASCADE, related_name="relation_obj_set"
49 )
50 obj_object_id = models.PositiveIntegerField()
51 obj = GenericForeignKey("obj_content_type", "obj_object_id")
53 objects = InheritanceManager()
55 def save(self, *args, **kwargs):
56 if self.subj_content_type:
57 if self.subj_content_type.model_class() not in self.subj_list():
58 raise ValidationError(
59 f"{self.subj} is not of any type in {self.subj_list()}"
60 )
61 if self.obj_content_type:
62 if self.obj_content_type.model_class() not in self.obj_list():
63 raise ValidationError(
64 f"{self.obj} is not of any type in {self.obj_list()}"
65 )
66 super().save(*args, **kwargs)
68 @property
69 def subj_to_obj_text(self) -> str:
70 if hasattr(self, "name"):
71 return f"{self.subj} {self.name()} {self.obj}"
72 return f"{self.subj} relation to {self.obj}"
74 @property
75 def obj_to_subj_text(self) -> str:
76 if hasattr(self, "reverse_name"):
77 return f"{self.obj} {self.reverse_name()} {self.subj}"
78 return f"{self.obj} relation to {self.subj}"
80 def __str__(self):
81 return self.subj_to_obj_text
83 @classmethod
84 def _get_models(cls, model):
85 models = model if isinstance(model, list) else [model]
86 return [
87 get_by_natural_key(model) if isinstance(model, str) else model
88 for model in models
89 ]
91 @classmethod
92 def subj_list(cls) -> list[models.Model]:
93 return cls._get_models(cls.subj_model)
95 @classmethod
96 def obj_list(cls) -> list[models.Model]:
97 return cls._get_models(cls.obj_model)
99 @classmethod
100 def name(cls) -> str:
101 return cls._meta.verbose_name
103 @classmethod
104 def reverse_name(cls) -> str:
105 return cls._meta.verbose_name + " reverse"