15
15
import strawberry
16
16
from django .db import models , transaction
17
17
from django .db .models .base import Model
18
+ from django .db .models .fields import Field
18
19
from django .db .models .fields .related import ManyToManyField
19
20
from django .db .models .fields .reverse_related import (
20
21
ForeignObjectRel ,
44
45
)
45
46
46
47
if TYPE_CHECKING :
47
- from django .db .models .manager import ManyToManyRelatedManager , RelatedManager
48
+ from django .db .models .manager import (
49
+ BaseManager ,
50
+ ManyToManyRelatedManager ,
51
+ RelatedManager ,
52
+ )
48
53
from strawberry .types .info import Info
49
54
50
55
@@ -88,6 +93,7 @@ def _parse_data(
88
93
value : Any ,
89
94
* ,
90
95
key_attr : str | None = None ,
96
+ full_clean : bool | FullCleanOptions = True ,
91
97
):
92
98
obj , data = _parse_pk (value , model , key_attr = key_attr )
93
99
parsed_data = {}
@@ -97,10 +103,21 @@ def _parse_data(
97
103
continue
98
104
99
105
if isinstance (v , ParsedObject ):
100
- if v .pk is None :
101
- v = create (info , model , v .data or {}) # noqa: PLW2901
106
+ if v .pk in {None , UNSET }:
107
+ related_field = cast ("Field" , get_model_fields (model ).get (k ))
108
+ related_model = related_field .related_model
109
+ v = create ( # noqa: PLW2901
110
+ info ,
111
+ cast ("type[Model]" , related_model ),
112
+ v .data or {},
113
+ key_attr = key_attr ,
114
+ full_clean = full_clean ,
115
+ exclude_m2m = [related_field .name ],
116
+ )
102
117
elif isinstance (v .pk , models .Model ) and v .data :
103
- v = update (info , v .pk , v .data , key_attr = key_attr ) # noqa: PLW2901
118
+ v = update ( # noqa: PLW2901
119
+ info , v .pk , v .data , key_attr = key_attr , full_clean = full_clean
120
+ )
104
121
else :
105
122
v = v .pk # noqa: PLW2901
106
123
@@ -222,6 +239,7 @@ def prepare_create_update(
222
239
data : dict [str , Any ],
223
240
key_attr : str | None = None ,
224
241
full_clean : bool | FullCleanOptions = True ,
242
+ exclude_m2m : list [str ] | None = None ,
225
243
) -> tuple [
226
244
Model ,
227
245
dict [str , object ],
@@ -237,6 +255,7 @@ def prepare_create_update(
237
255
fields = get_model_fields (model )
238
256
m2m : list [tuple [ManyToManyField | ForeignObjectRel , Any ]] = []
239
257
direct_field_values : dict [str , object ] = {}
258
+ exclude_m2m = exclude_m2m or []
240
259
241
260
if dataclasses .is_dataclass (data ):
242
261
data = vars (data )
@@ -256,6 +275,8 @@ def prepare_create_update(
256
275
# (but only if the instance is already saved and we are updating it)
257
276
value = False # noqa: PLW2901
258
277
elif isinstance (field , (ManyToManyField , ForeignObjectRel )):
278
+ if name in exclude_m2m :
279
+ continue
259
280
# m2m will be processed later
260
281
m2m .append ((field , value ))
261
282
direct_field_value = False
@@ -269,14 +290,19 @@ def prepare_create_update(
269
290
cast ("type[Model]" , field .related_model ),
270
291
value ,
271
292
key_attr = key_attr ,
293
+ full_clean = full_clean ,
272
294
)
273
295
if value is None and not value_data :
274
296
value = None # noqa: PLW2901
275
297
276
298
# If foreign object is not found, then create it
277
- elif value is None :
278
- value = field .related_model ._default_manager .create ( # noqa: PLW2901
279
- ** value_data ,
299
+ elif value in {None , UNSET }:
300
+ value = create ( # noqa: PLW2901
301
+ info ,
302
+ field .related_model ,
303
+ value_data ,
304
+ key_attr = key_attr ,
305
+ full_clean = full_clean ,
280
306
)
281
307
282
308
# If foreign object does not need updating, then skip it
@@ -309,6 +335,7 @@ def create(
309
335
key_attr : str | None = None ,
310
336
full_clean : bool | FullCleanOptions = True ,
311
337
pre_save_hook : Callable [[_M ], None ] | None = None ,
338
+ exclude_m2m : list [str ] | None = None ,
312
339
) -> _M : ...
313
340
314
341
@@ -321,10 +348,10 @@ def create(
321
348
key_attr : str | None = None ,
322
349
full_clean : bool | FullCleanOptions = True ,
323
350
pre_save_hook : Callable [[_M ], None ] | None = None ,
351
+ exclude_m2m : list [str ] | None = None ,
324
352
) -> list [_M ]: ...
325
353
326
354
327
- @transaction .atomic
328
355
def create (
329
356
info : Info ,
330
357
model : type [_M ],
@@ -333,12 +360,43 @@ def create(
333
360
key_attr : str | None = None ,
334
361
full_clean : bool | FullCleanOptions = True ,
335
362
pre_save_hook : Callable [[_M ], None ] | None = None ,
363
+ exclude_m2m : list [str ] | None = None ,
364
+ ) -> list [_M ] | _M :
365
+ return _create (
366
+ info ,
367
+ model ._default_manager ,
368
+ data ,
369
+ key_attr = key_attr ,
370
+ full_clean = full_clean ,
371
+ pre_save_hook = pre_save_hook ,
372
+ exclude_m2m = exclude_m2m ,
373
+ )
374
+
375
+
376
+ @transaction .atomic
377
+ def _create (
378
+ info : Info ,
379
+ manager : BaseManager ,
380
+ data : dict [str , Any ] | list [dict [str , Any ]],
381
+ * ,
382
+ key_attr : str | None = None ,
383
+ full_clean : bool | FullCleanOptions = True ,
384
+ pre_save_hook : Callable [[_M ], None ] | None = None ,
385
+ exclude_m2m : list [str ] | None = None ,
336
386
) -> list [_M ] | _M :
387
+ model = manager .model
337
388
# Before creating your instance, verify this is not a bulk create
338
389
# if so, add them one by one. Otherwise, get to work.
339
390
if isinstance (data , list ):
340
391
return [
341
- create (info , model , d , key_attr = key_attr , full_clean = full_clean )
392
+ create (
393
+ info ,
394
+ model ,
395
+ d ,
396
+ key_attr = key_attr ,
397
+ full_clean = full_clean ,
398
+ exclude_m2m = exclude_m2m ,
399
+ )
342
400
for d in data
343
401
]
344
402
@@ -365,6 +423,7 @@ def create(
365
423
data = data ,
366
424
full_clean = full_clean ,
367
425
key_attr = key_attr ,
426
+ exclude_m2m = exclude_m2m ,
368
427
)
369
428
370
429
# Creating the instance directly via create() without full-clean will
@@ -376,7 +435,7 @@ def create(
376
435
377
436
# Create the instance using the manager create method to respect
378
437
# manager create overrides. This also ensures support for proxy-models.
379
- instance = model . _default_manager .create (** create_kwargs )
438
+ instance = manager .create (** create_kwargs )
380
439
381
440
for field , value in m2m :
382
441
update_m2m (info , instance , field , value , key_attr )
@@ -393,6 +452,7 @@ def update(
393
452
key_attr : str | None = None ,
394
453
full_clean : bool | FullCleanOptions = True ,
395
454
pre_save_hook : Callable [[_M ], None ] | None = None ,
455
+ exclude_m2m : list [str ] | None = None ,
396
456
) -> _M : ...
397
457
398
458
@@ -405,6 +465,7 @@ def update(
405
465
key_attr : str | None = None ,
406
466
full_clean : bool | FullCleanOptions = True ,
407
467
pre_save_hook : Callable [[_M ], None ] | None = None ,
468
+ exclude_m2m : list [str ] | None = None ,
408
469
) -> list [_M ]: ...
409
470
410
471
@@ -417,6 +478,7 @@ def update(
417
478
key_attr : str | None = None ,
418
479
full_clean : bool | FullCleanOptions = True ,
419
480
pre_save_hook : Callable [[_M ], None ] | None = None ,
481
+ exclude_m2m : list [str ] | None = None ,
420
482
) -> _M | list [_M ]:
421
483
# Unwrap lazy objects since they have a proxy __iter__ method that will make
422
484
# them iterables even if the wrapped object isn't
@@ -433,6 +495,7 @@ def update(
433
495
key_attr = key_attr ,
434
496
full_clean = full_clean ,
435
497
pre_save_hook = pre_save_hook ,
498
+ exclude_m2m = exclude_m2m ,
436
499
)
437
500
for instance in instances
438
501
]
@@ -443,6 +506,7 @@ def update(
443
506
data = data ,
444
507
key_attr = key_attr ,
445
508
full_clean = full_clean ,
509
+ exclude_m2m = exclude_m2m ,
446
510
)
447
511
448
512
if pre_save_hook is not None :
@@ -554,15 +618,22 @@ def update_m2m(
554
618
use_remove = True
555
619
if isinstance (field , ManyToManyField ):
556
620
manager = cast ("RelatedManager" , getattr (instance , field .attname ))
621
+ reverse_field_name = field .remote_field .related_name # type: ignore
557
622
else :
558
623
assert isinstance (field , (ManyToManyRel , ManyToOneRel ))
559
624
accessor_name = field .get_accessor_name ()
625
+ reverse_field_name = field .field .name
560
626
assert accessor_name
561
627
manager = cast ("RelatedManager" , getattr (instance , accessor_name ))
562
628
if field .one_to_many :
563
629
# remove if field is nullable, otherwise delete
564
630
use_remove = field .remote_field .null is True
565
631
632
+ # Create a data dict containing the reference to the instance and exclude it from
633
+ # nested m2m creation (to break circular references)
634
+ ref_instance_data = {reverse_field_name : instance }
635
+ exclude_m2m = [reverse_field_name ]
636
+
566
637
to_add = []
567
638
to_remove = []
568
639
to_delete = []
@@ -581,7 +652,11 @@ def update_m2m(
581
652
need_remove_cache = need_remove_cache or bool (values )
582
653
for v in values :
583
654
obj , data = _parse_data (
584
- info , cast ("type[Model]" , manager .model ), v , key_attr = key_attr
655
+ info ,
656
+ cast ("type[Model]" , manager .model ),
657
+ v ,
658
+ key_attr = key_attr ,
659
+ full_clean = full_clean ,
585
660
)
586
661
if obj :
587
662
data .pop (key_attr , None )
@@ -621,14 +696,17 @@ def update_m2m(
621
696
622
697
existing .discard (obj )
623
698
else :
624
- if key_attr not in data : # we have a Input Type
625
- obj , _ = manager .get_or_create (** data )
626
- else :
627
- data .pop (key_attr )
628
- obj = manager .create (** data )
629
-
630
- if full_clean :
631
- obj .full_clean (** full_clean_options )
699
+ # If we've reached here, the key_attr should be UNSET or missing. So
700
+ # let's remove it if it is there.
701
+ data .pop (key_attr , None )
702
+ obj = _create (
703
+ info ,
704
+ manager ,
705
+ data | ref_instance_data ,
706
+ key_attr = key_attr ,
707
+ full_clean = full_clean ,
708
+ exclude_m2m = exclude_m2m ,
709
+ )
632
710
existing .discard (obj )
633
711
634
712
for remaining in existing :
@@ -645,6 +723,7 @@ def update_m2m(
645
723
cast ("type[Model]" , manager .model ),
646
724
v ,
647
725
key_attr = key_attr ,
726
+ full_clean = full_clean ,
648
727
)
649
728
if obj and data :
650
729
data .pop (key_attr , None )
@@ -656,18 +735,28 @@ def update_m2m(
656
735
data .pop (key_attr , None )
657
736
to_add .append (obj )
658
737
elif data :
659
- if key_attr not in data :
660
- manager .get_or_create (** data )
661
- else :
662
- data .pop (key_attr )
663
- manager .create (** data )
738
+ # If we've reached here, the key_attr should be UNSET or missing. So
739
+ # let's remove it if it is there.
740
+ data .pop (key_attr , None )
741
+ _create (
742
+ info ,
743
+ manager ,
744
+ data | ref_instance_data ,
745
+ key_attr = key_attr ,
746
+ full_clean = full_clean ,
747
+ exclude_m2m = exclude_m2m ,
748
+ )
664
749
else :
665
750
raise AssertionError
666
751
667
752
need_remove_cache = need_remove_cache or bool (value .remove )
668
753
for v in value .remove or []:
669
754
obj , data = _parse_data (
670
- info , cast ("type[Model]" , manager .model ), v , key_attr = key_attr
755
+ info ,
756
+ cast ("type[Model]" , manager .model ),
757
+ v ,
758
+ key_attr = key_attr ,
759
+ full_clean = full_clean ,
671
760
)
672
761
data .pop (key_attr , None )
673
762
assert not data
0 commit comments