django-bulk-hooks 0.1.258__py3-none-any.whl → 0.1.260__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.

Potentially problematic release.


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

@@ -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,29 +623,30 @@ 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
- # Preserve auto_now field values for existing records
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
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
- # Preserve the original updated_at timestamp
630
- setattr(obj, field.name, getattr(db_record, field.name))
645
+ field.pre_save(obj, add=False) # add=False for updates
646
+ print(f"DEBUG: Set {field.name} using pre_save for existing record {obj.pk}")
647
+ logger.debug(f"Set {field.name} using pre_save for existing record {obj.pk}")
631
648
 
632
- # CRITICAL: For existing records, we need to set updated_at to current time
633
- # since they are being updated, not just preserved
634
- if existing_records:
635
- from django.utils import timezone
636
- current_time = timezone.now()
637
- print(f"DEBUG: Setting updated_at to current time for {len(existing_records)} existing records")
638
- logger.debug(f"Setting updated_at to current time for {len(existing_records)} existing records")
639
- for obj in existing_records:
640
- for field in model_cls._meta.local_fields:
641
- if hasattr(field, "auto_now") and field.auto_now:
642
- setattr(obj, field.name, current_time)
643
- print(f"DEBUG: Set {field.name} to {current_time} for existing record {obj.pk}")
644
- logger.debug(f"Set {field.name} to {current_time} for existing record {obj.pk}")
649
+ # Remove duplicate code since we're now handling this above
645
650
 
646
651
  # CRITICAL: Exclude auto_now fields from update_fields for existing records
647
652
  # This prevents Django from including them in the ON CONFLICT DO UPDATE clause
@@ -752,59 +757,66 @@ class HookQuerySetMixin:
752
757
  delattr(ctx, 'auto_now_fields_excluded')
753
758
  logger.debug(f"Restored update_fields: {update_fields}")
754
759
 
755
- # For upsert operations, we need to determine which records were actually created vs updated
756
- # Use the same logic as before to separate records
757
- existing_records = []
758
- new_records = []
759
-
760
- # Build a filter to check which records already exist
761
- unique_values = []
762
- for obj in objs:
763
- unique_value = {}
764
- for field_name in unique_fields:
765
- if hasattr(obj, field_name):
766
- unique_value[field_name] = getattr(obj, field_name)
767
- if unique_value:
768
- unique_values.append(unique_value)
769
-
770
- if unique_values:
771
- # Query the database to see which records already exist
772
- from django.db.models import Q
773
- existing_filters = Q()
774
- for unique_value in unique_values:
775
- filter_kwargs = {}
776
- for field_name, value in unique_value.items():
777
- filter_kwargs[field_name] = value
778
- existing_filters |= Q(**filter_kwargs)
779
-
780
- existing_pks = set(
781
- model_cls.objects.filter(existing_filters).values_list('pk', flat=True)
782
- )
783
-
784
- # 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 = []
785
774
  for obj in objs:
786
- obj_unique_value = {}
775
+ unique_value = {}
787
776
  for field_name in unique_fields:
788
777
  if hasattr(obj, field_name):
789
- obj_unique_value[field_name] = getattr(obj, field_name)
790
-
791
- # Check if this record already exists
792
- if obj_unique_value:
793
- existing_q = Q()
794
- for field_name, value in obj_unique_value.items():
795
- existing_q &= Q(**{field_name: value})
796
-
797
- if model_cls.objects.filter(existing_q).exists():
798
- 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)
799
813
  else:
814
+ # If we can't determine uniqueness, treat as new
800
815
  new_records.append(obj)
801
- else:
802
- # If we can't determine uniqueness, treat as new
803
- new_records.append(obj)
804
- else:
805
- # If no unique fields, treat all as new
806
- new_records = objs
807
-
816
+ else:
817
+ # If no unique fields, treat all as new
818
+ new_records = objs
819
+
808
820
  # Run appropriate AFTER hooks based on what actually happened
809
821
  if new_records:
810
822
  engine.run(model_cls, AFTER_CREATE, new_records, ctx=ctx)
@@ -860,7 +872,8 @@ class HookQuerySetMixin:
860
872
  fields_set = set(fields)
861
873
  pk_fields = model_cls._meta.pk_fields
862
874
  auto_now_fields = []
863
- 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__}")
864
877
  for field in model_cls._meta.local_concrete_fields:
865
878
  # Only add auto_now fields (like updated_at) that aren't already in the fields list
866
879
  # Don't include auto_now_add fields (like created_at) as they should only be set on creation
@@ -879,6 +892,13 @@ class HookQuerySetMixin:
879
892
  print(f"DEBUG: Auto_now field {field.name} already in fields list or is PK")
880
893
  elif hasattr(field, "auto_now_add") and field.auto_now_add:
881
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}")
882
902
 
883
903
  logger.debug(f"Auto_now fields detected: {auto_now_fields}")
884
904
  print(f"DEBUG: Auto_now fields detected: {auto_now_fields}")
@@ -895,6 +915,28 @@ class HookQuerySetMixin:
895
915
  setattr(obj, field_name, current_time)
896
916
  print(f"DEBUG: Set {field_name} to {current_time} for object {obj.pk}")
897
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
+
898
940
  # Handle MTI models differently
899
941
  if is_mti:
900
942
  result = self._mti_bulk_update(objs, fields, **kwargs)
@@ -1270,13 +1312,24 @@ class HookQuerySetMixin:
1270
1312
  "Inheritance chain too deep - possible infinite recursion detected"
1271
1313
  )
1272
1314
 
1273
- # Handle auto_now fields by calling pre_save on objects
1274
- # 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 = []
1275
1318
  for obj in objs:
1276
1319
  for model in inheritance_chain:
1277
1320
  for field in model._meta.local_fields:
1278
1321
  if hasattr(field, "auto_now") and field.auto_now:
1279
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}")
1280
1333
 
1281
1334
  # Add auto_now fields to the fields list so they get updated in the database
1282
1335
  auto_now_fields = set()
@@ -1285,8 +1338,8 @@ class HookQuerySetMixin:
1285
1338
  if hasattr(field, "auto_now") and field.auto_now:
1286
1339
  auto_now_fields.add(field.name)
1287
1340
 
1288
- # Combine original fields with auto_now fields
1289
- 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
1290
1343
 
1291
1344
  # Group fields by model in the inheritance chain
1292
1345
  field_groups = {}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-bulk-hooks
3
- Version: 0.1.258
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
@@ -9,9 +9,9 @@ django_bulk_hooks/handler.py,sha256=Bx-W6yyiciKMyy-BRxUt3CmRPCrX9_LhQgU-5LaJTjg,
9
9
  django_bulk_hooks/manager.py,sha256=nfWiwU5-yAoxdnQsUMohxtyCpkV0MBv6X3wmipr9eQY,3697
10
10
  django_bulk_hooks/models.py,sha256=WtSfc4GBOG_oOt8n37cVvid0MtFIGze9JYKSixil2y0,4370
11
11
  django_bulk_hooks/priority.py,sha256=HG_2D35nga68lBCZmSXTcplXrjFoRgZFRDOy4ROKonY,376
12
- django_bulk_hooks/queryset.py,sha256=BcO9qK2zQUYnMa5c452gYwBrWo6k1PPV4LtVcw8WpUY,71269
12
+ django_bulk_hooks/queryset.py,sha256=LZv6iS7hKb-gSmcBt0EDwu8zfGytZfnthNmji6I1QbU,75863
13
13
  django_bulk_hooks/registry.py,sha256=GRUTGVQEO2sdkC9OaZ9Q3U7mM-3Ix83uTyvrlTtpatw,1317
14
- django_bulk_hooks-0.1.258.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
15
- django_bulk_hooks-0.1.258.dist-info/METADATA,sha256=tEc9hgkKa11e6B0qX6c1dAhDpvsUNgoGD5FD6JuEupQ,9061
16
- django_bulk_hooks-0.1.258.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
17
- django_bulk_hooks-0.1.258.dist-info/RECORD,,
14
+ django_bulk_hooks-0.1.260.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
15
+ django_bulk_hooks-0.1.260.dist-info/METADATA,sha256=bUETT2dh4Zg_1wiLGwl2TLySV2Hq4ubvEej7Xxk0DjI,9061
16
+ django_bulk_hooks-0.1.260.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
17
+ django_bulk_hooks-0.1.260.dist-info/RECORD,,