Compare commits

..

1 Commits

Author SHA1 Message Date
Jeremy Stretch
75193ee5fc Initial work on #21259 2026-01-23 16:28:07 -05:00
7 changed files with 44 additions and 90 deletions

View File

@@ -9,6 +9,7 @@ from django.db import connection, models
from django.db.models import Q
from django.utils.translation import gettext as _
from netbox.context import object_types_cache
from netbox.plugins import PluginConfig
from netbox.registry import registry
from utilities.string import title
@@ -70,6 +71,12 @@ class ObjectTypeManager(models.Manager):
"""
from netbox.models.features import get_model_features, model_is_public
# Check the request cache before hitting the database
cache = object_types_cache.get()
if cache is not None:
if ot := cache.get((model._meta.model, for_concrete_model)):
return ot
# TODO: Remove this in NetBox v5.0
# If the ObjectType table has not yet been provisioned (e.g. because we're in a pre-v4.4 migration),
# fall back to ContentType.
@@ -96,6 +103,10 @@ class ObjectTypeManager(models.Manager):
features=get_model_features(model),
)[0]
# Populate the request cache to avoid redundant lookups
if cache is not None:
cache[(model._meta.model, for_concrete_model)] = ot
return ot
def get_for_models(self, *models, for_concrete_models=True):

View File

@@ -1,32 +0,0 @@
from django.db import migrations
import mptt.managers
import mptt.models
def rebuild_mptt(apps, schema_editor):
"""
Rebuild the MPTT tree for ModuleBay to apply new ordering.
"""
ModuleBay = apps.get_model('dcim', 'ModuleBay')
# Set MPTTMeta with the correct order_insertion_by
class MPTTMeta:
order_insertion_by = ('module', 'name',)
ModuleBay.MPTTMeta = MPTTMeta
ModuleBay._mptt_meta = mptt.models.MPTTOptions(MPTTMeta)
manager = mptt.managers.TreeManager()
manager.model = ModuleBay
manager.contribute_to_class(ModuleBay, 'objects')
manager.rebuild()
class Migration(migrations.Migration):
dependencies = [
('dcim', '0225_gfk_indexes'),
]
operations = [
migrations.RunPython(code=rebuild_mptt, reverse_code=migrations.RunPython.noop),
]

View File

@@ -1273,7 +1273,7 @@ class ModuleBay(ModularComponentModel, TrackingModelMixin, MPTTModel):
verbose_name_plural = _('module bays')
class MPTTMeta:
order_insertion_by = ('module', 'name',)
order_insertion_by = ('module',)
def clean(self):
super().clean()

View File

@@ -5,7 +5,6 @@ from django.db import models
from django.db.models.signals import post_save
from django.utils.translation import gettext_lazy as _
from jsonschema.exceptions import ValidationError as JSONValidationError
from mptt.models import MPTTModel
from dcim.choices import *
from dcim.utils import update_interface_bridges
@@ -330,7 +329,7 @@ class Module(TrackingModelMixin, PrimaryModel, ConfigContextModel):
component._location = self.device.location
component._rack = self.device.rack
if not issubclass(component_model, MPTTModel):
if component_model is not ModuleBay:
component_model.objects.bulk_create(create_instances)
# Emit the post_save signal for each newly created object
for component in create_instances:
@@ -343,12 +342,11 @@ class Module(TrackingModelMixin, PrimaryModel, ConfigContextModel):
update_fields=None
)
else:
# MPTT models must be saved individually to maintain tree structure
# ModuleBays must be saved individually for MPTT
for instance in create_instances:
instance.save()
update_fields = ['module']
component_model.objects.bulk_update(update_instances, update_fields)
# Emit the post_save signal for each updated object
for component in update_instances:
@@ -361,9 +359,5 @@ class Module(TrackingModelMixin, PrimaryModel, ConfigContextModel):
update_fields=update_fields
)
# Rebuild MPTT tree if needed (bulk_update bypasses model save)
if issubclass(component_model, MPTTModel) and update_instances:
component_model.objects.rebuild()
# Interface bridges have to be set after interface instantiation
update_interface_bridges(self.device, self.module_type.interfacetemplates, self)

View File

@@ -3,8 +3,10 @@ from contextvars import ContextVar
__all__ = (
'current_request',
'events_queue',
'object_types_cache',
)
current_request = ContextVar('current_request', default=None)
events_queue = ContextVar('events_queue', default=dict())
object_types_cache = ContextVar('object_types_cache', default=None)

View File

@@ -1,6 +1,6 @@
from contextlib import contextmanager
from netbox.context import current_request, events_queue
from netbox.context import current_request, events_queue, object_types_cache
from netbox.utils import register_request_processor
from extras.events import flush_events
@@ -16,6 +16,7 @@ def event_tracking(request):
"""
current_request.set(request)
events_queue.set({})
object_types_cache.set({})
yield
@@ -26,3 +27,4 @@ def event_tracking(request):
# Clear context vars
current_request.set(None)
events_queue.set({})
object_types_cache.set(None)

View File

@@ -438,12 +438,30 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
"""
return object_form.save()
def _process_import_records(self, form, request, records, prefetched_objects):
"""
Process CSV import records and save objects.
"""
def create_and_update_objects(self, form, request):
saved_objects = []
records = list(form.cleaned_data['data'])
# Prefetch objects to be updated, if any
prefetch_ids = [int(record['id']) for record in records if record.get('id')]
# check for duplicate IDs
duplicate_pks = [pk for pk, count in Counter(prefetch_ids).items() if count > 1]
if duplicate_pks:
error_msg = _(
"Duplicate objects found: {model} with ID(s) {ids} appears multiple times"
).format(
model=title(self.queryset.model._meta.verbose_name),
ids=', '.join(str(pk) for pk in sorted(duplicate_pks))
)
raise ValidationError(error_msg)
prefetched_objects = {
obj.pk: obj
for obj in self.queryset.model.objects.filter(id__in=prefetch_ids)
} if prefetch_ids else {}
for i, record in enumerate(records, start=1):
object_id = int(record.pop('id')) if record.get('id') else None
@@ -508,38 +526,6 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
return saved_objects
def create_and_update_objects(self, form, request):
records = list(form.cleaned_data['data'])
# Prefetch objects to be updated, if any
prefetch_ids = [int(record['id']) for record in records if record.get('id')]
# check for duplicate IDs
duplicate_pks = [pk for pk, count in Counter(prefetch_ids).items() if count > 1]
if duplicate_pks:
error_msg = _(
"Duplicate objects found: {model} with ID(s) {ids} appears multiple times"
).format(
model=title(self.queryset.model._meta.verbose_name),
ids=', '.join(str(pk) for pk in sorted(duplicate_pks))
)
raise ValidationError(error_msg)
prefetched_objects = {
obj.pk: obj
for obj in self.queryset.model.objects.filter(id__in=prefetch_ids)
} if prefetch_ids else {}
# For MPTT models, delay tree updates until all saves are complete
if issubclass(self.queryset.model, MPTTModel):
with self.queryset.model.objects.delay_mptt_updates():
saved_objects = self._process_import_records(form, request, records, prefetched_objects)
self.queryset.model.objects.rebuild()
else:
saved_objects = self._process_import_records(form, request, records, prefetched_objects)
return saved_objects
#
# Request handlers
#
@@ -909,18 +895,9 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView):
renamed_pks = self._rename_objects(form, selected_objects)
if '_apply' in request.POST:
# For MPTT models, delay tree updates until all saves are complete
if issubclass(self.queryset.model, MPTTModel):
with self.queryset.model.objects.delay_mptt_updates():
for obj in selected_objects:
setattr(obj, self.field_name, obj.new_name)
obj.save()
self.queryset.model.objects.rebuild()
else:
for obj in selected_objects:
setattr(obj, self.field_name, obj.new_name)
obj.save()
for obj in selected_objects:
setattr(obj, self.field_name, obj.new_name)
obj.save()
# Enforce constrained permissions
if self.queryset.filter(pk__in=renamed_pks).count() != len(selected_objects):