django-bulk-hooks 0.1.251__tar.gz → 0.1.253__tar.gz

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.

Potentially problematic release.


This version of django-bulk-hooks might be problematic. Click here for more details.

Files changed (17) hide show
  1. {django_bulk_hooks-0.1.251 → django_bulk_hooks-0.1.253}/PKG-INFO +1 -1
  2. {django_bulk_hooks-0.1.251 → django_bulk_hooks-0.1.253}/django_bulk_hooks/queryset.py +190 -5
  3. {django_bulk_hooks-0.1.251 → django_bulk_hooks-0.1.253}/pyproject.toml +1 -1
  4. {django_bulk_hooks-0.1.251 → django_bulk_hooks-0.1.253}/LICENSE +0 -0
  5. {django_bulk_hooks-0.1.251 → django_bulk_hooks-0.1.253}/README.md +0 -0
  6. {django_bulk_hooks-0.1.251 → django_bulk_hooks-0.1.253}/django_bulk_hooks/__init__.py +0 -0
  7. {django_bulk_hooks-0.1.251 → django_bulk_hooks-0.1.253}/django_bulk_hooks/conditions.py +0 -0
  8. {django_bulk_hooks-0.1.251 → django_bulk_hooks-0.1.253}/django_bulk_hooks/constants.py +0 -0
  9. {django_bulk_hooks-0.1.251 → django_bulk_hooks-0.1.253}/django_bulk_hooks/context.py +0 -0
  10. {django_bulk_hooks-0.1.251 → django_bulk_hooks-0.1.253}/django_bulk_hooks/decorators.py +0 -0
  11. {django_bulk_hooks-0.1.251 → django_bulk_hooks-0.1.253}/django_bulk_hooks/engine.py +0 -0
  12. {django_bulk_hooks-0.1.251 → django_bulk_hooks-0.1.253}/django_bulk_hooks/enums.py +0 -0
  13. {django_bulk_hooks-0.1.251 → django_bulk_hooks-0.1.253}/django_bulk_hooks/handler.py +0 -0
  14. {django_bulk_hooks-0.1.251 → django_bulk_hooks-0.1.253}/django_bulk_hooks/manager.py +0 -0
  15. {django_bulk_hooks-0.1.251 → django_bulk_hooks-0.1.253}/django_bulk_hooks/models.py +0 -0
  16. {django_bulk_hooks-0.1.251 → django_bulk_hooks-0.1.253}/django_bulk_hooks/priority.py +0 -0
  17. {django_bulk_hooks-0.1.251 → django_bulk_hooks-0.1.253}/django_bulk_hooks/registry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-bulk-hooks
3
- Version: 0.1.251
3
+ Version: 0.1.253
4
4
  Summary: Hook-style hooks for Django bulk operations like bulk_create and bulk_update.
5
5
  License: MIT
6
6
  Keywords: django,bulk,hooks
@@ -520,9 +520,133 @@ class HookQuerySetMixin:
520
520
  # Fire hooks before DB ops
521
521
  if not bypass_hooks:
522
522
  ctx = HookContext(model_cls, bypass_hooks=False) # Pass bypass_hooks
523
- if not bypass_validation:
524
- engine.run(model_cls, VALIDATE_CREATE, objs, ctx=ctx)
525
- engine.run(model_cls, BEFORE_CREATE, objs, ctx=ctx)
523
+
524
+ if update_conflicts and unique_fields:
525
+ # For upsert operations, we need to determine which records will be created vs updated
526
+ # Check which records already exist in the database based on unique fields
527
+ existing_records = []
528
+ new_records = []
529
+
530
+ # Build a filter to check which records already exist
531
+ unique_values = []
532
+ for obj in objs:
533
+ unique_value = {}
534
+ for field_name in unique_fields:
535
+ if hasattr(obj, field_name):
536
+ unique_value[field_name] = getattr(obj, field_name)
537
+ if unique_value:
538
+ unique_values.append(unique_value)
539
+
540
+ if unique_values:
541
+ # Query the database to see which records already exist
542
+ from django.db.models import Q
543
+ existing_filters = Q()
544
+ for unique_value in unique_values:
545
+ filter_kwargs = {}
546
+ for field_name, value in unique_value.items():
547
+ filter_kwargs[field_name] = value
548
+ existing_filters |= Q(**filter_kwargs)
549
+
550
+ existing_pks = set(
551
+ model_cls.objects.filter(existing_filters).values_list('pk', flat=True)
552
+ )
553
+
554
+ # Separate records based on whether they already exist
555
+ for obj in objs:
556
+ obj_unique_value = {}
557
+ for field_name in unique_fields:
558
+ if hasattr(obj, field_name):
559
+ obj_unique_value[field_name] = getattr(obj, field_name)
560
+
561
+ # Check if this record already exists
562
+ if obj_unique_value:
563
+ existing_q = Q()
564
+ for field_name, value in obj_unique_value.items():
565
+ existing_q &= Q(**{field_name: value})
566
+
567
+ if model_cls.objects.filter(existing_q).exists():
568
+ existing_records.append(obj)
569
+ else:
570
+ new_records.append(obj)
571
+ else:
572
+ # If we can't determine uniqueness, treat as new
573
+ new_records.append(obj)
574
+ else:
575
+ # If no unique fields, treat all as new
576
+ new_records = objs
577
+
578
+ # Handle auto_now fields intelligently for upsert operations
579
+ # Only set auto_now fields on records that will actually be created
580
+ for obj in new_records:
581
+ for field in model_cls._meta.local_fields:
582
+ if hasattr(field, "auto_now") and field.auto_now:
583
+ field.pre_save(obj, add=True)
584
+ elif hasattr(field, "auto_now_add") and field.auto_now_add:
585
+ if getattr(obj, field.name) is None:
586
+ field.pre_save(obj, add=True)
587
+
588
+ # For existing records, preserve their original auto_now values
589
+ # We'll need to fetch them from the database to preserve the timestamps
590
+ if existing_records:
591
+ # Get the unique field values for existing records
592
+ existing_unique_values = []
593
+ for obj in existing_records:
594
+ unique_value = {}
595
+ for field_name in unique_fields:
596
+ if hasattr(obj, field_name):
597
+ unique_value[field_name] = getattr(obj, field_name)
598
+ if unique_value:
599
+ existing_unique_values.append(unique_value)
600
+
601
+ if existing_unique_values:
602
+ # Build filter to fetch existing records
603
+ existing_filters = Q()
604
+ for unique_value in existing_unique_values:
605
+ filter_kwargs = {}
606
+ for field_name, value in unique_value.items():
607
+ filter_kwargs[field_name] = value
608
+ existing_filters |= Q(**filter_kwargs)
609
+
610
+ # Fetch existing records to preserve their auto_now values
611
+ existing_db_records = model_cls.objects.filter(existing_filters)
612
+ existing_db_map = {}
613
+ for db_record in existing_db_records:
614
+ key = tuple(getattr(db_record, field) for field in unique_fields)
615
+ existing_db_map[key] = db_record
616
+
617
+ # Preserve auto_now field values for existing records
618
+ for obj in existing_records:
619
+ key = tuple(getattr(obj, field) for field in unique_fields)
620
+ if key in existing_db_map:
621
+ db_record = existing_db_map[key]
622
+ for field in model_cls._meta.local_fields:
623
+ if hasattr(field, "auto_now") and field.auto_now:
624
+ # Preserve the original updated_at timestamp
625
+ setattr(obj, field.name, getattr(db_record, field.name))
626
+
627
+ # Run validation hooks on all records
628
+ if not bypass_validation:
629
+ engine.run(model_cls, VALIDATE_CREATE, objs, ctx=ctx)
630
+
631
+ # Run appropriate BEFORE hooks based on what will happen
632
+ if new_records:
633
+ engine.run(model_cls, BEFORE_CREATE, new_records, ctx=ctx)
634
+ if existing_records:
635
+ engine.run(model_cls, BEFORE_UPDATE, existing_records, ctx=ctx)
636
+ else:
637
+ # For regular create operations, run create hooks before DB ops
638
+ # Handle auto_now fields normally for new records
639
+ for obj in objs:
640
+ for field in model_cls._meta.local_fields:
641
+ if hasattr(field, "auto_now") and field.auto_now:
642
+ field.pre_save(obj, add=True)
643
+ elif hasattr(field, "auto_now_add") and field.auto_now_add:
644
+ if getattr(obj, field.name) is None:
645
+ field.pre_save(obj, add=True)
646
+
647
+ if not bypass_validation:
648
+ engine.run(model_cls, VALIDATE_CREATE, objs, ctx=ctx)
649
+ engine.run(model_cls, BEFORE_CREATE, objs, ctx=ctx)
526
650
  else:
527
651
  ctx = HookContext(model_cls, bypass_hooks=True) # Pass bypass_hooks
528
652
  logger.debug("bulk_create bypassed hooks")
@@ -557,9 +681,70 @@ class HookQuerySetMixin:
557
681
  unique_fields=unique_fields,
558
682
  )
559
683
 
560
- # Fire AFTER_CREATE hooks
684
+ # Fire AFTER hooks
561
685
  if not bypass_hooks:
562
- engine.run(model_cls, AFTER_CREATE, objs, ctx=ctx)
686
+ if update_conflicts and unique_fields:
687
+ # For upsert operations, we need to determine which records were actually created vs updated
688
+ # Use the same logic as before to separate records
689
+ existing_records = []
690
+ new_records = []
691
+
692
+ # Build a filter to check which records already exist
693
+ unique_values = []
694
+ for obj in objs:
695
+ unique_value = {}
696
+ for field_name in unique_fields:
697
+ if hasattr(obj, field_name):
698
+ unique_value[field_name] = getattr(obj, field_name)
699
+ if unique_value:
700
+ unique_values.append(unique_value)
701
+
702
+ if unique_values:
703
+ # Query the database to see which records already exist
704
+ from django.db.models import Q
705
+ existing_filters = Q()
706
+ for unique_value in unique_values:
707
+ filter_kwargs = {}
708
+ for field_name, value in unique_value.items():
709
+ filter_kwargs[field_name] = value
710
+ existing_filters |= Q(**filter_kwargs)
711
+
712
+ existing_pks = set(
713
+ model_cls.objects.filter(existing_filters).values_list('pk', flat=True)
714
+ )
715
+
716
+ # Separate records based on whether they already exist
717
+ for obj in objs:
718
+ obj_unique_value = {}
719
+ for field_name in unique_fields:
720
+ if hasattr(obj, field_name):
721
+ obj_unique_value[field_name] = getattr(obj, field_name)
722
+
723
+ # Check if this record already exists
724
+ if obj_unique_value:
725
+ existing_q = Q()
726
+ for field_name, value in obj_unique_value.items():
727
+ existing_q &= Q(**{field_name: value})
728
+
729
+ if model_cls.objects.filter(existing_q).exists():
730
+ existing_records.append(obj)
731
+ else:
732
+ new_records.append(obj)
733
+ else:
734
+ # If we can't determine uniqueness, treat as new
735
+ new_records.append(obj)
736
+ else:
737
+ # If no unique fields, treat all as new
738
+ new_records = objs
739
+
740
+ # Run appropriate AFTER hooks based on what actually happened
741
+ if new_records:
742
+ engine.run(model_cls, AFTER_CREATE, new_records, ctx=ctx)
743
+ if existing_records:
744
+ engine.run(model_cls, AFTER_UPDATE, existing_records, ctx=ctx)
745
+ else:
746
+ # For regular create operations, run create hooks after DB ops
747
+ engine.run(model_cls, AFTER_CREATE, objs, ctx=ctx)
563
748
 
564
749
  return result
565
750
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "django-bulk-hooks"
3
- version = "0.1.251"
3
+ version = "0.1.253"
4
4
  description = "Hook-style hooks for Django bulk operations like bulk_create and bulk_update."
5
5
  authors = ["Konrad Beck <konrad.beck@merchantcapital.co.za>"]
6
6
  readme = "README.md"