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.
- plain/models/CHANGELOG.md +32 -0
- plain/models/README.md +65 -22
- plain/models/__init__.py +0 -2
- plain/models/base.py +14 -35
- plain/models/cli.py +16 -22
- plain/models/constraints.py +1 -1
- plain/models/deletion.py +1 -1
- plain/models/fields/__init__.py +1 -1
- plain/models/fields/related.py +7 -32
- plain/models/fields/related_descriptors.py +81 -631
- plain/models/fields/related_lookups.py +2 -2
- plain/models/fields/related_managers.py +629 -0
- plain/models/fields/reverse_related.py +5 -8
- plain/models/forms.py +1 -1
- plain/models/migrations/autodetector.py +0 -18
- plain/models/migrations/operations/__init__.py +0 -2
- plain/models/migrations/operations/models.py +3 -57
- plain/models/migrations/operations/special.py +2 -8
- plain/models/migrations/recorder.py +1 -1
- plain/models/migrations/serializer.py +0 -12
- plain/models/migrations/state.py +1 -55
- plain/models/options.py +23 -86
- plain/models/query.py +10 -41
- plain/models/query_utils.py +1 -1
- plain/models/sql/compiler.py +5 -5
- plain/models/sql/query.py +2 -2
- {plain_models-0.41.1.dist-info → plain_models-0.43.0.dist-info}/METADATA +66 -23
- {plain_models-0.41.1.dist-info → plain_models-0.43.0.dist-info}/RECORD +31 -31
- plain/models/manager.py +0 -176
- {plain_models-0.41.1.dist-info → plain_models-0.43.0.dist-info}/WHEEL +0 -0
- {plain_models-0.41.1.dist-info → plain_models-0.43.0.dist-info}/entry_points.txt +0 -0
- {plain_models-0.41.1.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,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
|
126
|
-
qs = self.field.remote_field.model.
|
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(
|
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
|
264
|
+
class RelationDescriptorBase:
|
289
265
|
"""
|
290
|
-
|
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
|
-
|
296
|
-
|
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
|
320
|
-
|
321
|
-
With the example above, when getting ``parent.children``:
|
279
|
+
Get the related manager when the descriptor is accessed.
|
322
280
|
|
323
|
-
|
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
|
-
|
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
|
-
|
334
|
-
|
335
|
-
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()"
|
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
|
-
|
308
|
+
class ReverseManyToOneDescriptor(RelationDescriptorBase):
|
347
309
|
"""
|
348
|
-
|
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
|
-
|
351
|
-
|
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
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
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
|
573
|
-
|
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``
|
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
|
-
|
600
|
-
|
601
|
-
|
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
|
-
"
|
612
|
-
|
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
|
-
|
365
|
+
class ReverseManyToManyDescriptor(RelationDescriptorBase):
|
618
366
|
"""
|
619
|
-
|
367
|
+
Accessor to the related objects manager on the reverse side of a
|
368
|
+
many-to-many relation.
|
620
369
|
|
621
|
-
|
622
|
-
the related model, and adds behaviors specific to many-to-many relations.
|
623
|
-
"""
|
370
|
+
In the example::
|
624
371
|
|
625
|
-
|
626
|
-
|
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
|
-
|
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(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
|
-
|
885
|
-
|
886
|
-
)
|
887
|
-
|
888
|
-
|
889
|
-
|
890
|
-
|
891
|
-
|
892
|
-
|
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
|
+
)
|