django-bulk-hooks 0.1.258__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.
- {django_bulk_hooks-0.1.258 → django_bulk_hooks-0.1.260}/PKG-INFO +1 -1
- {django_bulk_hooks-0.1.258 → django_bulk_hooks-0.1.260}/django_bulk_hooks/queryset.py +139 -86
- {django_bulk_hooks-0.1.258 → django_bulk_hooks-0.1.260}/pyproject.toml +1 -1
- {django_bulk_hooks-0.1.258 → django_bulk_hooks-0.1.260}/LICENSE +0 -0
- {django_bulk_hooks-0.1.258 → django_bulk_hooks-0.1.260}/README.md +0 -0
- {django_bulk_hooks-0.1.258 → django_bulk_hooks-0.1.260}/django_bulk_hooks/__init__.py +0 -0
- {django_bulk_hooks-0.1.258 → django_bulk_hooks-0.1.260}/django_bulk_hooks/conditions.py +0 -0
- {django_bulk_hooks-0.1.258 → django_bulk_hooks-0.1.260}/django_bulk_hooks/constants.py +0 -0
- {django_bulk_hooks-0.1.258 → django_bulk_hooks-0.1.260}/django_bulk_hooks/context.py +0 -0
- {django_bulk_hooks-0.1.258 → django_bulk_hooks-0.1.260}/django_bulk_hooks/decorators.py +0 -0
- {django_bulk_hooks-0.1.258 → django_bulk_hooks-0.1.260}/django_bulk_hooks/engine.py +0 -0
- {django_bulk_hooks-0.1.258 → django_bulk_hooks-0.1.260}/django_bulk_hooks/enums.py +0 -0
- {django_bulk_hooks-0.1.258 → django_bulk_hooks-0.1.260}/django_bulk_hooks/handler.py +0 -0
- {django_bulk_hooks-0.1.258 → django_bulk_hooks-0.1.260}/django_bulk_hooks/manager.py +0 -0
- {django_bulk_hooks-0.1.258 → django_bulk_hooks-0.1.260}/django_bulk_hooks/models.py +0 -0
- {django_bulk_hooks-0.1.258 → django_bulk_hooks-0.1.260}/django_bulk_hooks/priority.py +0 -0
- {django_bulk_hooks-0.1.258 → django_bulk_hooks-0.1.260}/django_bulk_hooks/registry.py +0 -0
|
@@ -89,7 +89,7 @@ class HookQuerySetMixin:
|
|
|
89
89
|
try:
|
|
90
90
|
from django.db.models import Subquery
|
|
91
91
|
|
|
92
|
-
logger.debug(
|
|
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
|
-
|
|
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
|
-
|
|
556
|
-
|
|
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
|
-
|
|
569
|
-
for field_name
|
|
570
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
630
|
-
|
|
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
|
-
#
|
|
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,
|
|
756
|
-
#
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
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
|
-
|
|
775
|
+
unique_value = {}
|
|
787
776
|
for field_name in unique_fields:
|
|
788
777
|
if hasattr(obj, field_name):
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
[tool.poetry]
|
|
2
2
|
name = "django-bulk-hooks"
|
|
3
|
-
version = "0.1.
|
|
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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|