plain.models 0.41.1__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.
@@ -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, Q
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,15 +100,13 @@ class ForwardManyToOneDescriptor:
122
100
  def is_cached(self, instance):
123
101
  return self.field.is_cached(instance)
124
102
 
125
- def get_queryset(self, **hints):
126
- qs = self.field.remote_field.model._base_manager.get_queryset()
127
- qs._add_hints(**hints)
103
+ def get_queryset(self) -> QuerySet:
104
+ qs = self.field.remote_field.model._meta.base_queryset
128
105
  return qs.all()
129
106
 
130
107
  def get_prefetch_queryset(self, instances, queryset=None):
131
108
  if queryset is None:
132
109
  queryset = self.get_queryset()
133
- queryset._add_hints(instance=instances[0])
134
110
 
135
111
  rel_obj_attr = self.field.get_foreign_related_value
136
112
  instance_attr = self.field.get_local_related_value
@@ -170,7 +146,7 @@ class ForwardManyToOneDescriptor:
170
146
  )
171
147
 
172
148
  def get_object(self, instance):
173
- qs = self.get_queryset(instance=instance)
149
+ qs = self.get_queryset()
174
150
  # Assuming the database enforces foreign keys, this won't fail.
175
151
  return qs.get(self.field.get_reverse_related_filter(instance))
176
152
 
@@ -285,57 +261,43 @@ class ForwardManyToOneDescriptor:
285
261
  return getattr, (self.field.model, self.field.name)
286
262
 
287
263
 
288
- class ReverseManyToOneDescriptor:
264
+ class RelationDescriptorBase:
289
265
  """
290
- Accessor to the related objects manager on the reverse side of a
291
- many-to-one relation.
292
-
293
- In the example::
266
+ Base class for relation descriptors that don't allow direct assignment.
294
267
 
295
- class Child(Model):
296
- parent = ForeignKey(Parent, related_name='children')
297
-
298
- ``Parent.children`` is a ``ReverseManyToOneDescriptor`` instance.
299
-
300
- Most of the implementation is delegated to a dynamically defined manager
301
- 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.
302
271
  """
303
272
 
304
273
  def __init__(self, rel):
305
274
  self.rel = rel
306
275
  self.field = rel.field
307
276
 
308
- @cached_property
309
- def related_manager_cls(self):
310
- related_model = self.rel.related_model
311
-
312
- return create_reverse_many_to_one_manager(
313
- related_model._default_manager.__class__,
314
- self.rel,
315
- )
316
-
317
277
  def __get__(self, instance, cls=None):
318
278
  """
319
- Get the related objects through the reverse relation.
320
-
321
- With the example above, when getting ``parent.children``:
279
+ Get the related manager when the descriptor is accessed.
322
280
 
323
- - ``self`` is the descriptor managing the ``children`` attribute
324
- - ``instance`` is the ``parent`` instance
325
- - ``cls`` is the ``Parent`` class (unused)
281
+ Subclasses must implement get_related_manager().
326
282
  """
327
283
  if instance is None:
328
284
  return self
285
+ return self.get_related_manager(instance)
329
286
 
330
- return self.related_manager_cls(instance)
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
+ )
331
292
 
332
293
  def _get_set_deprecation_msg_params(self):
333
- return (
334
- "reverse side of a related set",
335
- self.rel.get_accessor_name(),
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()"
336
297
  )
337
298
 
338
299
  def __set__(self, instance, value):
300
+ """Prevent direct assignment to the relation."""
339
301
  raise TypeError(
340
302
  "Direct assignment to the {} is prohibited. Use {}.set() instead.".format(
341
303
  *self._get_set_deprecation_msg_params()
@@ -343,252 +305,45 @@ class ReverseManyToOneDescriptor:
343
305
  )
344
306
 
345
307
 
346
- def create_reverse_many_to_one_manager(superclass, rel):
308
+ class ReverseManyToOneDescriptor(RelationDescriptorBase):
347
309
  """
348
- Create a manager for the reverse side of a many-to-one relation.
310
+ Accessor to the related objects manager on the reverse side of a
311
+ many-to-one relation.
312
+
313
+ In the example::
349
314
 
350
- This manager subclasses another manager, generally the default manager of
351
- the related model, and adds behaviors specific to many-to-one relations.
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.
352
321
  """
353
322
 
354
- class RelatedManager(superclass):
355
- def __init__(self, instance):
356
- super().__init__()
357
-
358
- self.instance = instance
359
- self.model = rel.related_model
360
- self.field = rel.field
361
-
362
- self.core_filters = {self.field.name: instance}
363
-
364
- def __call__(self, *, manager):
365
- manager = getattr(self.model, manager)
366
- manager_class = create_reverse_many_to_one_manager(manager.__class__, rel)
367
- return manager_class(self.instance)
368
-
369
- def _check_fk_val(self):
370
- for field in self.field.foreign_related_fields:
371
- if getattr(self.instance, field.attname) is None:
372
- raise ValueError(
373
- f'"{self.instance!r}" needs to have a value for field '
374
- f'"{field.attname}" before this relationship can be used.'
375
- )
376
-
377
- def _apply_rel_filters(self, queryset):
378
- """
379
- Filter the queryset for the instance this manager is bound to.
380
- """
381
- queryset._add_hints(instance=self.instance)
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
- queryset = super().get_queryset()
434
- return self._apply_rel_filters(queryset)
435
-
436
- def get_prefetch_queryset(self, instances, queryset=None):
437
- if queryset is None:
438
- queryset = super().get_queryset()
439
-
440
- queryset._add_hints(instance=instances[0])
441
-
442
- rel_obj_attr = self.field.get_local_related_value
443
- instance_attr = self.field.get_foreign_related_value
444
- instances_dict = {instance_attr(inst): inst for inst in instances}
445
- queryset = _filter_prefetch_queryset(queryset, self.field.name, instances)
446
-
447
- # Since we just bypassed this class' get_queryset(), we must manage
448
- # the reverse relation manually.
449
- for rel_obj in queryset:
450
- if not self.field.is_cached(rel_obj):
451
- instance = instances_dict[rel_obj_attr(rel_obj)]
452
- setattr(rel_obj, self.field.name, instance)
453
- cache_name = self.field.remote_field.get_cache_name()
454
- return queryset, rel_obj_attr, instance_attr, False, cache_name, False
455
-
456
- def add(self, *objs, bulk=True):
457
- self._check_fk_val()
458
- self._remove_prefetched_objects()
459
-
460
- def check_and_update_obj(obj):
461
- if not isinstance(obj, self.model):
462
- raise TypeError(
463
- f"'{self.model._meta.object_name}' instance expected, got {obj!r}"
464
- )
465
- setattr(obj, self.field.name, self.instance)
466
-
467
- if bulk:
468
- ids = []
469
- for obj in objs:
470
- check_and_update_obj(obj)
471
- if obj._state.adding:
472
- raise ValueError(
473
- f"{obj!r} instance isn't saved. Use bulk=False or save "
474
- "the object first."
475
- )
476
- ids.append(obj.id)
477
- self.model._base_manager.filter(id__in=ids).update(
478
- **{
479
- self.field.name: self.instance,
480
- }
481
- )
482
- else:
483
- with transaction.atomic(savepoint=False):
484
- for obj in objs:
485
- check_and_update_obj(obj)
486
- obj.save()
487
-
488
- def create(self, **kwargs):
489
- self._check_fk_val()
490
- kwargs[self.field.name] = self.instance
491
- return super().create(**kwargs)
492
-
493
- def get_or_create(self, **kwargs):
494
- self._check_fk_val()
495
- kwargs[self.field.name] = self.instance
496
- return super().get_or_create(**kwargs)
497
-
498
- def update_or_create(self, **kwargs):
499
- self._check_fk_val()
500
- kwargs[self.field.name] = self.instance
501
- return super().update_or_create(**kwargs)
502
-
503
- # remove() and clear() are only provided if the ForeignKey can have a
504
- # value of null.
505
- if rel.field.allow_null:
506
-
507
- def remove(self, *objs, bulk=True):
508
- if not objs:
509
- return
510
- self._check_fk_val()
511
- val = self.field.get_foreign_related_value(self.instance)
512
- old_ids = set()
513
- for obj in objs:
514
- if not isinstance(obj, self.model):
515
- raise TypeError(
516
- f"'{self.model._meta.object_name}' instance expected, got {obj!r}"
517
- )
518
- # Is obj actually part of this descriptor set?
519
- if self.field.get_local_related_value(obj) == val:
520
- old_ids.add(obj.id)
521
- else:
522
- raise self.field.remote_field.model.DoesNotExist(
523
- f"{obj!r} is not related to {self.instance!r}."
524
- )
525
- self._clear(self.filter(id__in=old_ids), bulk)
526
-
527
- def clear(self, *, bulk=True):
528
- self._check_fk_val()
529
- self._clear(self, bulk)
530
-
531
- def _clear(self, queryset, bulk):
532
- self._remove_prefetched_objects()
533
- if bulk:
534
- # `QuerySet.update()` is intrinsically atomic.
535
- queryset.update(**{self.field.name: None})
536
- else:
537
- with transaction.atomic(savepoint=False):
538
- for obj in queryset:
539
- setattr(obj, self.field.name, None)
540
- obj.save(update_fields=[self.field.name])
541
-
542
- def set(self, objs, *, bulk=True, clear=False):
543
- self._check_fk_val()
544
- # Force evaluation of `objs` in case it's a queryset whose value
545
- # could be affected by `manager.clear()`. Refs #19816.
546
- objs = tuple(objs)
547
-
548
- if self.field.allow_null:
549
- with transaction.atomic(savepoint=False):
550
- if clear:
551
- self.clear(bulk=bulk)
552
- self.add(*objs, bulk=bulk)
553
- else:
554
- old_objs = set(self.all())
555
- new_objs = []
556
- for obj in objs:
557
- if obj in old_objs:
558
- old_objs.remove(obj)
559
- else:
560
- new_objs.append(obj)
561
-
562
- self.remove(*old_objs, bulk=bulk)
563
- self.add(*new_objs, bulk=bulk)
564
- else:
565
- self.add(*objs, bulk=bulk)
566
-
567
- return RelatedManager
568
-
569
-
570
- 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):
571
335
  """
572
- Accessor to the related objects manager on the forward and reverse sides of
573
- a many-to-many relation.
336
+ Accessor to the related objects manager on the forward side of a
337
+ many-to-many relation.
574
338
 
575
339
  In the example::
576
340
 
577
341
  class Pizza(Model):
578
342
  toppings = ManyToManyField(Topping, related_name='pizzas')
579
343
 
580
- ``Pizza.toppings`` and ``Topping.pizzas`` are ``ManyToManyDescriptor``
581
- instances.
582
-
583
- Most of the implementation is delegated to a dynamically defined manager
584
- class built by ``create_forward_many_to_many_manager()`` defined below.
344
+ ``Pizza.toppings`` is a ``ForwardManyToManyDescriptor`` instance.
585
345
  """
586
346
 
587
- def __init__(self, rel, reverse=False):
588
- super().__init__(rel)
589
-
590
- self.reverse = reverse
591
-
592
347
  @property
593
348
  def through(self):
594
349
  # through is provided so that you have easy access to the through
@@ -596,348 +351,43 @@ class ManyToManyDescriptor(ReverseManyToOneDescriptor):
596
351
  # a property to ensure that the fully resolved value is returned.
597
352
  return self.rel.through
598
353
 
599
- @cached_property
600
- def related_manager_cls(self):
601
- related_model = self.rel.related_model if self.reverse else self.rel.model
602
-
603
- return create_forward_many_to_many_manager(
604
- related_model._default_manager.__class__,
605
- self.rel,
606
- reverse=self.reverse,
607
- )
354
+ def get_related_manager(self, instance):
355
+ """Return the ForwardManyToManyManager for this relation."""
356
+ return ForwardManyToManyManager(instance, self.rel)
608
357
 
609
358
  def _get_set_deprecation_msg_params(self):
610
359
  return (
611
- "%s side of a many-to-many set"
612
- % ("reverse" if self.reverse else "forward"),
613
- 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,
614
362
  )
615
363
 
616
364
 
617
- def create_forward_many_to_many_manager(superclass, rel, reverse):
365
+ class ReverseManyToManyDescriptor(RelationDescriptorBase):
618
366
  """
619
- Create a manager for the either side of a many-to-many relation.
367
+ Accessor to the related objects manager on the reverse side of a
368
+ many-to-many relation.
620
369
 
621
- This manager subclasses another manager, generally the default manager of
622
- the related model, and adds behaviors specific to many-to-many relations.
623
- """
370
+ In the example::
624
371
 
625
- class ManyRelatedManager(superclass):
626
- def __init__(self, instance=None):
627
- super().__init__()
628
-
629
- self.instance = instance
630
-
631
- if not reverse:
632
- self.model = rel.model
633
- self.query_field_name = rel.field.related_query_name()
634
- self.prefetch_cache_name = rel.field.name
635
- self.source_field_name = rel.field.m2m_field_name()
636
- self.target_field_name = rel.field.m2m_reverse_field_name()
637
- self.symmetrical = rel.symmetrical
638
- else:
639
- self.model = rel.related_model
640
- self.query_field_name = rel.field.name
641
- self.prefetch_cache_name = rel.field.related_query_name()
642
- self.source_field_name = rel.field.m2m_reverse_field_name()
643
- self.target_field_name = rel.field.m2m_field_name()
644
- self.symmetrical = False
645
-
646
- self.through = rel.through
647
- self.reverse = reverse
648
-
649
- self.source_field = self.through._meta.get_field(self.source_field_name)
650
- self.target_field = self.through._meta.get_field(self.target_field_name)
651
-
652
- self.core_filters = {}
653
- self.id_field_names = {}
654
- for lh_field, rh_field in self.source_field.related_fields:
655
- core_filter_key = f"{self.query_field_name}__{rh_field.name}"
656
- self.core_filters[core_filter_key] = getattr(instance, rh_field.attname)
657
- self.id_field_names[lh_field.name] = rh_field.name
658
-
659
- self.related_val = self.source_field.get_foreign_related_value(instance)
660
- if None in self.related_val:
661
- raise ValueError(
662
- f'"{instance!r}" needs to have a value for field "{self.id_field_names[self.source_field_name]}" before '
663
- "this many-to-many relationship can be used."
664
- )
665
- # Even if this relation is not to primary key, we require still primary key value.
666
- # The wish is that the instance has been already saved to DB,
667
- # although having a primary key value isn't a guarantee of that.
668
- if instance.id is None:
669
- raise ValueError(
670
- f"{instance.__class__.__name__!r} instance needs to have a primary key value before "
671
- "a many-to-many relationship can be used."
672
- )
673
-
674
- def __call__(self, *, manager):
675
- manager = getattr(self.model, manager)
676
- manager_class = create_forward_many_to_many_manager(
677
- manager.__class__, rel, reverse
678
- )
679
- return manager_class(instance=self.instance)
680
-
681
- def _build_remove_filters(self, removed_vals):
682
- filters = Q.create([(self.source_field_name, self.related_val)])
683
- # No need to add a subquery condition if removed_vals is a QuerySet without
684
- # filters.
685
- removed_vals_filters = (
686
- not isinstance(removed_vals, QuerySet) or removed_vals._has_filters()
687
- )
688
- if removed_vals_filters:
689
- filters &= Q.create([(f"{self.target_field_name}__in", removed_vals)])
690
- if self.symmetrical:
691
- symmetrical_filters = Q.create(
692
- [(self.target_field_name, self.related_val)]
693
- )
694
- if removed_vals_filters:
695
- symmetrical_filters &= Q.create(
696
- [(f"{self.source_field_name}__in", removed_vals)]
697
- )
698
- filters |= symmetrical_filters
699
- return filters
700
-
701
- def _apply_rel_filters(self, queryset):
702
- """
703
- Filter the queryset for the instance this manager is bound to.
704
- """
705
- queryset._add_hints(instance=self.instance)
706
- queryset._defer_next_filter = True
707
- return queryset._next_is_sticky().filter(**self.core_filters)
708
-
709
- def _remove_prefetched_objects(self):
710
- try:
711
- self.instance._prefetched_objects_cache.pop(self.prefetch_cache_name)
712
- except (AttributeError, KeyError):
713
- pass # nothing to clear from cache
714
-
715
- def get_queryset(self):
716
- try:
717
- return self.instance._prefetched_objects_cache[self.prefetch_cache_name]
718
- except (AttributeError, KeyError):
719
- queryset = super().get_queryset()
720
- return self._apply_rel_filters(queryset)
721
-
722
- def get_prefetch_queryset(self, instances, queryset=None):
723
- if queryset is None:
724
- queryset = super().get_queryset()
725
-
726
- queryset._add_hints(instance=instances[0])
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
- # M2M: need to annotate the query in order to get the primary model
732
- # that the secondary model was actually related to. We know that
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
- def add(self, *objs, through_defaults=None):
763
- self._remove_prefetched_objects()
764
- with transaction.atomic(savepoint=False):
765
- self._add_items(
766
- self.source_field_name,
767
- self.target_field_name,
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(super().get_queryset())
789
- self.through._default_manager.filter(filters).delete()
790
-
791
- def set(self, objs, *, clear=False, through_defaults=None):
792
- # Force evaluation of `objs` in case it's a queryset whose value
793
- # could be affected by `manager.clear()`. Refs #19816.
794
- objs = tuple(objs)
795
-
796
- with transaction.atomic(savepoint=False):
797
- if clear:
798
- self.clear()
799
- self.add(*objs, through_defaults=through_defaults)
800
- else:
801
- old_ids = set(
802
- self.values_list(
803
- self.target_field.target_field.attname, flat=True
804
- )
805
- )
806
-
807
- new_objs = []
808
- for obj in objs:
809
- fk_val = (
810
- self.target_field.get_foreign_related_value(obj)[0]
811
- if isinstance(obj, self.model)
812
- else self.target_field.get_prep_value(obj)
813
- )
814
- if fk_val in old_ids:
815
- old_ids.remove(fk_val)
816
- else:
817
- new_objs.append(obj)
818
-
819
- self.remove(*old_ids)
820
- self.add(*new_objs, through_defaults=through_defaults)
821
-
822
- def create(self, *, through_defaults=None, **kwargs):
823
- new_obj = super().create(**kwargs)
824
- self.add(new_obj, through_defaults=through_defaults)
825
- return new_obj
826
-
827
- def get_or_create(self, *, through_defaults=None, **kwargs):
828
- obj, created = super().get_or_create(**kwargs)
829
- # We only need to add() if created because if we got an object back
830
- # from get() then the relationship already exists.
831
- if created:
832
- self.add(obj, through_defaults=through_defaults)
833
- return obj, created
834
-
835
- def update_or_create(self, *, through_defaults=None, **kwargs):
836
- obj, created = super().update_or_create(**kwargs)
837
- # We only need to add() if created because if we got an object back
838
- # from get() then the relationship already exists.
839
- if created:
840
- self.add(obj, through_defaults=through_defaults)
841
- return obj, created
842
-
843
- def _get_target_ids(self, target_field_name, objs):
844
- """
845
- Return the set of ids of `objs` that the target field references.
846
- """
847
- from plain.models import Model
848
-
849
- target_ids = set()
850
- target_field = self.through._meta.get_field(target_field_name)
851
- for obj in objs:
852
- if isinstance(obj, self.model):
853
- target_id = target_field.get_foreign_related_value(obj)[0]
854
- if target_id is None:
855
- raise ValueError(
856
- f'Cannot add "{obj!r}": the value for field "{target_field_name}" is None'
857
- )
858
- target_ids.add(target_id)
859
- elif isinstance(obj, Model):
860
- raise TypeError(
861
- f"'{self.model._meta.object_name}' instance expected, got {obj!r}"
862
- )
863
- else:
864
- target_ids.add(target_field.get_prep_value(obj))
865
- return target_ids
866
-
867
- def _get_missing_target_ids(
868
- self, source_field_name, target_field_name, target_ids
869
- ):
870
- """
871
- Return the subset of ids of `objs` that aren't already assigned to
872
- this relationship.
873
- """
874
- vals = self.through._default_manager.values_list(
875
- target_field_name, flat=True
876
- ).filter(
877
- **{
878
- source_field_name: self.related_val[0],
879
- f"{target_field_name}__in": target_ids,
880
- }
881
- )
882
- 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
883
384
 
884
- def _add_items(
885
- self, source_field_name, target_field_name, *objs, through_defaults=None
886
- ):
887
- # source_field_name: the PK fieldname in join table for the source object
888
- # target_field_name: the PK fieldname in join table for the target object
889
- # *objs - objects to add. Either object instances, or primary keys
890
- # of object instances.
891
- if not objs:
892
- return
893
-
894
- through_defaults = dict(resolve_callables(through_defaults or {}))
895
- target_ids = self._get_target_ids(target_field_name, objs)
896
-
897
- missing_target_ids = self._get_missing_target_ids(
898
- source_field_name, target_field_name, target_ids
899
- )
900
- with transaction.atomic(savepoint=False):
901
- # Add the ones that aren't there already.
902
- self.through._default_manager.bulk_create(
903
- [
904
- self.through(
905
- **through_defaults,
906
- **{
907
- f"{source_field_name}_id": self.related_val[0],
908
- f"{target_field_name}_id": target_id,
909
- },
910
- )
911
- for target_id in missing_target_ids
912
- ],
913
- )
914
-
915
- def _remove_items(self, source_field_name, target_field_name, *objs):
916
- # source_field_name: the PK colname in join table for the source object
917
- # target_field_name: the PK colname in join table for the target object
918
- # *objs - objects to remove. Either object instances, or primary
919
- # keys of object instances.
920
- if not objs:
921
- return
922
-
923
- # Check that all the objects are of the right type
924
- old_ids = set()
925
- for obj in objs:
926
- if isinstance(obj, self.model):
927
- fk_val = self.target_field.get_foreign_related_value(obj)[0]
928
- old_ids.add(fk_val)
929
- else:
930
- old_ids.add(obj)
931
-
932
- with transaction.atomic(savepoint=False):
933
- target_model_qs = super().get_queryset()
934
- if target_model_qs._has_filters():
935
- old_vals = target_model_qs.filter(
936
- **{f"{self.target_field.target_field.attname}__in": old_ids}
937
- )
938
- else:
939
- old_vals = old_ids
940
- filters = self._build_remove_filters(old_vals)
941
- self.through._default_manager.filter(filters).delete()
942
-
943
- 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
+ )