Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/polymorphic/models.py: 81%

97 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1""" 

2Seamless Polymorphic Inheritance for Django Models 

3""" 

4from django.contrib.contenttypes.models import ContentType 

5from django.db import models 

6from django.db.models.fields.related import ForwardManyToOneDescriptor, ReverseOneToOneDescriptor 

7from django.db.utils import DEFAULT_DB_ALIAS 

8 

9from polymorphic.compat import with_metaclass 

10 

11from .base import PolymorphicModelBase 

12from .managers import PolymorphicManager 

13from .query_translate import translate_polymorphic_Q_object 

14 

15################################################################################### 

16# PolymorphicModel 

17 

18 

19class PolymorphicTypeUndefined(LookupError): 

20 pass 

21 

22 

23class PolymorphicTypeInvalid(RuntimeError): 

24 pass 

25 

26 

27class PolymorphicModel(with_metaclass(PolymorphicModelBase, models.Model)): 

28 """ 

29 Abstract base class that provides polymorphic behaviour 

30 for any model directly or indirectly derived from it. 

31 

32 PolymorphicModel declares one field for internal use (:attr:`polymorphic_ctype`) 

33 and provides a polymorphic manager as the default manager (and as 'objects'). 

34 """ 

35 

36 # for PolymorphicModelBase, so it can tell which models are polymorphic and which are not (duck typing) 

37 polymorphic_model_marker = True 

38 

39 # for PolymorphicQuery, True => an overloaded __repr__ with nicer multi-line output is used by PolymorphicQuery 

40 polymorphic_query_multiline_output = False 

41 

42 # avoid ContentType related field accessor clash (an error emitted by model validation) 

43 #: The model field that stores the :class:`~django.contrib.contenttypes.models.ContentType` reference to the actual class. 

44 polymorphic_ctype = models.ForeignKey( 

45 ContentType, 

46 null=True, 

47 editable=False, 

48 on_delete=models.CASCADE, 

49 related_name="polymorphic_%(app_label)s.%(class)s_set+", 

50 ) 

51 

52 # some applications want to know the name of the fields that are added to its models 

53 polymorphic_internal_model_fields = ["polymorphic_ctype"] 

54 

55 # Note that Django 1.5 removes these managers because the model is abstract. 

56 # They are pretended to be there by the metaclass in PolymorphicModelBase.get_inherited_managers() 

57 objects = PolymorphicManager() 

58 

59 class Meta: 

60 abstract = True 

61 base_manager_name = "objects" 

62 

63 @classmethod 

64 def translate_polymorphic_Q_object(cls, q): 

65 return translate_polymorphic_Q_object(cls, q) 

66 

67 def pre_save_polymorphic(self, using=DEFAULT_DB_ALIAS): 

68 """ 

69 Make sure the ``polymorphic_ctype`` value is correctly set on this model. 

70 """ 

71 # This function may be called manually in special use-cases. When the object 

72 # is saved for the first time, we store its real class in polymorphic_ctype. 

73 # When the object later is retrieved by PolymorphicQuerySet, it uses this 

74 # field to figure out the real class of this object 

75 # (used by PolymorphicQuerySet._get_real_instances) 

76 if not self.polymorphic_ctype_id: 

77 self.polymorphic_ctype = ContentType.objects.db_manager(using).get_for_model( 

78 self, for_concrete_model=False 

79 ) 

80 

81 pre_save_polymorphic.alters_data = True 

82 

83 def save(self, *args, **kwargs): 

84 """Calls :meth:`pre_save_polymorphic` and saves the model.""" 

85 using = kwargs.get("using", self._state.db or DEFAULT_DB_ALIAS) 

86 self.pre_save_polymorphic(using=using) 

87 return super().save(*args, **kwargs) 

88 

89 save.alters_data = True 

90 

91 def get_real_instance_class(self): 

92 """ 

93 Return the actual model type of the object. 

94 

95 If a non-polymorphic manager (like base_objects) has been used to 

96 retrieve objects, then the real class/type of these objects may be 

97 determined using this method. 

98 """ 

99 if self.polymorphic_ctype_id is None: 99 ↛ 100line 99 didn't jump to line 100, because the condition on line 99 was never true

100 raise PolymorphicTypeUndefined( 

101 ( 

102 "The model {}#{} does not have a `polymorphic_ctype_id` value defined.\n" 

103 "If you created models outside polymorphic, e.g. through an import or migration, " 

104 "make sure the `polymorphic_ctype_id` field points to the ContentType ID of the model subclass." 

105 ).format(self.__class__.__name__, self.pk) 

106 ) 

107 

108 # the following line would be the easiest way to do this, but it produces sql queries 

109 # return self.polymorphic_ctype.model_class() 

110 # so we use the following version, which uses the ContentType manager cache. 

111 # Note that model_class() can return None for stale content types; 

112 # when the content type record still exists but no longer refers to an existing model. 

113 model = ( 

114 ContentType.objects.db_manager(self._state.db) 

115 .get_for_id(self.polymorphic_ctype_id) 

116 .model_class() 

117 ) 

118 

119 # Protect against bad imports (dumpdata without --natural) or other 

120 # issues missing with the ContentType models. 

121 if ( 121 ↛ 129line 121 didn't jump to line 129

122 model is not None 

123 and not issubclass(model, self.__class__) 

124 and ( 

125 self.__class__._meta.proxy_for_model is None 

126 or not issubclass(model, self.__class__._meta.proxy_for_model) 

127 ) 

128 ): 

129 raise PolymorphicTypeInvalid( 

130 "ContentType {} for {} #{} does not point to a subclass!".format( 

131 self.polymorphic_ctype_id, model, self.pk 

132 ) 

133 ) 

134 

135 return model 

136 

137 def get_real_concrete_instance_class_id(self): 

138 model_class = self.get_real_instance_class() 

139 if model_class is None: 139 ↛ 140line 139 didn't jump to line 140, because the condition on line 139 was never true

140 return None 

141 return ( 

142 ContentType.objects.db_manager(self._state.db) 

143 .get_for_model(model_class, for_concrete_model=True) 

144 .pk 

145 ) 

146 

147 def get_real_concrete_instance_class(self): 

148 model_class = self.get_real_instance_class() 

149 if model_class is None: 

150 return None 

151 return ( 

152 ContentType.objects.db_manager(self._state.db) 

153 .get_for_model(model_class, for_concrete_model=True) 

154 .model_class() 

155 ) 

156 

157 def get_real_instance(self): 

158 """ 

159 Upcast an object to it's actual type. 

160 

161 If a non-polymorphic manager (like base_objects) has been used to 

162 retrieve objects, then the complete object with it's real class/type 

163 and all fields may be retrieved with this method. 

164 

165 .. note:: 

166 Each method call executes one db query (if necessary). 

167 Use the :meth:`~polymorphic.managers.PolymorphicQuerySet.get_real_instances` 

168 to upcast a complete list in a single efficient query. 

169 """ 

170 real_model = self.get_real_instance_class() 

171 if real_model == self.__class__: 

172 return self 

173 return real_model.objects.db_manager(self._state.db).get(pk=self.pk) 

174 

175 def __init__(self, *args, **kwargs): 

176 """Replace Django's inheritance accessor member functions for our model 

177 (self.__class__) with our own versions. 

178 We monkey patch them until a patch can be added to Django 

179 (which would probably be very small and make all of this obsolete). 

180 

181 If we have inheritance of the form ModelA -> ModelB ->ModelC then 

182 Django creates accessors like this: 

183 - ModelA: modelb 

184 - ModelB: modela_ptr, modelb, modelc 

185 - ModelC: modela_ptr, modelb, modelb_ptr, modelc 

186 

187 These accessors allow Django (and everyone else) to travel up and down 

188 the inheritance tree for the db object at hand. 

189 

190 The original Django accessors use our polymorphic manager. 

191 But they should not. So we replace them with our own accessors that use 

192 our appropriate base_objects manager. 

193 """ 

194 super().__init__(*args, **kwargs) 

195 

196 if self.__class__.polymorphic_super_sub_accessors_replaced: 

197 return 

198 self.__class__.polymorphic_super_sub_accessors_replaced = True 

199 

200 def create_accessor_function_for_model(model, accessor_name): 

201 def accessor_function(self): 

202 objects = getattr(model, "_base_objects", model.objects) 

203 attr = objects.get(pk=self.pk) 

204 return attr 

205 

206 return accessor_function 

207 

208 subclasses_and_superclasses_accessors = self._get_inheritance_relation_fields_and_models() 

209 

210 for name, model in subclasses_and_superclasses_accessors.items(): 

211 # Here be dragons. 

212 orig_accessor = getattr(self.__class__, name, None) 

213 if issubclass( 213 ↛ 210line 213 didn't jump to line 210, because the condition on line 213 was never false

214 type(orig_accessor), 

215 (ReverseOneToOneDescriptor, ForwardManyToOneDescriptor), 

216 ): 

217 setattr( 

218 self.__class__, 

219 name, 

220 property(create_accessor_function_for_model(model, name)), 

221 ) 

222 

223 def _get_inheritance_relation_fields_and_models(self): 

224 """helper function for __init__: 

225 determine names of all Django inheritance accessor member functions for type(self)""" 

226 

227 def add_model(model, field_name, result): 

228 result[field_name] = model 

229 

230 def add_model_if_regular(model, field_name, result): 

231 if ( 231 ↛ exitline 231 didn't jump to the function exit

232 issubclass(model, models.Model) 

233 and model != models.Model 

234 and model != self.__class__ 

235 and model != PolymorphicModel 

236 ): 

237 add_model(model, field_name, result) 

238 

239 def add_all_super_models(model, result): 

240 for super_cls, field_to_super in model._meta.parents.items(): 

241 if field_to_super is not None: 

242 # if not a link to a proxy model, the field on model can have 

243 # a different name to super_cls._meta.module_name, when the field 

244 # is created manually using 'parent_link' 

245 field_name = field_to_super.name 

246 add_model_if_regular(super_cls, field_name, result) 

247 add_all_super_models(super_cls, result) 

248 

249 def add_all_sub_models(super_cls, result): 

250 # go through all subclasses of model 

251 for sub_cls in super_cls.__subclasses__(): 

252 # super_cls may not be in sub_cls._meta.parents if super_cls is a proxy model 

253 if super_cls in sub_cls._meta.parents: 

254 # get the field that links sub_cls to super_cls 

255 field_to_super = sub_cls._meta.parents[super_cls] 

256 # if filed_to_super is not a link to a proxy model 

257 if field_to_super is not None: 

258 super_to_sub_related_field = field_to_super.remote_field 

259 if super_to_sub_related_field.related_name is None: 259 ↛ 264line 259 didn't jump to line 264, because the condition on line 259 was never false

260 # if related name is None the related field is the name of the subclass 

261 to_subclass_fieldname = sub_cls.__name__.lower() 

262 else: 

263 # otherwise use the given related name 

264 to_subclass_fieldname = super_to_sub_related_field.related_name 

265 

266 add_model_if_regular(sub_cls, to_subclass_fieldname, result) 

267 

268 result = {} 

269 add_all_super_models(self.__class__, result) 

270 add_all_sub_models(self.__class__, result) 

271 return result