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.
@@ -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,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 ReverseManyToOneDescriptor:
264
+ class RelationDescriptorBase:
287
265
  """
288
- Accessor to the related objects manager on the reverse side of a
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
- class Child(Model):
294
- parent = ForeignKey(Parent, related_name='children')
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 objects through the reverse relation.
318
-
319
- With the example above, when getting ``parent.children``:
279
+ Get the related manager when the descriptor is accessed.
320
280
 
321
- - ``self`` is the descriptor managing the ``children`` attribute
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
- 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
+ )
329
292
 
330
293
  def _get_set_deprecation_msg_params(self):
331
- return (
332
- "reverse side of a related set",
333
- 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()"
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
- def create_reverse_many_to_one_manager(superclass, rel):
308
+ class ReverseManyToOneDescriptor(RelationDescriptorBase):
345
309
  """
346
- 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::
347
314
 
348
- This manager 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.
349
321
  """
350
322
 
351
- class RelatedManager:
352
- def __init__(self, instance):
353
- self.model = rel.related_model
354
- self.instance = instance
355
- self.field = rel.field
356
- self.core_filters = {self.field.name: instance}
357
- # Store the base queryset class for this model
358
- self.base_queryset_class = rel.related_model._meta.queryset.__class__
359
-
360
- @property
361
- def query(self):
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 and reverse sides of
572
- a many-to-many relation.
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`` and ``Topping.pizzas`` are ``ManyToManyDescriptor``
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
- @cached_property
599
- def related_manager_cls(self):
600
- related_model = self.rel.related_model if self.reverse else self.rel.model
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
- "%s side of a many-to-many set"
611
- % ("reverse" if self.reverse else "forward"),
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
- def create_forward_many_to_many_manager(superclass, rel, reverse):
365
+ class ReverseManyToManyDescriptor(RelationDescriptorBase):
617
366
  """
618
- 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.
619
369
 
620
- This manager adds behaviors specific to many-to-many relations.
621
- """
370
+ In the example::
622
371
 
623
- class ManyRelatedManager:
624
- def __init__(self, instance=None):
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
- # 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(
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
- def _add_items(
889
- self, source_field_name, target_field_name, *objs, through_defaults=None
890
- ):
891
- # source_field_name: the PK fieldname in join table for the source object
892
- # target_field_name: the PK fieldname in join table for the target object
893
- # *objs - objects to add. Either object instances, or primary keys
894
- # of object instances.
895
- if not objs:
896
- return
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
+ )