django-bulk-hooks 0.1.259__tar.gz → 0.1.260__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.259 → django_bulk_hooks-0.1.260}/PKG-INFO +1 -1
  2. {django_bulk_hooks-0.1.259 → django_bulk_hooks-0.1.260}/django_bulk_hooks/queryset.py +136 -72
  3. {django_bulk_hooks-0.1.259 → django_bulk_hooks-0.1.260}/pyproject.toml +1 -1
  4. {django_bulk_hooks-0.1.259 → django_bulk_hooks-0.1.260}/LICENSE +0 -0
  5. {django_bulk_hooks-0.1.259 → django_bulk_hooks-0.1.260}/README.md +0 -0
  6. {django_bulk_hooks-0.1.259 → django_bulk_hooks-0.1.260}/django_bulk_hooks/__init__.py +0 -0
  7. {django_bulk_hooks-0.1.259 → django_bulk_hooks-0.1.260}/django_bulk_hooks/conditions.py +0 -0
  8. {django_bulk_hooks-0.1.259 → django_bulk_hooks-0.1.260}/django_bulk_hooks/constants.py +0 -0
  9. {django_bulk_hooks-0.1.259 → django_bulk_hooks-0.1.260}/django_bulk_hooks/context.py +0 -0
  10. {django_bulk_hooks-0.1.259 → django_bulk_hooks-0.1.260}/django_bulk_hooks/decorators.py +0 -0
  11. {django_bulk_hooks-0.1.259 → django_bulk_hooks-0.1.260}/django_bulk_hooks/engine.py +0 -0
  12. {django_bulk_hooks-0.1.259 → django_bulk_hooks-0.1.260}/django_bulk_hooks/enums.py +0 -0
  13. {django_bulk_hooks-0.1.259 → django_bulk_hooks-0.1.260}/django_bulk_hooks/handler.py +0 -0
  14. {django_bulk_hooks-0.1.259 → django_bulk_hooks-0.1.260}/django_bulk_hooks/manager.py +0 -0
  15. {django_bulk_hooks-0.1.259 → django_bulk_hooks-0.1.260}/django_bulk_hooks/models.py +0 -0
  16. {django_bulk_hooks-0.1.259 → django_bulk_hooks-0.1.260}/django_bulk_hooks/priority.py +0 -0
  17. {django_bulk_hooks-0.1.259 → django_bulk_hooks-0.1.260}/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.259
3
+ Version: 0.1.260
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
@@ -89,7 +89,7 @@ class HookQuerySetMixin:
89
89
  try:
90
90
  from django.db.models import Subquery
91
91
 
92
- logger.debug(f"Successfully imported Subquery from django.db.models")
92
+ logger.debug("Successfully imported Subquery from django.db.models")
93
93
  except ImportError as e:
94
94
  logger.error(f"Failed to import Subquery: {e}")
95
95
  raise
@@ -367,7 +367,7 @@ class HookQuerySetMixin:
367
367
 
368
368
  if has_nested_subquery:
369
369
  logger.debug(
370
- f"Expression contains Subquery, ensuring proper output_field"
370
+ "Expression contains Subquery, ensuring proper output_field"
371
371
  )
372
372
  # Try to resolve the expression to ensure it's properly formatted
373
373
  try:
@@ -531,6 +531,10 @@ class HookQuerySetMixin:
531
531
  # Check which records already exist in the database based on unique fields
532
532
  existing_records = []
533
533
  new_records = []
534
+
535
+ # Store the records for AFTER hooks to avoid duplicate queries
536
+ ctx.upsert_existing_records = existing_records
537
+ ctx.upsert_new_records = new_records
534
538
 
535
539
  # Build a filter to check which records already exist
536
540
  unique_values = []
@@ -541,9 +545,9 @@ class HookQuerySetMixin:
541
545
  unique_value[field_name] = getattr(obj, field_name)
542
546
  if unique_value:
543
547
  unique_values.append(unique_value)
544
-
548
+
545
549
  if unique_values:
546
- # Query the database to see which records already exist
550
+ # Query the database to see which records already exist - SINGLE BULK QUERY
547
551
  from django.db.models import Q
548
552
  existing_filters = Q()
549
553
  for unique_value in unique_values:
@@ -551,25 +555,25 @@ class HookQuerySetMixin:
551
555
  for field_name, value in unique_value.items():
552
556
  filter_kwargs[field_name] = value
553
557
  existing_filters |= Q(**filter_kwargs)
554
-
555
- existing_pks = set(
556
- model_cls.objects.filter(existing_filters).values_list('pk', flat=True)
557
- )
558
-
558
+
559
+ # Get all existing records in one query and create a lookup set
560
+ existing_records_lookup = set()
561
+ for existing_record in model_cls.objects.filter(existing_filters).values_list(*unique_fields):
562
+ # Convert tuple to a hashable key for lookup
563
+ existing_records_lookup.add(existing_record)
564
+
559
565
  # Separate records based on whether they already exist
560
566
  for obj in objs:
561
567
  obj_unique_value = {}
562
568
  for field_name in unique_fields:
563
569
  if hasattr(obj, field_name):
564
570
  obj_unique_value[field_name] = getattr(obj, field_name)
565
-
566
- # Check if this record already exists
571
+
572
+ # Check if this record already exists using our bulk lookup
567
573
  if obj_unique_value:
568
- existing_q = Q()
569
- for field_name, value in obj_unique_value.items():
570
- existing_q &= Q(**{field_name: value})
571
-
572
- if model_cls.objects.filter(existing_q).exists():
574
+ # Convert object values to tuple for comparison with existing records
575
+ obj_unique_tuple = tuple(obj_unique_value[field_name] for field_name in unique_fields)
576
+ if obj_unique_tuple in existing_records_lookup:
573
577
  existing_records.append(obj)
574
578
  else:
575
579
  new_records.append(obj)
@@ -579,7 +583,7 @@ class HookQuerySetMixin:
579
583
  else:
580
584
  # If no unique fields, treat all as new
581
585
  new_records = objs
582
-
586
+
583
587
  # Handle auto_now fields intelligently for upsert operations
584
588
  # Only set auto_now fields on records that will actually be created
585
589
  for obj in new_records:
@@ -619,11 +623,23 @@ class HookQuerySetMixin:
619
623
  key = tuple(getattr(db_record, field) for field in unique_fields)
620
624
  existing_db_map[key] = db_record
621
625
 
622
- # For existing records, set auto_now fields using Django's pre_save method
626
+ # For existing records, populate all fields from database and set auto_now fields
623
627
  for obj in existing_records:
624
628
  key = tuple(getattr(obj, field) for field in unique_fields)
625
629
  if key in existing_db_map:
626
- # Use Django's pre_save method to set auto_now fields (like updated_at)
630
+ db_record = existing_db_map[key]
631
+ # Copy all fields from the database record to ensure completeness
632
+ populated_fields = []
633
+ for field in model_cls._meta.local_fields:
634
+ if field.name != 'id': # Don't overwrite the ID
635
+ db_value = getattr(db_record, field.name)
636
+ if db_value is not None: # Only set non-None values
637
+ setattr(obj, field.name, db_value)
638
+ populated_fields.append(field.name)
639
+ print(f"DEBUG: Populated {len(populated_fields)} fields for existing record: {populated_fields}")
640
+ logger.debug(f"Populated {len(populated_fields)} fields for existing record: {populated_fields}")
641
+
642
+ # Now set auto_now fields using Django's pre_save method
627
643
  for field in model_cls._meta.local_fields:
628
644
  if hasattr(field, "auto_now") and field.auto_now:
629
645
  field.pre_save(obj, add=False) # add=False for updates
@@ -741,59 +757,66 @@ class HookQuerySetMixin:
741
757
  delattr(ctx, 'auto_now_fields_excluded')
742
758
  logger.debug(f"Restored update_fields: {update_fields}")
743
759
 
744
- # For upsert operations, we need to determine which records were actually created vs updated
745
- # Use the same logic as before to separate records
746
- existing_records = []
747
- new_records = []
748
-
749
- # Build a filter to check which records already exist
750
- unique_values = []
751
- for obj in objs:
752
- unique_value = {}
753
- for field_name in unique_fields:
754
- if hasattr(obj, field_name):
755
- unique_value[field_name] = getattr(obj, field_name)
756
- if unique_value:
757
- unique_values.append(unique_value)
758
-
759
- if unique_values:
760
- # Query the database to see which records already exist
761
- from django.db.models import Q
762
- existing_filters = Q()
763
- for unique_value in unique_values:
764
- filter_kwargs = {}
765
- for field_name, value in unique_value.items():
766
- filter_kwargs[field_name] = value
767
- existing_filters |= Q(**filter_kwargs)
768
-
769
- existing_pks = set(
770
- model_cls.objects.filter(existing_filters).values_list('pk', flat=True)
771
- )
772
-
773
- # Separate records based on whether they already exist
760
+ # For upsert operations, reuse the existing/new records determination from BEFORE hooks
761
+ # This avoids duplicate queries and improves performance
762
+ if hasattr(ctx, 'upsert_existing_records') and hasattr(ctx, 'upsert_new_records'):
763
+ existing_records = ctx.upsert_existing_records
764
+ new_records = ctx.upsert_new_records
765
+ logger.debug(f"Reusing upsert record classification from BEFORE hooks: {len(existing_records)} existing, {len(new_records)} new")
766
+ else:
767
+ # Fallback: determine records that actually exist after bulk operation
768
+ logger.warning("Upsert record classification not found in context, performing fallback query")
769
+ existing_records = []
770
+ new_records = []
771
+
772
+ # Build a filter to check which records now exist
773
+ unique_values = []
774
774
  for obj in objs:
775
- obj_unique_value = {}
775
+ unique_value = {}
776
776
  for field_name in unique_fields:
777
777
  if hasattr(obj, field_name):
778
- obj_unique_value[field_name] = getattr(obj, field_name)
779
-
780
- # Check if this record already exists
781
- if obj_unique_value:
782
- existing_q = Q()
783
- for field_name, value in obj_unique_value.items():
784
- existing_q &= Q(**{field_name: value})
785
-
786
- if model_cls.objects.filter(existing_q).exists():
787
- existing_records.append(obj)
778
+ unique_value[field_name] = getattr(obj, field_name)
779
+ if unique_value:
780
+ unique_values.append(unique_value)
781
+
782
+ if unique_values:
783
+ # Query the database to see which records exist after bulk operation
784
+ from django.db.models import Q
785
+ existing_filters = Q()
786
+ for unique_value in unique_values:
787
+ filter_kwargs = {}
788
+ for field_name, value in unique_value.items():
789
+ filter_kwargs[field_name] = value
790
+ existing_filters |= Q(**filter_kwargs)
791
+
792
+ # Get all existing records in one query and create a lookup set
793
+ existing_records_lookup = set()
794
+ for existing_record in model_cls.objects.filter(existing_filters).values_list(*unique_fields):
795
+ # Convert tuple to a hashable key for lookup
796
+ existing_records_lookup.add(existing_record)
797
+
798
+ # Separate records based on whether they now exist
799
+ for obj in objs:
800
+ obj_unique_value = {}
801
+ for field_name in unique_fields:
802
+ if hasattr(obj, field_name):
803
+ obj_unique_value[field_name] = getattr(obj, field_name)
804
+
805
+ # Check if this record exists using our bulk lookup
806
+ if obj_unique_value:
807
+ # Convert object values to tuple for comparison with existing records
808
+ obj_unique_tuple = tuple(obj_unique_value[field_name] for field_name in unique_fields)
809
+ if obj_unique_tuple in existing_records_lookup:
810
+ existing_records.append(obj)
811
+ else:
812
+ new_records.append(obj)
788
813
  else:
814
+ # If we can't determine uniqueness, treat as new
789
815
  new_records.append(obj)
790
- else:
791
- # If we can't determine uniqueness, treat as new
792
- new_records.append(obj)
793
- else:
794
- # If no unique fields, treat all as new
795
- new_records = objs
796
-
816
+ else:
817
+ # If no unique fields, treat all as new
818
+ new_records = objs
819
+
797
820
  # Run appropriate AFTER hooks based on what actually happened
798
821
  if new_records:
799
822
  engine.run(model_cls, AFTER_CREATE, new_records, ctx=ctx)
@@ -849,7 +872,8 @@ class HookQuerySetMixin:
849
872
  fields_set = set(fields)
850
873
  pk_fields = model_cls._meta.pk_fields
851
874
  auto_now_fields = []
852
- logger.debug(f"Checking for auto_now fields in {model_cls.__name__}")
875
+ custom_update_fields = [] # Fields that need pre_save() called on update
876
+ logger.debug(f"Checking for auto_now and custom update fields in {model_cls.__name__}")
853
877
  for field in model_cls._meta.local_concrete_fields:
854
878
  # Only add auto_now fields (like updated_at) that aren't already in the fields list
855
879
  # Don't include auto_now_add fields (like created_at) as they should only be set on creation
@@ -868,6 +892,13 @@ class HookQuerySetMixin:
868
892
  print(f"DEBUG: Auto_now field {field.name} already in fields list or is PK")
869
893
  elif hasattr(field, "auto_now_add") and field.auto_now_add:
870
894
  logger.debug(f"Found auto_now_add field: {field.name} (skipping)")
895
+ # Check for custom fields that might need pre_save() on update (like CurrentUserField)
896
+ elif hasattr(field, 'pre_save'):
897
+ # 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:
899
+ custom_update_fields.append(field)
900
+ logger.debug(f"Found custom field with pre_save: {field.name}")
901
+ print(f"DEBUG: Found custom field with pre_save: {field.name}")
871
902
 
872
903
  logger.debug(f"Auto_now fields detected: {auto_now_fields}")
873
904
  print(f"DEBUG: Auto_now fields detected: {auto_now_fields}")
@@ -884,6 +915,28 @@ class HookQuerySetMixin:
884
915
  setattr(obj, field_name, current_time)
885
916
  print(f"DEBUG: Set {field_name} to {current_time} for object {obj.pk}")
886
917
 
918
+ # Call pre_save() on custom fields that need update handling
919
+ if custom_update_fields:
920
+ logger.debug(f"Calling pre_save() on custom update fields: {[f.name for f in custom_update_fields]}")
921
+ print(f"DEBUG: Calling pre_save() on custom update fields: {[f.name for f in custom_update_fields]}")
922
+ for obj in objs:
923
+ for field in custom_update_fields:
924
+ try:
925
+ # Call pre_save with add=False to indicate this is an update
926
+ new_value = field.pre_save(obj, add=False)
927
+ # Only update the field if pre_save returned a new value
928
+ if new_value is not None:
929
+ 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:
932
+ fields_set.add(field.name)
933
+ fields.append(field.name)
934
+ logger.debug(f"Custom field {field.name} updated via pre_save() for object {obj.pk}")
935
+ print(f"DEBUG: Custom field {field.name} updated via pre_save() for object {obj.pk}")
936
+ except Exception as e:
937
+ logger.warning(f"Failed to call pre_save() on custom field {field.name}: {e}")
938
+ print(f"DEBUG: Failed to call pre_save() on custom field {field.name}: {e}")
939
+
887
940
  # Handle MTI models differently
888
941
  if is_mti:
889
942
  result = self._mti_bulk_update(objs, fields, **kwargs)
@@ -1259,13 +1312,24 @@ class HookQuerySetMixin:
1259
1312
  "Inheritance chain too deep - possible infinite recursion detected"
1260
1313
  )
1261
1314
 
1262
- # Handle auto_now fields by calling pre_save on objects
1263
- # Check all models in the inheritance chain for auto_now fields
1315
+ # Handle auto_now fields and custom fields by calling pre_save on objects
1316
+ # Check all models in the inheritance chain for auto_now and custom fields
1317
+ custom_update_fields = []
1264
1318
  for obj in objs:
1265
1319
  for model in inheritance_chain:
1266
1320
  for field in model._meta.local_fields:
1267
1321
  if hasattr(field, "auto_now") and field.auto_now:
1268
1322
  field.pre_save(obj, add=False)
1323
+ # Check for custom fields that might need pre_save() on update (like CurrentUserField)
1324
+ elif hasattr(field, 'pre_save') and field.name not in fields:
1325
+ try:
1326
+ new_value = field.pre_save(obj, add=False)
1327
+ if new_value is not None:
1328
+ setattr(obj, field.name, new_value)
1329
+ custom_update_fields.append(field.name)
1330
+ logger.debug(f"Custom field {field.name} updated via pre_save() for MTI object {obj.pk}")
1331
+ except Exception as e:
1332
+ logger.warning(f"Failed to call pre_save() on custom field {field.name} in MTI: {e}")
1269
1333
 
1270
1334
  # Add auto_now fields to the fields list so they get updated in the database
1271
1335
  auto_now_fields = set()
@@ -1274,8 +1338,8 @@ class HookQuerySetMixin:
1274
1338
  if hasattr(field, "auto_now") and field.auto_now:
1275
1339
  auto_now_fields.add(field.name)
1276
1340
 
1277
- # Combine original fields with auto_now fields
1278
- all_fields = list(fields) + list(auto_now_fields)
1341
+ # Add custom fields that were updated to the fields list
1342
+ all_fields = list(fields) + list(auto_now_fields) + custom_update_fields
1279
1343
 
1280
1344
  # Group fields by model in the inheritance chain
1281
1345
  field_groups = {}
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "django-bulk-hooks"
3
- version = "0.1.259"
3
+ version = "0.1.260"
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"