Compare commits

...

4 Commits

Author SHA1 Message Date
Vincent Simonin
b0f7024dcb Merge 605c61ef5b into f0507d00bf 2025-12-10 16:05:56 +09:00
github-actions
f0507d00bf Update source translation strings
Some checks failed
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Has been cancelled
2025-12-10 05:02:48 +00:00
Arthur Hanson
77b389f105 Fixes #20873: fix webhooks with image fields (#20955) 2025-12-09 22:06:11 -06:00
Vincent Simonin
605c61ef5b Fix on delete cascade entity order
Since [#20708](https://github.com/netbox-community/netbox/pull/20708)
relation with a on delete RESTRICT are not deleted in the proper order.
Then the error `violate not-null constraint` occurs and breaks the
delete cascade feature.
2025-12-08 17:06:13 +01:00
4 changed files with 369 additions and 352 deletions

View File

@@ -3,7 +3,7 @@ from threading import local
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db.models import CASCADE from django.db.models import CASCADE, RESTRICT
from django.db.models.fields.reverse_related import ManyToManyRel, ManyToOneRel from django.db.models.fields.reverse_related import ManyToManyRel, ManyToOneRel
from django.db.models.signals import m2m_changed, post_migrate, post_save, pre_delete from django.db.models.signals import m2m_changed, post_migrate, post_save, pre_delete
from django.dispatch import receiver, Signal from django.dispatch import receiver, Signal
@@ -47,6 +47,7 @@ clear_events = Signal()
# Object types # Object types
# #
@receiver(post_migrate) @receiver(post_migrate)
def update_object_types(sender, **kwargs): def update_object_types(sender, **kwargs):
""" """
@@ -133,7 +134,7 @@ def handle_changed_object(sender, instance, **kwargs):
prev_change := ObjectChange.objects.filter( prev_change := ObjectChange.objects.filter(
changed_object_type=ContentType.objects.get_for_model(instance), changed_object_type=ContentType.objects.get_for_model(instance),
changed_object_id=instance.pk, changed_object_id=instance.pk,
request_id=request.id request_id=request.id,
).first() ).first()
): ):
prev_change.postchange_data = objectchange.postchange_data prev_change.postchange_data = objectchange.postchange_data
@@ -172,9 +173,7 @@ def handle_deleted_object(sender, instance, **kwargs):
try: try:
run_validators(instance, validators) run_validators(instance, validators)
except ValidationError as e: except ValidationError as e:
raise AbortRequest( raise AbortRequest(_("Deletion is prevented by a protection rule: {message}").format(message=e))
_("Deletion is prevented by a protection rule: {message}").format(message=e)
)
# Get the current request, or bail if not set # Get the current request, or bail if not set
request = current_request.get() request = current_request.get()
@@ -221,7 +220,12 @@ def handle_deleted_object(sender, instance, **kwargs):
obj.snapshot() # Ensure the change record includes the "before" state obj.snapshot() # Ensure the change record includes the "before" state
if type(relation) is ManyToManyRel: if type(relation) is ManyToManyRel:
getattr(obj, related_field_name).remove(instance) getattr(obj, related_field_name).remove(instance)
elif type(relation) is ManyToOneRel and relation.null and relation.on_delete is not CASCADE: elif (
type(relation) is ManyToOneRel
and relation.null
and relation.on_delete is not CASCADE
and relation.on_delete is not RESTRICT
):
setattr(obj, related_field_name, None) setattr(obj, related_field_name, None)
obj.save() obj.save()
@@ -256,6 +260,7 @@ def clear_events_queue(sender, **kwargs):
# DataSource handlers # DataSource handlers
# #
@receiver(post_save, sender=DataSource) @receiver(post_save, sender=DataSource)
def enqueue_sync_job(instance, created, **kwargs): def enqueue_sync_job(instance, created, **kwargs):
""" """
@@ -267,9 +272,10 @@ def enqueue_sync_job(instance, created, **kwargs):
SyncDataSourceJob.enqueue_once(instance, interval=instance.sync_interval) SyncDataSourceJob.enqueue_once(instance, interval=instance.sync_interval)
elif not created: elif not created:
# Delete any previously scheduled recurring jobs for this DataSource # Delete any previously scheduled recurring jobs for this DataSource
for job in SyncDataSourceJob.get_jobs(instance).defer('data').filter( for job in (
interval__isnull=False, SyncDataSourceJob.get_jobs(instance)
status=JobStatusChoices.STATUS_SCHEDULED .defer('data')
.filter(interval__isnull=False, status=JobStatusChoices.STATUS_SCHEDULED)
): ):
# Call delete() per instance to ensure the associated background task is deleted as well # Call delete() per instance to ensure the associated background task is deleted as well
job.delete() job.delete()

View File

@@ -119,7 +119,9 @@ def process_event_rules(event_rules, object_type, event_type, data, username=Non
if snapshots: if snapshots:
params["snapshots"] = snapshots params["snapshots"] = snapshots
if request: if request:
params["request"] = copy_safe_request(request) # Exclude FILES - webhooks don't need uploaded files,
# which can cause pickle errors with Pillow.
params["request"] = copy_safe_request(request, include_files=False)
# Enqueue the task # Enqueue the task
rq_queue.enqueue( rq_queue.enqueue(

File diff suppressed because it is too large Load Diff

View File

@@ -35,27 +35,34 @@ class NetBoxFakeRequest:
# Utility functions # Utility functions
# #
def copy_safe_request(request): def copy_safe_request(request, include_files=True):
""" """
Copy selected attributes from a request object into a new fake request object. This is needed in places where Copy selected attributes from a request object into a new fake request object. This is needed in places where
thread safe pickling of the useful request data is needed. thread safe pickling of the useful request data is needed.
Args:
request: The original request object
include_files: Whether to include request.FILES.
""" """
meta = { meta = {
k: request.META[k] k: request.META[k]
for k in HTTP_REQUEST_META_SAFE_COPY for k in HTTP_REQUEST_META_SAFE_COPY
if k in request.META and isinstance(request.META[k], str) if k in request.META and isinstance(request.META[k], str)
} }
return NetBoxFakeRequest({ data = {
'META': meta, 'META': meta,
'COOKIES': request.COOKIES, 'COOKIES': request.COOKIES,
'POST': request.POST, 'POST': request.POST,
'GET': request.GET, 'GET': request.GET,
'FILES': request.FILES,
'user': request.user, 'user': request.user,
'method': request.method, 'method': request.method,
'path': request.path, 'path': request.path,
'id': getattr(request, 'id', None), # UUID assigned by middleware 'id': getattr(request, 'id', None), # UUID assigned by middleware
}) }
if include_files:
data['FILES'] = request.FILES
return NetBoxFakeRequest(data)
def get_client_ip(request, additional_headers=()): def get_client_ip(request, additional_headers=()):