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

1import functools 

2import logging 

3 

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 

10 

11from apis_core.generic.abc import GenericModel 

12 

13logger = logging.getLogger(__name__) 

14 

15 

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 ) 

35 

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) 

42 

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 ) 

49 

50 return new_class 

51 

52 

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() 

57 

58 

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") 

70 

71 objects = InheritanceManager() 

72 

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 ] 

92 

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) 

101 

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}" 

107 

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}" 

113 

114 def __str__(self): 

115 return self.subj_to_obj_text 

116 

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 ] 

124 

125 @classmethod 

126 def subj_list(cls) -> list[models.Model]: 

127 return cls._get_models(cls.subj_model) 

128 

129 @classmethod 

130 def obj_list(cls) -> list[models.Model]: 

131 return cls._get_models(cls.obj_model) 

132 

133 @classmethod 

134 def name(cls) -> str: 

135 return cls._meta.verbose_name 

136 

137 @classmethod 

138 def reverse_name(cls) -> str: 

139 return cls._meta.verbose_name + " reverse"