django-bulk-hooks 0.1.260__tar.gz → 0.1.262__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.260 → django_bulk_hooks-0.1.262}/PKG-INFO +1 -1
  2. {django_bulk_hooks-0.1.260 → django_bulk_hooks-0.1.262}/django_bulk_hooks/queryset.py +64 -43
  3. {django_bulk_hooks-0.1.260 → django_bulk_hooks-0.1.262}/pyproject.toml +1 -1
  4. {django_bulk_hooks-0.1.260 → django_bulk_hooks-0.1.262}/LICENSE +0 -0
  5. {django_bulk_hooks-0.1.260 → django_bulk_hooks-0.1.262}/README.md +0 -0
  6. {django_bulk_hooks-0.1.260 → django_bulk_hooks-0.1.262}/django_bulk_hooks/__init__.py +0 -0
  7. {django_bulk_hooks-0.1.260 → django_bulk_hooks-0.1.262}/django_bulk_hooks/conditions.py +0 -0
  8. {django_bulk_hooks-0.1.260 → django_bulk_hooks-0.1.262}/django_bulk_hooks/constants.py +0 -0
  9. {django_bulk_hooks-0.1.260 → django_bulk_hooks-0.1.262}/django_bulk_hooks/context.py +0 -0
  10. {django_bulk_hooks-0.1.260 → django_bulk_hooks-0.1.262}/django_bulk_hooks/decorators.py +0 -0
  11. {django_bulk_hooks-0.1.260 → django_bulk_hooks-0.1.262}/django_bulk_hooks/engine.py +0 -0
  12. {django_bulk_hooks-0.1.260 → django_bulk_hooks-0.1.262}/django_bulk_hooks/enums.py +0 -0
  13. {django_bulk_hooks-0.1.260 → django_bulk_hooks-0.1.262}/django_bulk_hooks/handler.py +0 -0
  14. {django_bulk_hooks-0.1.260 → django_bulk_hooks-0.1.262}/django_bulk_hooks/manager.py +0 -0
  15. {django_bulk_hooks-0.1.260 → django_bulk_hooks-0.1.262}/django_bulk_hooks/models.py +0 -0
  16. {django_bulk_hooks-0.1.260 → django_bulk_hooks-0.1.262}/django_bulk_hooks/priority.py +0 -0
  17. {django_bulk_hooks-0.1.260 → django_bulk_hooks-0.1.262}/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.260
3
+ Version: 0.1.262
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
@@ -648,38 +648,38 @@ class HookQuerySetMixin:
648
648
 
649
649
  # Remove duplicate code since we're now handling this above
650
650
 
651
- # CRITICAL: Exclude auto_now fields from update_fields for existing records
652
- # This prevents Django from including them in the ON CONFLICT DO UPDATE clause
651
+ # CRITICAL: Handle auto_now fields intelligently for existing records
652
+ # We need to exclude them from Django's ON CONFLICT DO UPDATE clause to prevent
653
+ # Django's default behavior, but still ensure they get updated via pre_save
653
654
  if existing_records and update_fields:
654
655
  logger.debug(f"Processing {len(existing_records)} existing records with update_fields: {update_fields}")
655
-
656
- # Identify auto_now fields that should be excluded from updates
657
- auto_now_fields_to_exclude = set()
656
+
657
+ # Identify auto_now fields
658
+ auto_now_fields = set()
658
659
  for field in model_cls._meta.local_fields:
659
660
  if hasattr(field, "auto_now") and field.auto_now:
660
- auto_now_fields_to_exclude.add(field.name)
661
-
662
- logger.debug(f"Found auto_now fields: {auto_now_fields_to_exclude}")
663
-
664
- # Filter out auto_now fields from update_fields for existing records
665
- if auto_now_fields_to_exclude:
666
- # Create a filtered version of update_fields that excludes auto_now fields
667
- filtered_update_fields = [f for f in update_fields if f not in auto_now_fields_to_exclude]
668
-
669
- logger.debug(f"Filtered update_fields: {filtered_update_fields}")
670
- logger.debug(f"Excluded auto_now fields: {auto_now_fields_to_exclude}")
671
-
672
- # Store the original update_fields to restore later
661
+ auto_now_fields.add(field.name)
662
+
663
+ logger.debug(f"Found auto_now fields: {auto_now_fields}")
664
+
665
+ if auto_now_fields:
666
+ # Store original update_fields and auto_now fields for later restoration
673
667
  ctx.original_update_fields = update_fields
674
- ctx.auto_now_fields_excluded = auto_now_fields_to_exclude
675
-
676
- # Use the filtered update_fields for the database operation
677
- # This prevents Django from overwriting the timestamps during upsert
668
+ ctx.auto_now_fields = auto_now_fields
669
+
670
+ # Filter out auto_now fields from update_fields for the database operation
671
+ # This prevents Django from including them in ON CONFLICT DO UPDATE
672
+ filtered_update_fields = [f for f in update_fields if f not in auto_now_fields]
673
+
674
+ logger.debug(f"Filtered update_fields: {filtered_update_fields}")
675
+ logger.debug(f"Excluded auto_now fields: {auto_now_fields}")
676
+
677
+ # Use filtered update_fields for Django's bulk_create operation
678
678
  update_fields = filtered_update_fields
679
-
679
+
680
680
  logger.debug(f"Final update_fields for DB operation: {update_fields}")
681
681
  else:
682
- logger.debug("No auto_now fields found to exclude")
682
+ logger.debug("No auto_now fields found to handle")
683
683
  else:
684
684
  logger.debug(f"No existing records or update_fields to process. existing_records: {len(existing_records) if existing_records else 0}, update_fields: {update_fields}")
685
685
 
@@ -749,14 +749,32 @@ class HookQuerySetMixin:
749
749
  # Fire AFTER hooks
750
750
  if not bypass_hooks:
751
751
  if update_conflicts and unique_fields:
752
+ # Handle auto_now fields that were excluded from the main update
753
+ if hasattr(ctx, 'auto_now_fields') and existing_records:
754
+ logger.debug(f"Performing separate update for auto_now fields: {ctx.auto_now_fields}")
755
+
756
+ # Perform a separate bulk_update for the auto_now fields that were set via pre_save
757
+ # This ensures they get saved to the database even though they were excluded from the main upsert
758
+ try:
759
+ # Use Django's base manager to bypass hooks and ensure the update happens
760
+ base_manager = model_cls._base_manager
761
+ auto_now_update_result = base_manager.bulk_update(
762
+ existing_records, list(ctx.auto_now_fields)
763
+ )
764
+ logger.debug(f"Auto_now fields update completed with result: {auto_now_update_result}")
765
+ except Exception as e:
766
+ logger.error(f"Failed to update auto_now fields: {e}")
767
+ # Don't raise the exception - the main operation succeeded
768
+
752
769
  # Restore original update_fields if we modified them
753
770
  if hasattr(ctx, 'original_update_fields'):
754
771
  logger.debug(f"Restoring original update_fields: {ctx.original_update_fields}")
755
772
  update_fields = ctx.original_update_fields
756
773
  delattr(ctx, 'original_update_fields')
757
- delattr(ctx, 'auto_now_fields_excluded')
774
+ if hasattr(ctx, 'auto_now_fields'):
775
+ delattr(ctx, 'auto_now_fields')
758
776
  logger.debug(f"Restored update_fields: {update_fields}")
759
-
777
+
760
778
  # For upsert operations, reuse the existing/new records determination from BEFORE hooks
761
779
  # This avoids duplicate queries and improves performance
762
780
  if hasattr(ctx, 'upsert_existing_records') and hasattr(ctx, 'upsert_new_records'):
@@ -871,6 +889,7 @@ class HookQuerySetMixin:
871
889
  # Handle auto_now fields like Django's update_or_create does
872
890
  fields_set = set(fields)
873
891
  pk_fields = model_cls._meta.pk_fields
892
+ pk_field_names = [f.name for f in pk_fields]
874
893
  auto_now_fields = []
875
894
  custom_update_fields = [] # Fields that need pre_save() called on update
876
895
  logger.debug(f"Checking for auto_now and custom update fields in {model_cls.__name__}")
@@ -880,7 +899,7 @@ class HookQuerySetMixin:
880
899
  if hasattr(field, "auto_now") and field.auto_now:
881
900
  logger.debug(f"Found auto_now field: {field.name}")
882
901
  print(f"DEBUG: Found auto_now field: {field.name}")
883
- if field.name not in fields_set and field.name not in pk_fields:
902
+ if field.name not in fields_set and field.name not in pk_field_names:
884
903
  fields_set.add(field.name)
885
904
  if field.name != field.attname:
886
905
  fields_set.add(field.attname)
@@ -895,7 +914,7 @@ class HookQuerySetMixin:
895
914
  # Check for custom fields that might need pre_save() on update (like CurrentUserField)
896
915
  elif hasattr(field, 'pre_save'):
897
916
  # Only call pre_save on fields that aren't already being updated
898
- if field.name not in fields_set and field.name not in pk_fields:
917
+ if field.name not in fields_set and field.name not in pk_field_names:
899
918
  custom_update_fields.append(field)
900
919
  logger.debug(f"Found custom field with pre_save: {field.name}")
901
920
  print(f"DEBUG: Found custom field with pre_save: {field.name}")
@@ -927,8 +946,8 @@ class HookQuerySetMixin:
927
946
  # Only update the field if pre_save returned a new value
928
947
  if new_value is not None:
929
948
  setattr(obj, field.name, new_value)
930
- # Add this field to the update fields if it's not already there
931
- if field.name not in fields_set:
949
+ # Add this field to the update fields if it's not already there and not a primary key
950
+ if field.name not in fields_set and field.name not in pk_field_names:
932
951
  fields_set.add(field.name)
933
952
  fields.append(field.name)
934
953
  logger.debug(f"Custom field {field.name} updated via pre_save() for object {obj.pk}")
@@ -1291,13 +1310,14 @@ class HookQuerySetMixin:
1291
1310
 
1292
1311
  return child_obj
1293
1312
 
1294
- def _mti_bulk_update(self, objs, fields, **kwargs):
1313
+ def _mti_bulk_update(self, objs, fields, field_groups=None, inheritance_chain=None, **kwargs):
1295
1314
  """
1296
1315
  Custom bulk update implementation for MTI models.
1297
1316
  Updates each table in the inheritance chain efficiently using Django's batch_size.
1298
1317
  """
1299
1318
  model_cls = self.model
1300
- inheritance_chain = self._get_inheritance_chain()
1319
+ if inheritance_chain is None:
1320
+ inheritance_chain = self._get_inheritance_chain()
1301
1321
 
1302
1322
  # Remove custom hook kwargs before passing to Django internals
1303
1323
  django_kwargs = {
@@ -1341,17 +1361,18 @@ class HookQuerySetMixin:
1341
1361
  # Add custom fields that were updated to the fields list
1342
1362
  all_fields = list(fields) + list(auto_now_fields) + custom_update_fields
1343
1363
 
1344
- # Group fields by model in the inheritance chain
1345
- field_groups = {}
1346
- for field_name in all_fields:
1347
- field = model_cls._meta.get_field(field_name)
1348
- # Find which model in the inheritance chain this field belongs to
1349
- for model in inheritance_chain:
1350
- if field in model._meta.local_fields:
1351
- if model not in field_groups:
1352
- field_groups[model] = []
1353
- field_groups[model].append(field_name)
1354
- break
1364
+ # Group fields by model in the inheritance chain (if not provided)
1365
+ if field_groups is None:
1366
+ field_groups = {}
1367
+ for field_name in all_fields:
1368
+ field = model_cls._meta.get_field(field_name)
1369
+ # Find which model in the inheritance chain this field belongs to
1370
+ for model in inheritance_chain:
1371
+ if field in model._meta.local_fields:
1372
+ if model not in field_groups:
1373
+ field_groups[model] = []
1374
+ field_groups[model].append(field_name)
1375
+ break
1355
1376
 
1356
1377
  # Process in batches
1357
1378
  batch_size = django_kwargs.get("batch_size") or len(objs)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "django-bulk-hooks"
3
- version = "0.1.260"
3
+ version = "0.1.262"
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"