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
« 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
9from polymorphic.compat import with_metaclass
11from .base import PolymorphicModelBase
12from .managers import PolymorphicManager
13from .query_translate import translate_polymorphic_Q_object
15###################################################################################
16# PolymorphicModel
19class PolymorphicTypeUndefined(LookupError):
20 pass
23class PolymorphicTypeInvalid(RuntimeError):
24 pass
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.
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 """
36 # for PolymorphicModelBase, so it can tell which models are polymorphic and which are not (duck typing)
37 polymorphic_model_marker = True
39 # for PolymorphicQuery, True => an overloaded __repr__ with nicer multi-line output is used by PolymorphicQuery
40 polymorphic_query_multiline_output = False
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 )
52 # some applications want to know the name of the fields that are added to its models
53 polymorphic_internal_model_fields = ["polymorphic_ctype"]
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()
59 class Meta:
60 abstract = True
61 base_manager_name = "objects"
63 @classmethod
64 def translate_polymorphic_Q_object(cls, q):
65 return translate_polymorphic_Q_object(cls, q)
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 )
81 pre_save_polymorphic.alters_data = True
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)
89 save.alters_data = True
91 def get_real_instance_class(self):
92 """
93 Return the actual model type of the object.
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 )
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 )
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 )
135 return model
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 )
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 )
157 def get_real_instance(self):
158 """
159 Upcast an object to it's actual type.
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.
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)
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).
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
187 These accessors allow Django (and everyone else) to travel up and down
188 the inheritance tree for the db object at hand.
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)
196 if self.__class__.polymorphic_super_sub_accessors_replaced:
197 return
198 self.__class__.polymorphic_super_sub_accessors_replaced = True
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
206 return accessor_function
208 subclasses_and_superclasses_accessors = self._get_inheritance_relation_fields_and_models()
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 )
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)"""
227 def add_model(model, field_name, result):
228 result[field_name] = model
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)
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)
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
266 add_model_if_regular(sub_cls, to_subclass_fieldname, result)
268 result = {}
269 add_all_super_models(self.__class__, result)
270 add_all_sub_models(self.__class__, result)
271 return result