Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. """
  2. QuerySet for PolymorphicModel
  3. """
  4. import copy
  5. from collections import defaultdict
  6. from django import get_version as get_django_version
  7. from django.contrib.contenttypes.models import ContentType
  8. from django.core.exceptions import FieldDoesNotExist
  9. from django.db.models import FilteredRelation
  10. from django.db.models.query import ModelIterable, Q, QuerySet
  11. from .query_translate import (
  12. translate_polymorphic_field_path,
  13. translate_polymorphic_filter_definitions_in_args,
  14. translate_polymorphic_filter_definitions_in_kwargs,
  15. translate_polymorphic_Q_object,
  16. )
  17. # chunk-size: maximum number of objects requested per db-request
  18. # by the polymorphic queryset.iterator() implementation
  19. Polymorphic_QuerySet_objects_per_request = 100
  20. class PolymorphicModelIterable(ModelIterable):
  21. """
  22. ModelIterable for PolymorphicModel
  23. Yields real instances if qs.polymorphic_disabled is False,
  24. otherwise acts like a regular ModelIterable.
  25. """
  26. def __iter__(self):
  27. base_iter = super().__iter__()
  28. if self.queryset.polymorphic_disabled:
  29. return base_iter
  30. return self._polymorphic_iterator(base_iter)
  31. def _polymorphic_iterator(self, base_iter):
  32. """
  33. Here we do the same as::
  34. real_results = queryset._get_real_instances(list(base_iter))
  35. for o in real_results: yield o
  36. but it requests the objects in chunks from the database,
  37. with Polymorphic_QuerySet_objects_per_request per chunk
  38. """
  39. while True:
  40. base_result_objects = []
  41. reached_end = False
  42. # Make sure the base iterator is read in chunks instead of
  43. # reading it completely, in case our caller read only a few objects.
  44. for i in range(Polymorphic_QuerySet_objects_per_request):
  45. try:
  46. o = next(base_iter)
  47. base_result_objects.append(o)
  48. except StopIteration:
  49. reached_end = True
  50. break
  51. real_results = self.queryset._get_real_instances(base_result_objects)
  52. for o in real_results:
  53. yield o
  54. if reached_end:
  55. return
  56. def transmogrify(cls, obj):
  57. """
  58. Upcast a class to a different type without asking questions.
  59. """
  60. if "__init__" not in obj.__dict__:
  61. # Just assign __class__ to a different value.
  62. new = obj
  63. new.__class__ = cls
  64. else:
  65. # Run constructor, reassign values
  66. new = cls()
  67. for k, v in obj.__dict__.items():
  68. new.__dict__[k] = v
  69. return new
  70. ###################################################################################
  71. # PolymorphicQuerySet
  72. class PolymorphicQuerySet(QuerySet):
  73. """
  74. QuerySet for PolymorphicModel
  75. Contains the core functionality for PolymorphicModel
  76. Usually not explicitly needed, except if a custom queryset class
  77. is to be used.
  78. """
  79. def __init__(self, *args, **kwargs):
  80. super().__init__(*args, **kwargs)
  81. self._iterable_class = PolymorphicModelIterable
  82. self.polymorphic_disabled = False
  83. # A parallel structure to django.db.models.query.Query.deferred_loading,
  84. # which we maintain with the untranslated field names passed to
  85. # .defer() and .only() in order to be able to retranslate them when
  86. # retrieving the real instance (so that the deferred fields apply
  87. # to that queryset as well).
  88. self.polymorphic_deferred_loading = (set(), True)
  89. def _clone(self, *args, **kwargs):
  90. # Django's _clone only copies its own variables, so we need to copy ours here
  91. new = super()._clone(*args, **kwargs)
  92. new.polymorphic_disabled = self.polymorphic_disabled
  93. new.polymorphic_deferred_loading = (
  94. copy.copy(self.polymorphic_deferred_loading[0]),
  95. self.polymorphic_deferred_loading[1],
  96. )
  97. return new
  98. def as_manager(cls):
  99. from .managers import PolymorphicManager
  100. manager = PolymorphicManager.from_queryset(cls)()
  101. manager._built_with_as_manager = True
  102. return manager
  103. as_manager.queryset_only = True
  104. as_manager = classmethod(as_manager)
  105. def bulk_create(self, objs, batch_size=None, ignore_conflicts=False):
  106. objs = list(objs)
  107. for obj in objs:
  108. obj.pre_save_polymorphic()
  109. return super().bulk_create(objs, batch_size, ignore_conflicts=ignore_conflicts)
  110. def non_polymorphic(self):
  111. """switch off polymorphic behaviour for this query.
  112. When the queryset is evaluated, only objects of the type of the
  113. base class used for this query are returned."""
  114. qs = self._clone()
  115. qs.polymorphic_disabled = True
  116. if issubclass(qs._iterable_class, PolymorphicModelIterable):
  117. qs._iterable_class = ModelIterable
  118. return qs
  119. def instance_of(self, *args):
  120. """Filter the queryset to only include the classes in args (and their subclasses)."""
  121. # Implementation in _translate_polymorphic_filter_defnition.
  122. return self.filter(instance_of=args)
  123. def not_instance_of(self, *args):
  124. """Filter the queryset to exclude the classes in args (and their subclasses)."""
  125. # Implementation in _translate_polymorphic_filter_defnition."""
  126. return self.filter(not_instance_of=args)
  127. # Makes _filter_or_exclude compatible with the change in signature introduced in django at 9c9a3fe
  128. if get_django_version() >= "3.2":
  129. def _filter_or_exclude(self, negate, args, kwargs):
  130. # We override this internal Django function as it is used for all filter member functions.
  131. q_objects = translate_polymorphic_filter_definitions_in_args(
  132. queryset_model=self.model, args=args, using=self.db
  133. )
  134. # filter_field='data'
  135. additional_args = translate_polymorphic_filter_definitions_in_kwargs(
  136. queryset_model=self.model, kwargs=kwargs, using=self.db
  137. )
  138. args = list(q_objects) + additional_args
  139. return super()._filter_or_exclude(negate=negate, args=args, kwargs=kwargs)
  140. else:
  141. def _filter_or_exclude(self, negate, *args, **kwargs):
  142. # We override this internal Django function as it is used for all filter member functions.
  143. q_objects = translate_polymorphic_filter_definitions_in_args(
  144. self.model, args, using=self.db
  145. )
  146. # filter_field='data'
  147. additional_args = translate_polymorphic_filter_definitions_in_kwargs(
  148. self.model, kwargs, using=self.db
  149. )
  150. return super()._filter_or_exclude(
  151. negate, *(list(q_objects) + additional_args), **kwargs
  152. )
  153. def order_by(self, *field_names):
  154. """translate the field paths in the args, then call vanilla order_by."""
  155. field_names = [
  156. translate_polymorphic_field_path(self.model, a)
  157. if isinstance(a, str)
  158. else a # allow expressions to pass unchanged
  159. for a in field_names
  160. ]
  161. return super().order_by(*field_names)
  162. def defer(self, *fields):
  163. """
  164. Translate the field paths in the args, then call vanilla defer.
  165. Also retain a copy of the original fields passed, which we'll need
  166. when we're retrieving the real instance (since we'll need to translate
  167. them again, as the model will have changed).
  168. """
  169. new_fields = [translate_polymorphic_field_path(self.model, a) for a in fields]
  170. clone = super().defer(*new_fields)
  171. clone._polymorphic_add_deferred_loading(fields)
  172. return clone
  173. def only(self, *fields):
  174. """
  175. Translate the field paths in the args, then call vanilla only.
  176. Also retain a copy of the original fields passed, which we'll need
  177. when we're retrieving the real instance (since we'll need to translate
  178. them again, as the model will have changed).
  179. """
  180. new_fields = [translate_polymorphic_field_path(self.model, a) for a in fields]
  181. clone = super().only(*new_fields)
  182. clone._polymorphic_add_immediate_loading(fields)
  183. return clone
  184. def _polymorphic_add_deferred_loading(self, field_names):
  185. """
  186. Follows the logic of django.db.models.query.Query.add_deferred_loading(),
  187. but for the non-translated field names that were passed to self.defer().
  188. """
  189. existing, defer = self.polymorphic_deferred_loading
  190. if defer:
  191. # Add to existing deferred names.
  192. self.polymorphic_deferred_loading = existing.union(field_names), True
  193. else:
  194. # Remove names from the set of any existing "immediate load" names.
  195. self.polymorphic_deferred_loading = existing.difference(field_names), False
  196. def _polymorphic_add_immediate_loading(self, field_names):
  197. """
  198. Follows the logic of django.db.models.query.Query.add_immediate_loading(),
  199. but for the non-translated field names that were passed to self.only()
  200. """
  201. existing, defer = self.polymorphic_deferred_loading
  202. field_names = set(field_names)
  203. if "pk" in field_names:
  204. field_names.remove("pk")
  205. field_names.add(self.model._meta.pk.name)
  206. if defer:
  207. # Remove any existing deferred names from the current set before
  208. # setting the new names.
  209. self.polymorphic_deferred_loading = field_names.difference(existing), False
  210. else:
  211. # Replace any existing "immediate load" field names.
  212. self.polymorphic_deferred_loading = field_names, False
  213. def _process_aggregate_args(self, args, kwargs):
  214. """for aggregate and annotate kwargs: allow ModelX___field syntax for kwargs, forbid it for args.
  215. Modifies kwargs if needed (these are Aggregate objects, we translate the lookup member variable)"""
  216. ___lookup_assert_msg = "PolymorphicModel: annotate()/aggregate(): ___ model lookup supported for keyword arguments only"
  217. def patch_lookup(a):
  218. # The field on which the aggregate operates is
  219. # stored inside a complex query expression.
  220. if isinstance(a, Q):
  221. translate_polymorphic_Q_object(self.model, a)
  222. elif isinstance(a, FilteredRelation):
  223. patch_lookup(a.condition)
  224. elif hasattr(a, "get_source_expressions"):
  225. for source_expression in a.get_source_expressions():
  226. if source_expression is not None:
  227. patch_lookup(source_expression)
  228. else:
  229. a.name = translate_polymorphic_field_path(self.model, a.name)
  230. def test___lookup(a):
  231. """*args might be complex expressions too in django 1.8 so
  232. the testing for a '___' is rather complex on this one"""
  233. if isinstance(a, Q):
  234. def tree_node_test___lookup(my_model, node):
  235. "process all children of this Q node"
  236. for i in range(len(node.children)):
  237. child = node.children[i]
  238. if type(child) == tuple:
  239. # this Q object child is a tuple => a kwarg like Q( instance_of=ModelB )
  240. assert "___" not in child[0], ___lookup_assert_msg
  241. else:
  242. # this Q object child is another Q object, recursively process this as well
  243. tree_node_test___lookup(my_model, child)
  244. tree_node_test___lookup(self.model, a)
  245. elif hasattr(a, "get_source_expressions"):
  246. for source_expression in a.get_source_expressions():
  247. test___lookup(source_expression)
  248. else:
  249. assert "___" not in a.name, ___lookup_assert_msg
  250. for a in args:
  251. test___lookup(a)
  252. for a in kwargs.values():
  253. patch_lookup(a)
  254. def annotate(self, *args, **kwargs):
  255. """translate the polymorphic field paths in the kwargs, then call vanilla annotate.
  256. _get_real_instances will do the rest of the job after executing the query."""
  257. self._process_aggregate_args(args, kwargs)
  258. return super().annotate(*args, **kwargs)
  259. def aggregate(self, *args, **kwargs):
  260. """translate the polymorphic field paths in the kwargs, then call vanilla aggregate.
  261. We need no polymorphic object retrieval for aggregate => switch it off."""
  262. self._process_aggregate_args(args, kwargs)
  263. qs = self.non_polymorphic()
  264. return super(PolymorphicQuerySet, qs).aggregate(*args, **kwargs)
  265. # Starting with Django 1.9, the copy returned by 'qs.values(...)' has the
  266. # same class as 'qs', so our polymorphic modifications would apply.
  267. # We want to leave values queries untouched, so we set 'polymorphic_disabled'.
  268. def _values(self, *args, **kwargs):
  269. clone = super()._values(*args, **kwargs)
  270. clone.polymorphic_disabled = True
  271. return clone
  272. # Since django_polymorphic 'V1.0 beta2', extra() always returns polymorphic results.
  273. # The resulting objects are required to have a unique primary key within the result set
  274. # (otherwise an error is thrown).
  275. # The "polymorphic" keyword argument is not supported anymore.
  276. # def extra(self, *args, **kwargs):
  277. def _get_real_instances(self, base_result_objects):
  278. """
  279. Polymorphic object loader
  280. Does the same as:
  281. return [ o.get_real_instance() for o in base_result_objects ]
  282. but more efficiently.
  283. The list base_result_objects contains the objects from the executed
  284. base class query. The class of all of them is self.model (our base model).
  285. Some, many or all of these objects were not created and stored as
  286. class self.model, but as a class derived from self.model. We want to re-fetch
  287. these objects from the db as their original class so we can return them
  288. just as they were created/saved.
  289. We identify these objects by looking at o.polymorphic_ctype, which specifies
  290. the real class of these objects (the class at the time they were saved).
  291. First, we sort the result objects in base_result_objects for their
  292. subclass (from o.polymorphic_ctype), and then we execute one db query per
  293. subclass of objects. Here, we handle any annotations from annotate().
  294. Finally we re-sort the resulting objects into the correct order and
  295. return them as a list.
  296. """
  297. resultlist = [] # polymorphic list of result-objects
  298. # dict contains one entry per unique model type occurring in result,
  299. # in the format idlist_per_model[modelclass]=[list-of-object-ids]
  300. idlist_per_model = defaultdict(list)
  301. indexlist_per_model = defaultdict(list)
  302. # django's automatic ".pk" field does not always work correctly for
  303. # custom fields in derived objects (unclear yet who to put the blame on).
  304. # We get different type(o.pk) in this case.
  305. # We work around this by using the real name of the field directly
  306. # for accessing the primary key of the the derived objects.
  307. # We might assume that self.model._meta.pk.name gives us the name of the primary key field,
  308. # but it doesn't. Therefore we use polymorphic_primary_key_name, which we set up in base.py.
  309. pk_name = self.model.polymorphic_primary_key_name
  310. # - sort base_result_object ids into idlist_per_model lists, depending on their real class;
  311. # - store objects that already have the correct class into "results"
  312. content_type_manager = ContentType.objects.db_manager(self.db)
  313. self_model_class_id = content_type_manager.get_for_model(
  314. self.model, for_concrete_model=False
  315. ).pk
  316. self_concrete_model_class_id = content_type_manager.get_for_model(
  317. self.model, for_concrete_model=True
  318. ).pk
  319. for i, base_object in enumerate(base_result_objects):
  320. if base_object.polymorphic_ctype_id == self_model_class_id:
  321. # Real class is exactly the same as base class, go straight to results
  322. resultlist.append(base_object)
  323. else:
  324. real_concrete_class = base_object.get_real_instance_class()
  325. real_concrete_class_id = base_object.get_real_concrete_instance_class_id()
  326. if real_concrete_class_id is None:
  327. # Dealing with a stale content type
  328. continue
  329. elif real_concrete_class_id == self_concrete_model_class_id:
  330. # Real and base classes share the same concrete ancestor,
  331. # upcast it and put it in the results
  332. resultlist.append(transmogrify(real_concrete_class, base_object))
  333. else:
  334. # This model has a concrete derived class, track it for bulk retrieval.
  335. real_concrete_class = content_type_manager.get_for_id(
  336. real_concrete_class_id
  337. ).model_class()
  338. idlist_per_model[real_concrete_class].append(getattr(base_object, pk_name))
  339. indexlist_per_model[real_concrete_class].append((i, len(resultlist)))
  340. resultlist.append(None)
  341. # For each model in "idlist_per_model" request its objects (the real model)
  342. # from the db and store them in results[].
  343. # Then we copy the annotate fields from the base objects to the real objects.
  344. # Then we copy the extra() select fields from the base objects to the real objects.
  345. # TODO: defer(), only(): support for these would be around here
  346. for real_concrete_class, idlist in idlist_per_model.items():
  347. indices = indexlist_per_model[real_concrete_class]
  348. real_objects = real_concrete_class._base_objects.db_manager(self.db).filter(
  349. **{("%s__in" % pk_name): idlist}
  350. )
  351. # copy select related configuration to new qs
  352. real_objects.query.select_related = self.query.select_related
  353. # Copy deferred fields configuration to the new queryset
  354. deferred_loading_fields = []
  355. existing_fields = self.polymorphic_deferred_loading[0]
  356. for field in existing_fields:
  357. try:
  358. translated_field_name = translate_polymorphic_field_path(
  359. real_concrete_class, field
  360. )
  361. except AssertionError:
  362. if "___" in field:
  363. # The originally passed argument to .defer() or .only()
  364. # was in the form Model2B___field2, where Model2B is
  365. # now a superclass of real_concrete_class. Thus it's
  366. # sufficient to just use the field name.
  367. translated_field_name = field.rpartition("___")[-1]
  368. # Check if the field does exist.
  369. # Ignore deferred fields that don't exist in this subclass type.
  370. try:
  371. real_concrete_class._meta.get_field(translated_field_name)
  372. except FieldDoesNotExist:
  373. continue
  374. else:
  375. raise
  376. deferred_loading_fields.append(translated_field_name)
  377. real_objects.query.deferred_loading = (
  378. set(deferred_loading_fields),
  379. self.query.deferred_loading[1],
  380. )
  381. real_objects_dict = {
  382. getattr(real_object, pk_name): real_object for real_object in real_objects
  383. }
  384. for i, j in indices:
  385. base_object = base_result_objects[i]
  386. o_pk = getattr(base_object, pk_name)
  387. real_object = real_objects_dict.get(o_pk)
  388. if real_object is None:
  389. continue
  390. # need shallow copy to avoid duplication in caches (see PR #353)
  391. real_object = copy.copy(real_object)
  392. real_class = real_object.get_real_instance_class()
  393. # If the real class is a proxy, upcast it
  394. if real_class != real_concrete_class:
  395. real_object = transmogrify(real_class, real_object)
  396. if self.query.annotations:
  397. for anno_field_name in self.query.annotations.keys():
  398. attr = getattr(base_object, anno_field_name)
  399. setattr(real_object, anno_field_name, attr)
  400. if self.query.extra_select:
  401. for select_field_name in self.query.extra_select.keys():
  402. attr = getattr(base_object, select_field_name)
  403. setattr(real_object, select_field_name, attr)
  404. resultlist[j] = real_object
  405. resultlist = [i for i in resultlist if i]
  406. # set polymorphic_annotate_names in all objects (currently just used for debugging/printing)
  407. if self.query.annotations:
  408. # get annotate field list
  409. annotate_names = list(self.query.annotations.keys())
  410. for real_object in resultlist:
  411. real_object.polymorphic_annotate_names = annotate_names
  412. # set polymorphic_extra_select_names in all objects (currently just used for debugging/printing)
  413. if self.query.extra_select:
  414. # get extra select field list
  415. extra_select_names = list(self.query.extra_select.keys())
  416. for real_object in resultlist:
  417. real_object.polymorphic_extra_select_names = extra_select_names
  418. return resultlist
  419. def __repr__(self, *args, **kwargs):
  420. if self.model.polymorphic_query_multiline_output:
  421. result = [repr(o) for o in self.all()]
  422. return "[ " + ",\n ".join(result) + " ]"
  423. else:
  424. return super().__repr__(*args, **kwargs)
  425. class _p_list_class(list):
  426. def __repr__(self, *args, **kwargs):
  427. result = [repr(o) for o in self]
  428. return "[ " + ",\n ".join(result) + " ]"
  429. def get_real_instances(self, base_result_objects=None):
  430. """
  431. Cast a list of objects to their actual classes.
  432. This does roughly the same as::
  433. return [ o.get_real_instance() for o in base_result_objects ]
  434. but more efficiently.
  435. :rtype: PolymorphicQuerySet
  436. """
  437. "same as _get_real_instances, but make sure that __repr__ for ShowField... creates correct output"
  438. if base_result_objects is None:
  439. base_result_objects = self
  440. olist = self._get_real_instances(base_result_objects)
  441. if not self.model.polymorphic_query_multiline_output:
  442. return olist
  443. clist = PolymorphicQuerySet._p_list_class(olist)
  444. return clist