Просмотр исходного кода

20911 Fix sorting in dropdown (#21101)

* Fix TomSelect dropdown ordering

* cleanup

* cleanup

* cleanup

* use correct node version

* change ordering field, remove front-end changes

* rebuild tree after rename

* add migration

* fix migration

* fix migration

* fix migration

* fix migration

* fix migration

* cleanup

* use bulk_update and rebuild

* use bulk_update and rebuild

* cleanup

* fix csv import

* Review feedback

* Review feedback

* fix dropdown sorting

* fix ordering

* review feedback

* review feedback
Arthur Hanson 15 часов назад
Родитель
Сommit
915ac90119

+ 1 - 1
netbox/dcim/forms/model_forms.py

@@ -755,7 +755,7 @@ class ModuleForm(ModuleCommonForm, PrimaryModelForm):
         label=_('Module bay'),
         queryset=ModuleBay.objects.all(),
         query_params={
-            'device_id': '$device'
+            'device_id': '$device',
         },
         context={
             'disabled': 'installed_module',

+ 32 - 0
netbox/dcim/migrations/0226_modulebay_rebuild_tree.py

@@ -0,0 +1,32 @@
+import mptt.managers
+import mptt.models
+from django.db import migrations
+
+
+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 = ('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', '0226_add_mptt_tree_indexes'),
+    ]
+
+    operations = [
+        migrations.RunPython(code=rebuild_mptt, reverse_code=migrations.RunPython.noop),
+    ]

+ 1 - 1
netbox/dcim/models/device_components.py

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

+ 11 - 2
netbox/dcim/models/modules.py

@@ -5,6 +5,7 @@ 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 create_port_mappings, update_interface_bridges
@@ -332,7 +333,8 @@ class Module(TrackingModelMixin, PrimaryModel, ConfigContextModel):
                 component._location = self.device.location
                 component._rack = self.device.rack
 
-            if component_model is not ModuleBay:
+            # we handle create and update separately - this is for create
+            if not issubclass(component_model, MPTTModel):
                 component_model.objects.bulk_create(create_instances)
                 # Emit the post_save signal for each newly created object
                 for component in create_instances:
@@ -345,11 +347,13 @@ class Module(TrackingModelMixin, PrimaryModel, ConfigContextModel):
                         update_fields=None
                     )
             else:
-                # ModuleBays must be saved individually for MPTT
+                # MPTT models must be saved individually to maintain tree structure
                 for instance in create_instances:
                     instance.save()
 
             update_fields = ['module']
+
+            # we handle create and update separately - this is for update
             component_model.objects.bulk_update(update_instances, update_fields)
             # Emit the post_save signal for each updated object
             for component in update_instances:
@@ -362,7 +366,12 @@ 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()
+
         # Replicate any front/rear port mappings from the ModuleType
         create_port_mappings(self.device, self.module_type, self)
+
         # Interface bridges have to be set after interface instantiation
         update_interface_bridges(self.device, self.module_type.interfacetemplates, self)

+ 45 - 25
netbox/netbox/views/generic/bulk_views.py

@@ -437,30 +437,12 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
         """
         return object_form.save()
 
-    def create_and_update_objects(self, form, request):
+    def _process_import_records(self, form, request, records, prefetched_objects):
+        """
+        Process CSV import records and save objects.
+        """
         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
 
@@ -524,6 +506,37 @@ 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)
+        else:
+            saved_objects = self._process_import_records(form, request, records, prefetched_objects)
+
+        return saved_objects
+
     #
     # Request handlers
     #
@@ -893,9 +906,16 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView):
                         renamed_pks = self._rename_objects(form, selected_objects)
 
                         if '_apply' in request.POST:
-                            for obj in selected_objects:
-                                setattr(obj, self.field_name, obj.new_name)
-                                obj.save()
+                            # 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()
+                            else:
+                                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):