plain.models 0.42.0__py3-none-any.whl → 0.43.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plain/models/CHANGELOG.md +15 -0
- plain/models/base.py +3 -2
- plain/models/fields/related.py +6 -28
- plain/models/fields/related_descriptors.py +79 -633
- plain/models/fields/related_managers.py +629 -0
- plain/models/fields/reverse_related.py +4 -7
- {plain_models-0.42.0.dist-info → plain_models-0.43.0.dist-info}/METADATA +1 -1
- {plain_models-0.42.0.dist-info → plain_models-0.43.0.dist-info}/RECORD +11 -10
- {plain_models-0.42.0.dist-info → plain_models-0.43.0.dist-info}/WHEEL +0 -0
- {plain_models-0.42.0.dist-info → plain_models-0.43.0.dist-info}/entry_points.txt +0 -0
- {plain_models-0.42.0.dist-info → plain_models-0.43.0.dist-info}/licenses/LICENSE +0 -0
@@ -48,20 +48,16 @@ reverse many-to-one relation.
|
|
48
48
|
|
49
49
|
from functools import cached_property
|
50
50
|
|
51
|
-
from plain.exceptions import FieldError
|
52
|
-
from plain.models import transaction
|
53
|
-
from plain.models.db import (
|
54
|
-
NotSupportedError,
|
55
|
-
db_connection,
|
56
|
-
)
|
57
|
-
from plain.models.expressions import Window
|
58
|
-
from plain.models.functions import RowNumber
|
59
|
-
from plain.models.lookups import GreaterThan, LessThanOrEqual
|
60
51
|
from plain.models.query import QuerySet
|
61
|
-
from plain.models.query_utils import DeferredAttribute
|
62
|
-
from plain.models.utils import resolve_callables
|
52
|
+
from plain.models.query_utils import DeferredAttribute
|
63
53
|
from plain.utils.functional import LazyObject
|
64
54
|
|
55
|
+
from .related_managers import (
|
56
|
+
ForwardManyToManyManager,
|
57
|
+
ReverseManyToManyManager,
|
58
|
+
ReverseManyToOneManager,
|
59
|
+
)
|
60
|
+
|
65
61
|
|
66
62
|
class ForeignKeyDeferredAttribute(DeferredAttribute):
|
67
63
|
def __set__(self, instance, value):
|
@@ -72,24 +68,6 @@ class ForeignKeyDeferredAttribute(DeferredAttribute):
|
|
72
68
|
instance.__dict__[self.field.attname] = value
|
73
69
|
|
74
70
|
|
75
|
-
def _filter_prefetch_queryset(queryset, field_name, instances):
|
76
|
-
predicate = Q(**{f"{field_name}__in": instances})
|
77
|
-
if queryset.query.is_sliced:
|
78
|
-
if not db_connection.features.supports_over_clause:
|
79
|
-
raise NotSupportedError(
|
80
|
-
"Prefetching from a limited queryset is only supported on backends "
|
81
|
-
"that support window functions."
|
82
|
-
)
|
83
|
-
low_mark, high_mark = queryset.query.low_mark, queryset.query.high_mark
|
84
|
-
order_by = [expr for expr, _ in queryset.query.get_compiler().get_order_by()]
|
85
|
-
window = Window(RowNumber(), partition_by=field_name, order_by=order_by)
|
86
|
-
predicate &= GreaterThan(window, low_mark)
|
87
|
-
if high_mark is not None:
|
88
|
-
predicate &= LessThanOrEqual(window, high_mark)
|
89
|
-
queryset.query.clear_limits()
|
90
|
-
return queryset.filter(predicate)
|
91
|
-
|
92
|
-
|
93
71
|
class ForwardManyToOneDescriptor:
|
94
72
|
"""
|
95
73
|
Accessor to the related object on the forward side of a many-to-one relation.
|
@@ -122,7 +100,7 @@ class ForwardManyToOneDescriptor:
|
|
122
100
|
def is_cached(self, instance):
|
123
101
|
return self.field.is_cached(instance)
|
124
102
|
|
125
|
-
def get_queryset(self):
|
103
|
+
def get_queryset(self) -> QuerySet:
|
126
104
|
qs = self.field.remote_field.model._meta.base_queryset
|
127
105
|
return qs.all()
|
128
106
|
|
@@ -283,57 +261,43 @@ class ForwardManyToOneDescriptor:
|
|
283
261
|
return getattr, (self.field.model, self.field.name)
|
284
262
|
|
285
263
|
|
286
|
-
class
|
264
|
+
class RelationDescriptorBase:
|
287
265
|
"""
|
288
|
-
|
289
|
-
many-to-one relation.
|
290
|
-
|
291
|
-
In the example::
|
266
|
+
Base class for relation descriptors that don't allow direct assignment.
|
292
267
|
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
``Parent.children`` is a ``ReverseManyToOneDescriptor`` instance.
|
297
|
-
|
298
|
-
Most of the implementation is delegated to a dynamically defined manager
|
299
|
-
class built by ``create_forward_many_to_many_manager()`` defined below.
|
268
|
+
This is used for descriptors that manage collections of related objects
|
269
|
+
(reverse FK and M2M relations). Forward FK relations don't inherit from
|
270
|
+
this because they allow direct assignment.
|
300
271
|
"""
|
301
272
|
|
302
273
|
def __init__(self, rel):
|
303
274
|
self.rel = rel
|
304
275
|
self.field = rel.field
|
305
276
|
|
306
|
-
@cached_property
|
307
|
-
def related_manager_cls(self):
|
308
|
-
related_model = self.rel.related_model
|
309
|
-
|
310
|
-
return create_reverse_many_to_one_manager(
|
311
|
-
related_model.query.__class__,
|
312
|
-
self.rel,
|
313
|
-
)
|
314
|
-
|
315
277
|
def __get__(self, instance, cls=None):
|
316
278
|
"""
|
317
|
-
Get the related
|
318
|
-
|
319
|
-
With the example above, when getting ``parent.children``:
|
279
|
+
Get the related manager when the descriptor is accessed.
|
320
280
|
|
321
|
-
|
322
|
-
- ``instance`` is the ``parent`` instance
|
323
|
-
- ``cls`` is the ``Parent`` class (unused)
|
281
|
+
Subclasses must implement get_related_manager().
|
324
282
|
"""
|
325
283
|
if instance is None:
|
326
284
|
return self
|
285
|
+
return self.get_related_manager(instance)
|
327
286
|
|
328
|
-
|
287
|
+
def get_related_manager(self, instance):
|
288
|
+
"""Return the appropriate manager for this relation."""
|
289
|
+
raise NotImplementedError(
|
290
|
+
f"{self.__class__.__name__} must implement get_related_manager()"
|
291
|
+
)
|
329
292
|
|
330
293
|
def _get_set_deprecation_msg_params(self):
|
331
|
-
|
332
|
-
|
333
|
-
self.
|
294
|
+
"""Return parameters for the error message when direct assignment is attempted."""
|
295
|
+
raise NotImplementedError(
|
296
|
+
f"{self.__class__.__name__} must implement _get_set_deprecation_msg_params()"
|
334
297
|
)
|
335
298
|
|
336
299
|
def __set__(self, instance, value):
|
300
|
+
"""Prevent direct assignment to the relation."""
|
337
301
|
raise TypeError(
|
338
302
|
"Direct assignment to the {} is prohibited. Use {}.set() instead.".format(
|
339
303
|
*self._get_set_deprecation_msg_params()
|
@@ -341,253 +305,45 @@ class ReverseManyToOneDescriptor:
|
|
341
305
|
)
|
342
306
|
|
343
307
|
|
344
|
-
|
308
|
+
class ReverseManyToOneDescriptor(RelationDescriptorBase):
|
345
309
|
"""
|
346
|
-
|
310
|
+
Accessor to the related objects manager on the reverse side of a
|
311
|
+
many-to-one relation.
|
312
|
+
|
313
|
+
In the example::
|
347
314
|
|
348
|
-
|
315
|
+
class Child(Model):
|
316
|
+
parent = ForeignKey(Parent, related_name='children')
|
317
|
+
|
318
|
+
``Parent.children`` is a ``ReverseManyToOneDescriptor`` instance.
|
319
|
+
|
320
|
+
Most of the implementation is delegated to the ReverseManyToOneManager class.
|
349
321
|
"""
|
350
322
|
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
self.
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
Access the QuerySet for this relationship.
|
364
|
-
|
365
|
-
Example:
|
366
|
-
parent.children.query.filter(active=True)
|
367
|
-
"""
|
368
|
-
return self.get_queryset()
|
369
|
-
|
370
|
-
def _check_fk_val(self):
|
371
|
-
for field in self.field.foreign_related_fields:
|
372
|
-
if getattr(self.instance, field.attname) is None:
|
373
|
-
raise ValueError(
|
374
|
-
f'"{self.instance!r}" needs to have a value for field '
|
375
|
-
f'"{field.attname}" before this relationship can be used.'
|
376
|
-
)
|
377
|
-
|
378
|
-
def _apply_rel_filters(self, queryset):
|
379
|
-
"""
|
380
|
-
Filter the queryset for the instance this manager is bound to.
|
381
|
-
"""
|
382
|
-
queryset._defer_next_filter = True
|
383
|
-
queryset = queryset.filter(**self.core_filters)
|
384
|
-
for field in self.field.foreign_related_fields:
|
385
|
-
val = getattr(self.instance, field.attname)
|
386
|
-
if val is None:
|
387
|
-
return queryset.none()
|
388
|
-
if self.field.many_to_one:
|
389
|
-
# Guard against field-like objects such as GenericRelation
|
390
|
-
# that abuse create_reverse_many_to_one_manager() with reverse
|
391
|
-
# one-to-many relationships instead and break known related
|
392
|
-
# objects assignment.
|
393
|
-
try:
|
394
|
-
target_field = self.field.target_field
|
395
|
-
except FieldError:
|
396
|
-
# The relationship has multiple target fields. Use a tuple
|
397
|
-
# for related object id.
|
398
|
-
rel_obj_id = tuple(
|
399
|
-
[
|
400
|
-
getattr(self.instance, target_field.attname)
|
401
|
-
for target_field in self.field.path_infos[-1].target_fields
|
402
|
-
]
|
403
|
-
)
|
404
|
-
else:
|
405
|
-
rel_obj_id = getattr(self.instance, target_field.attname)
|
406
|
-
queryset._known_related_objects = {
|
407
|
-
self.field: {rel_obj_id: self.instance}
|
408
|
-
}
|
409
|
-
return queryset
|
410
|
-
|
411
|
-
def _remove_prefetched_objects(self):
|
412
|
-
try:
|
413
|
-
self.instance._prefetched_objects_cache.pop(
|
414
|
-
self.field.remote_field.get_cache_name()
|
415
|
-
)
|
416
|
-
except (AttributeError, KeyError):
|
417
|
-
pass # nothing to clear from cache
|
418
|
-
|
419
|
-
def get_queryset(self):
|
420
|
-
# Even if this relation is not to primary key, we require still primary key value.
|
421
|
-
# The wish is that the instance has been already saved to DB,
|
422
|
-
# although having a primary key value isn't a guarantee of that.
|
423
|
-
if self.instance.id is None:
|
424
|
-
raise ValueError(
|
425
|
-
f"{self.instance.__class__.__name__!r} instance needs to have a "
|
426
|
-
f"primary key value before this relationship can be used."
|
427
|
-
)
|
428
|
-
try:
|
429
|
-
return self.instance._prefetched_objects_cache[
|
430
|
-
self.field.remote_field.get_cache_name()
|
431
|
-
]
|
432
|
-
except (AttributeError, KeyError):
|
433
|
-
# Use the base queryset class for this model
|
434
|
-
queryset = self.base_queryset_class(model=self.model)
|
435
|
-
return self._apply_rel_filters(queryset)
|
436
|
-
|
437
|
-
def get_prefetch_queryset(self, instances, queryset=None):
|
438
|
-
if queryset is None:
|
439
|
-
queryset = self.base_queryset_class(model=self.model)
|
440
|
-
|
441
|
-
rel_obj_attr = self.field.get_local_related_value
|
442
|
-
instance_attr = self.field.get_foreign_related_value
|
443
|
-
instances_dict = {instance_attr(inst): inst for inst in instances}
|
444
|
-
queryset = _filter_prefetch_queryset(queryset, self.field.name, instances)
|
445
|
-
|
446
|
-
# Since we just bypassed this class' get_queryset(), we must manage
|
447
|
-
# the reverse relation manually.
|
448
|
-
for rel_obj in queryset:
|
449
|
-
if not self.field.is_cached(rel_obj):
|
450
|
-
instance = instances_dict[rel_obj_attr(rel_obj)]
|
451
|
-
setattr(rel_obj, self.field.name, instance)
|
452
|
-
cache_name = self.field.remote_field.get_cache_name()
|
453
|
-
return queryset, rel_obj_attr, instance_attr, False, cache_name, False
|
454
|
-
|
455
|
-
def add(self, *objs, bulk=True):
|
456
|
-
self._check_fk_val()
|
457
|
-
self._remove_prefetched_objects()
|
458
|
-
|
459
|
-
def check_and_update_obj(obj):
|
460
|
-
if not isinstance(obj, self.model):
|
461
|
-
raise TypeError(
|
462
|
-
f"'{self.model._meta.object_name}' instance expected, got {obj!r}"
|
463
|
-
)
|
464
|
-
setattr(obj, self.field.name, self.instance)
|
465
|
-
|
466
|
-
if bulk:
|
467
|
-
ids = []
|
468
|
-
for obj in objs:
|
469
|
-
check_and_update_obj(obj)
|
470
|
-
if obj._state.adding:
|
471
|
-
raise ValueError(
|
472
|
-
f"{obj!r} instance isn't saved. Use bulk=False or save "
|
473
|
-
"the object first."
|
474
|
-
)
|
475
|
-
ids.append(obj.id)
|
476
|
-
self.model._meta.base_queryset.filter(id__in=ids).update(
|
477
|
-
**{
|
478
|
-
self.field.name: self.instance,
|
479
|
-
}
|
480
|
-
)
|
481
|
-
else:
|
482
|
-
with transaction.atomic(savepoint=False):
|
483
|
-
for obj in objs:
|
484
|
-
check_and_update_obj(obj)
|
485
|
-
obj.save()
|
486
|
-
|
487
|
-
def create(self, **kwargs):
|
488
|
-
self._check_fk_val()
|
489
|
-
kwargs[self.field.name] = self.instance
|
490
|
-
return self.base_queryset_class(model=self.model).create(**kwargs)
|
491
|
-
|
492
|
-
def get_or_create(self, **kwargs):
|
493
|
-
self._check_fk_val()
|
494
|
-
kwargs[self.field.name] = self.instance
|
495
|
-
return self.base_queryset_class(model=self.model).get_or_create(**kwargs)
|
496
|
-
|
497
|
-
def update_or_create(self, **kwargs):
|
498
|
-
self._check_fk_val()
|
499
|
-
kwargs[self.field.name] = self.instance
|
500
|
-
return self.base_queryset_class(model=self.model).update_or_create(**kwargs)
|
501
|
-
|
502
|
-
# remove() and clear() are only provided if the ForeignKey can have a
|
503
|
-
# value of null.
|
504
|
-
if rel.field.allow_null:
|
505
|
-
|
506
|
-
def remove(self, *objs, bulk=True):
|
507
|
-
if not objs:
|
508
|
-
return
|
509
|
-
self._check_fk_val()
|
510
|
-
val = self.field.get_foreign_related_value(self.instance)
|
511
|
-
old_ids = set()
|
512
|
-
for obj in objs:
|
513
|
-
if not isinstance(obj, self.model):
|
514
|
-
raise TypeError(
|
515
|
-
f"'{self.model._meta.object_name}' instance expected, got {obj!r}"
|
516
|
-
)
|
517
|
-
# Is obj actually part of this descriptor set?
|
518
|
-
if self.field.get_local_related_value(obj) == val:
|
519
|
-
old_ids.add(obj.id)
|
520
|
-
else:
|
521
|
-
raise self.field.remote_field.model.DoesNotExist(
|
522
|
-
f"{obj!r} is not related to {self.instance!r}."
|
523
|
-
)
|
524
|
-
self._clear(self.filter(id__in=old_ids), bulk)
|
525
|
-
|
526
|
-
def clear(self, *, bulk=True):
|
527
|
-
self._check_fk_val()
|
528
|
-
self._clear(self, bulk)
|
529
|
-
|
530
|
-
def _clear(self, queryset, bulk):
|
531
|
-
self._remove_prefetched_objects()
|
532
|
-
if bulk:
|
533
|
-
# `QuerySet.update()` is intrinsically atomic.
|
534
|
-
queryset.update(**{self.field.name: None})
|
535
|
-
else:
|
536
|
-
with transaction.atomic(savepoint=False):
|
537
|
-
for obj in queryset:
|
538
|
-
setattr(obj, self.field.name, None)
|
539
|
-
obj.save(update_fields=[self.field.name])
|
540
|
-
|
541
|
-
def set(self, objs, *, bulk=True, clear=False):
|
542
|
-
self._check_fk_val()
|
543
|
-
# Force evaluation of `objs` in case it's a queryset whose value
|
544
|
-
# could be affected by `manager.clear()`. Refs #19816.
|
545
|
-
objs = tuple(objs)
|
546
|
-
|
547
|
-
if self.field.allow_null:
|
548
|
-
with transaction.atomic(savepoint=False):
|
549
|
-
if clear:
|
550
|
-
self.clear(bulk=bulk)
|
551
|
-
self.add(*objs, bulk=bulk)
|
552
|
-
else:
|
553
|
-
old_objs = set(self.all())
|
554
|
-
new_objs = []
|
555
|
-
for obj in objs:
|
556
|
-
if obj in old_objs:
|
557
|
-
old_objs.remove(obj)
|
558
|
-
else:
|
559
|
-
new_objs.append(obj)
|
560
|
-
|
561
|
-
self.remove(*old_objs, bulk=bulk)
|
562
|
-
self.add(*new_objs, bulk=bulk)
|
563
|
-
else:
|
564
|
-
self.add(*objs, bulk=bulk)
|
565
|
-
|
566
|
-
return RelatedManager
|
567
|
-
|
568
|
-
|
569
|
-
class ManyToManyDescriptor(ReverseManyToOneDescriptor):
|
323
|
+
def get_related_manager(self, instance):
|
324
|
+
"""Return the ReverseManyToOneManager for this relation."""
|
325
|
+
return ReverseManyToOneManager(instance, self.rel)
|
326
|
+
|
327
|
+
def _get_set_deprecation_msg_params(self):
|
328
|
+
return (
|
329
|
+
"reverse side of a related set",
|
330
|
+
self.rel.get_accessor_name(),
|
331
|
+
)
|
332
|
+
|
333
|
+
|
334
|
+
class ForwardManyToManyDescriptor(RelationDescriptorBase):
|
570
335
|
"""
|
571
|
-
Accessor to the related objects manager on the forward
|
572
|
-
|
336
|
+
Accessor to the related objects manager on the forward side of a
|
337
|
+
many-to-many relation.
|
573
338
|
|
574
339
|
In the example::
|
575
340
|
|
576
341
|
class Pizza(Model):
|
577
342
|
toppings = ManyToManyField(Topping, related_name='pizzas')
|
578
343
|
|
579
|
-
``Pizza.toppings``
|
580
|
-
instances.
|
581
|
-
|
582
|
-
Most of the implementation is delegated to a dynamically defined manager
|
583
|
-
class built by ``create_forward_many_to_many_manager()`` defined below.
|
344
|
+
``Pizza.toppings`` is a ``ForwardManyToManyDescriptor`` instance.
|
584
345
|
"""
|
585
346
|
|
586
|
-
def __init__(self, rel, reverse=False):
|
587
|
-
super().__init__(rel)
|
588
|
-
|
589
|
-
self.reverse = reverse
|
590
|
-
|
591
347
|
@property
|
592
348
|
def through(self):
|
593
349
|
# through is provided so that you have easy access to the through
|
@@ -595,353 +351,43 @@ class ManyToManyDescriptor(ReverseManyToOneDescriptor):
|
|
595
351
|
# a property to ensure that the fully resolved value is returned.
|
596
352
|
return self.rel.through
|
597
353
|
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
return create_forward_many_to_many_manager(
|
603
|
-
related_model.query.__class__,
|
604
|
-
self.rel,
|
605
|
-
reverse=self.reverse,
|
606
|
-
)
|
354
|
+
def get_related_manager(self, instance):
|
355
|
+
"""Return the ForwardManyToManyManager for this relation."""
|
356
|
+
return ForwardManyToManyManager(instance, self.rel)
|
607
357
|
|
608
358
|
def _get_set_deprecation_msg_params(self):
|
609
359
|
return (
|
610
|
-
"
|
611
|
-
|
612
|
-
self.rel.get_accessor_name() if self.reverse else self.field.name,
|
360
|
+
"forward side of a many-to-many set",
|
361
|
+
self.field.name,
|
613
362
|
)
|
614
363
|
|
615
364
|
|
616
|
-
|
365
|
+
class ReverseManyToManyDescriptor(RelationDescriptorBase):
|
617
366
|
"""
|
618
|
-
|
367
|
+
Accessor to the related objects manager on the reverse side of a
|
368
|
+
many-to-many relation.
|
619
369
|
|
620
|
-
|
621
|
-
"""
|
370
|
+
In the example::
|
622
371
|
|
623
|
-
|
624
|
-
|
625
|
-
self.instance = instance
|
626
|
-
|
627
|
-
if not reverse:
|
628
|
-
self.model = rel.model
|
629
|
-
self.query_field_name = rel.field.related_query_name()
|
630
|
-
self.prefetch_cache_name = rel.field.name
|
631
|
-
self.source_field_name = rel.field.m2m_field_name()
|
632
|
-
self.target_field_name = rel.field.m2m_reverse_field_name()
|
633
|
-
self.symmetrical = rel.symmetrical
|
634
|
-
else:
|
635
|
-
self.model = rel.related_model
|
636
|
-
self.query_field_name = rel.field.name
|
637
|
-
self.prefetch_cache_name = rel.field.related_query_name()
|
638
|
-
self.source_field_name = rel.field.m2m_reverse_field_name()
|
639
|
-
self.target_field_name = rel.field.m2m_field_name()
|
640
|
-
self.symmetrical = False
|
641
|
-
|
642
|
-
# Store the base queryset class for this model
|
643
|
-
self.base_queryset_class = self.model._meta.queryset.__class__
|
644
|
-
|
645
|
-
self.through = rel.through
|
646
|
-
self.reverse = reverse
|
647
|
-
|
648
|
-
self.source_field = self.through._meta.get_field(self.source_field_name)
|
649
|
-
self.target_field = self.through._meta.get_field(self.target_field_name)
|
650
|
-
|
651
|
-
self.core_filters = {}
|
652
|
-
self.id_field_names = {}
|
653
|
-
for lh_field, rh_field in self.source_field.related_fields:
|
654
|
-
core_filter_key = f"{self.query_field_name}__{rh_field.name}"
|
655
|
-
self.core_filters[core_filter_key] = getattr(instance, rh_field.attname)
|
656
|
-
self.id_field_names[lh_field.name] = rh_field.name
|
657
|
-
|
658
|
-
self.related_val = self.source_field.get_foreign_related_value(instance)
|
659
|
-
if None in self.related_val:
|
660
|
-
raise ValueError(
|
661
|
-
f'"{instance!r}" needs to have a value for field "{self.id_field_names[self.source_field_name]}" before '
|
662
|
-
"this many-to-many relationship can be used."
|
663
|
-
)
|
664
|
-
# Even if this relation is not to primary key, we require still primary key value.
|
665
|
-
# The wish is that the instance has been already saved to DB,
|
666
|
-
# although having a primary key value isn't a guarantee of that.
|
667
|
-
if instance.id is None:
|
668
|
-
raise ValueError(
|
669
|
-
f"{instance.__class__.__name__!r} instance needs to have a primary key value before "
|
670
|
-
"a many-to-many relationship can be used."
|
671
|
-
)
|
672
|
-
|
673
|
-
@property
|
674
|
-
def query(self):
|
675
|
-
"""
|
676
|
-
Access the QuerySet for this relationship.
|
677
|
-
|
678
|
-
Example:
|
679
|
-
book.authors.query.filter(active=True)
|
680
|
-
"""
|
681
|
-
return self.get_queryset()
|
682
|
-
|
683
|
-
def _build_remove_filters(self, removed_vals):
|
684
|
-
filters = Q.create([(self.source_field_name, self.related_val)])
|
685
|
-
# No need to add a subquery condition if removed_vals is a QuerySet without
|
686
|
-
# filters.
|
687
|
-
removed_vals_filters = (
|
688
|
-
not isinstance(removed_vals, QuerySet) or removed_vals._has_filters()
|
689
|
-
)
|
690
|
-
if removed_vals_filters:
|
691
|
-
filters &= Q.create([(f"{self.target_field_name}__in", removed_vals)])
|
692
|
-
if self.symmetrical:
|
693
|
-
symmetrical_filters = Q.create(
|
694
|
-
[(self.target_field_name, self.related_val)]
|
695
|
-
)
|
696
|
-
if removed_vals_filters:
|
697
|
-
symmetrical_filters &= Q.create(
|
698
|
-
[(f"{self.source_field_name}__in", removed_vals)]
|
699
|
-
)
|
700
|
-
filters |= symmetrical_filters
|
701
|
-
return filters
|
702
|
-
|
703
|
-
def _apply_rel_filters(self, queryset):
|
704
|
-
"""
|
705
|
-
Filter the queryset for the instance this manager is bound to.
|
706
|
-
"""
|
707
|
-
queryset._defer_next_filter = True
|
708
|
-
return queryset._next_is_sticky().filter(**self.core_filters)
|
709
|
-
|
710
|
-
def _remove_prefetched_objects(self):
|
711
|
-
try:
|
712
|
-
self.instance._prefetched_objects_cache.pop(self.prefetch_cache_name)
|
713
|
-
except (AttributeError, KeyError):
|
714
|
-
pass # nothing to clear from cache
|
715
|
-
|
716
|
-
def get_queryset(self):
|
717
|
-
try:
|
718
|
-
return self.instance._prefetched_objects_cache[self.prefetch_cache_name]
|
719
|
-
except (AttributeError, KeyError):
|
720
|
-
queryset = self.base_queryset_class(model=self.model)
|
721
|
-
return self._apply_rel_filters(queryset)
|
722
|
-
|
723
|
-
def get_prefetch_queryset(self, instances, queryset=None):
|
724
|
-
if queryset is None:
|
725
|
-
queryset = self.base_queryset_class(model=self.model)
|
726
|
-
|
727
|
-
queryset = _filter_prefetch_queryset(
|
728
|
-
queryset._next_is_sticky(), self.query_field_name, instances
|
729
|
-
)
|
372
|
+
class Pizza(Model):
|
373
|
+
toppings = ManyToManyField(Topping, related_name='pizzas')
|
730
374
|
|
731
|
-
|
732
|
-
|
733
|
-
# there will already be a join on the join table, so we can just add
|
734
|
-
# the select.
|
735
|
-
|
736
|
-
# For non-autocreated 'through' models, can't assume we are
|
737
|
-
# dealing with PK values.
|
738
|
-
fk = self.through._meta.get_field(self.source_field_name)
|
739
|
-
join_table = fk.model._meta.db_table
|
740
|
-
qn = db_connection.ops.quote_name
|
741
|
-
queryset = queryset.extra(
|
742
|
-
select={
|
743
|
-
f"_prefetch_related_val_{f.attname}": f"{qn(join_table)}.{qn(f.column)}"
|
744
|
-
for f in fk.local_related_fields
|
745
|
-
}
|
746
|
-
)
|
747
|
-
return (
|
748
|
-
queryset,
|
749
|
-
lambda result: tuple(
|
750
|
-
getattr(result, f"_prefetch_related_val_{f.attname}")
|
751
|
-
for f in fk.local_related_fields
|
752
|
-
),
|
753
|
-
lambda inst: tuple(
|
754
|
-
f.get_db_prep_value(getattr(inst, f.attname), db_connection)
|
755
|
-
for f in fk.foreign_related_fields
|
756
|
-
),
|
757
|
-
False,
|
758
|
-
self.prefetch_cache_name,
|
759
|
-
False,
|
760
|
-
)
|
375
|
+
``Topping.pizzas`` is a ``ReverseManyToManyDescriptor`` instance.
|
376
|
+
"""
|
761
377
|
|
762
|
-
|
763
|
-
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
*objs,
|
769
|
-
through_defaults=through_defaults,
|
770
|
-
)
|
771
|
-
# If this is a symmetrical m2m relation to self, add the mirror
|
772
|
-
# entry in the m2m table.
|
773
|
-
if self.symmetrical:
|
774
|
-
self._add_items(
|
775
|
-
self.target_field_name,
|
776
|
-
self.source_field_name,
|
777
|
-
*objs,
|
778
|
-
through_defaults=through_defaults,
|
779
|
-
)
|
780
|
-
|
781
|
-
def remove(self, *objs):
|
782
|
-
self._remove_prefetched_objects()
|
783
|
-
self._remove_items(self.source_field_name, self.target_field_name, *objs)
|
784
|
-
|
785
|
-
def clear(self):
|
786
|
-
with transaction.atomic(savepoint=False):
|
787
|
-
self._remove_prefetched_objects()
|
788
|
-
filters = self._build_remove_filters(
|
789
|
-
self.base_queryset_class(model=self.model)
|
790
|
-
)
|
791
|
-
self.through.query.filter(filters).delete()
|
792
|
-
|
793
|
-
def set(self, objs, *, clear=False, through_defaults=None):
|
794
|
-
# Force evaluation of `objs` in case it's a queryset whose value
|
795
|
-
# could be affected by `manager.clear()`. Refs #19816.
|
796
|
-
objs = tuple(objs)
|
797
|
-
|
798
|
-
with transaction.atomic(savepoint=False):
|
799
|
-
if clear:
|
800
|
-
self.clear()
|
801
|
-
self.add(*objs, through_defaults=through_defaults)
|
802
|
-
else:
|
803
|
-
old_ids = set(
|
804
|
-
self.values_list(
|
805
|
-
self.target_field.target_field.attname, flat=True
|
806
|
-
)
|
807
|
-
)
|
808
|
-
|
809
|
-
new_objs = []
|
810
|
-
for obj in objs:
|
811
|
-
fk_val = (
|
812
|
-
self.target_field.get_foreign_related_value(obj)[0]
|
813
|
-
if isinstance(obj, self.model)
|
814
|
-
else self.target_field.get_prep_value(obj)
|
815
|
-
)
|
816
|
-
if fk_val in old_ids:
|
817
|
-
old_ids.remove(fk_val)
|
818
|
-
else:
|
819
|
-
new_objs.append(obj)
|
820
|
-
|
821
|
-
self.remove(*old_ids)
|
822
|
-
self.add(*new_objs, through_defaults=through_defaults)
|
823
|
-
|
824
|
-
def create(self, *, through_defaults=None, **kwargs):
|
825
|
-
new_obj = self.base_queryset_class(model=self.model).create(**kwargs)
|
826
|
-
self.add(new_obj, through_defaults=through_defaults)
|
827
|
-
return new_obj
|
828
|
-
|
829
|
-
def get_or_create(self, *, through_defaults=None, **kwargs):
|
830
|
-
obj, created = self.base_queryset_class(model=self.model).get_or_create(
|
831
|
-
**kwargs
|
832
|
-
)
|
833
|
-
# We only need to add() if created because if we got an object back
|
834
|
-
# from get() then the relationship already exists.
|
835
|
-
if created:
|
836
|
-
self.add(obj, through_defaults=through_defaults)
|
837
|
-
return obj, created
|
838
|
-
|
839
|
-
def update_or_create(self, *, through_defaults=None, **kwargs):
|
840
|
-
obj, created = self.base_queryset_class(model=self.model).update_or_create(
|
841
|
-
**kwargs
|
842
|
-
)
|
843
|
-
# We only need to add() if created because if we got an object back
|
844
|
-
# from get() then the relationship already exists.
|
845
|
-
if created:
|
846
|
-
self.add(obj, through_defaults=through_defaults)
|
847
|
-
return obj, created
|
848
|
-
|
849
|
-
def _get_target_ids(self, target_field_name, objs):
|
850
|
-
"""
|
851
|
-
Return the set of ids of `objs` that the target field references.
|
852
|
-
"""
|
853
|
-
from plain.models import Model
|
854
|
-
|
855
|
-
target_ids = set()
|
856
|
-
target_field = self.through._meta.get_field(target_field_name)
|
857
|
-
for obj in objs:
|
858
|
-
if isinstance(obj, self.model):
|
859
|
-
target_id = target_field.get_foreign_related_value(obj)[0]
|
860
|
-
if target_id is None:
|
861
|
-
raise ValueError(
|
862
|
-
f'Cannot add "{obj!r}": the value for field "{target_field_name}" is None'
|
863
|
-
)
|
864
|
-
target_ids.add(target_id)
|
865
|
-
elif isinstance(obj, Model):
|
866
|
-
raise TypeError(
|
867
|
-
f"'{self.model._meta.object_name}' instance expected, got {obj!r}"
|
868
|
-
)
|
869
|
-
else:
|
870
|
-
target_ids.add(target_field.get_prep_value(obj))
|
871
|
-
return target_ids
|
872
|
-
|
873
|
-
def _get_missing_target_ids(
|
874
|
-
self, source_field_name, target_field_name, target_ids
|
875
|
-
):
|
876
|
-
"""
|
877
|
-
Return the subset of ids of `objs` that aren't already assigned to
|
878
|
-
this relationship.
|
879
|
-
"""
|
880
|
-
vals = self.through.query.values_list(target_field_name, flat=True).filter(
|
881
|
-
**{
|
882
|
-
source_field_name: self.related_val[0],
|
883
|
-
f"{target_field_name}__in": target_ids,
|
884
|
-
}
|
885
|
-
)
|
886
|
-
return target_ids.difference(vals)
|
378
|
+
@property
|
379
|
+
def through(self):
|
380
|
+
# through is provided so that you have easy access to the through
|
381
|
+
# model (Book.authors.through) for inlines, etc. This is done as
|
382
|
+
# a property to ensure that the fully resolved value is returned.
|
383
|
+
return self.rel.through
|
887
384
|
|
888
|
-
|
889
|
-
|
890
|
-
)
|
891
|
-
|
892
|
-
|
893
|
-
|
894
|
-
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
through_defaults = dict(resolve_callables(through_defaults or {}))
|
899
|
-
target_ids = self._get_target_ids(target_field_name, objs)
|
900
|
-
|
901
|
-
missing_target_ids = self._get_missing_target_ids(
|
902
|
-
source_field_name, target_field_name, target_ids
|
903
|
-
)
|
904
|
-
with transaction.atomic(savepoint=False):
|
905
|
-
# Add the ones that aren't there already.
|
906
|
-
self.through.query.bulk_create(
|
907
|
-
[
|
908
|
-
self.through(
|
909
|
-
**through_defaults,
|
910
|
-
**{
|
911
|
-
f"{source_field_name}_id": self.related_val[0],
|
912
|
-
f"{target_field_name}_id": target_id,
|
913
|
-
},
|
914
|
-
)
|
915
|
-
for target_id in missing_target_ids
|
916
|
-
],
|
917
|
-
)
|
918
|
-
|
919
|
-
def _remove_items(self, source_field_name, target_field_name, *objs):
|
920
|
-
# source_field_name: the PK colname in join table for the source object
|
921
|
-
# target_field_name: the PK colname in join table for the target object
|
922
|
-
# *objs - objects to remove. Either object instances, or primary
|
923
|
-
# keys of object instances.
|
924
|
-
if not objs:
|
925
|
-
return
|
926
|
-
|
927
|
-
# Check that all the objects are of the right type
|
928
|
-
old_ids = set()
|
929
|
-
for obj in objs:
|
930
|
-
if isinstance(obj, self.model):
|
931
|
-
fk_val = self.target_field.get_foreign_related_value(obj)[0]
|
932
|
-
old_ids.add(fk_val)
|
933
|
-
else:
|
934
|
-
old_ids.add(obj)
|
935
|
-
|
936
|
-
with transaction.atomic(savepoint=False):
|
937
|
-
target_model_qs = self.base_queryset_class(model=self.model)
|
938
|
-
if target_model_qs._has_filters():
|
939
|
-
old_vals = target_model_qs.filter(
|
940
|
-
**{f"{self.target_field.target_field.attname}__in": old_ids}
|
941
|
-
)
|
942
|
-
else:
|
943
|
-
old_vals = old_ids
|
944
|
-
filters = self._build_remove_filters(old_vals)
|
945
|
-
self.through.query.filter(filters).delete()
|
946
|
-
|
947
|
-
return ManyRelatedManager
|
385
|
+
def get_related_manager(self, instance):
|
386
|
+
"""Return the ReverseManyToManyManager for this relation."""
|
387
|
+
return ReverseManyToManyManager(instance, self.rel)
|
388
|
+
|
389
|
+
def _get_set_deprecation_msg_params(self):
|
390
|
+
return (
|
391
|
+
"reverse side of a many-to-many set",
|
392
|
+
self.rel.get_accessor_name(),
|
393
|
+
)
|