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

Closes #4837: Use dynamic form widget for relationships to MPTT objects

Jeremy Stretch 5 лет назад
Родитель
Сommit
15525392a2

+ 2 - 0
docs/release-notes/version-2.9.md

@@ -25,6 +25,7 @@ When running a report or custom script, the task is now queued for background pr
 * [#4806](https://github.com/netbox-community/netbox/issues/4806) - Add a `url` field to all API serializers
 * [#4806](https://github.com/netbox-community/netbox/issues/4806) - Add a `url` field to all API serializers
 * [#4807](https://github.com/netbox-community/netbox/issues/4807) - Add bulk edit ability for device bay templates
 * [#4807](https://github.com/netbox-community/netbox/issues/4807) - Add bulk edit ability for device bay templates
 * [#4817](https://github.com/netbox-community/netbox/issues/4817) - Standardize device/VM component `name` field to 64 characters
 * [#4817](https://github.com/netbox-community/netbox/issues/4817) - Standardize device/VM component `name` field to 64 characters
+* [#4837](https://github.com/netbox-community/netbox/issues/4837) - Use dynamic form widget for relationships to MPTT objects (e.g. regions)
 
 
 ### Configuration Changes
 ### Configuration Changes
 
 
@@ -52,6 +53,7 @@ When running a report or custom script, the task is now queued for background pr
 * extras.Report: The `failed` field has been removed. The `completed` (boolean) and `status` (string) fields have been introduced to convey the status of a report's most recent execution. Additionally, the `result` field now conveys the nested representation of a JobResult.
 * extras.Report: The `failed` field has been removed. The `completed` (boolean) and `status` (string) fields have been introduced to convey the status of a report's most recent execution. Additionally, the `result` field now conveys the nested representation of a JobResult.
 * extras.Script: Added `module` and `result` fields. The `result` field now conveys the nested representation of a JobResult.
 * extras.Script: Added `module` and `result` fields. The `result` field now conveys the nested representation of a JobResult.
 * A `url` field is now included on all object representations, identifying the unique REST API URL for each object.
 * A `url` field is now included on all object representations, identifying the unique REST API URL for each object.
+* A `_depth` field has been added to all objects which feature a self-recursive hierarchy (namely regions, rack groups, and tenant groups).
 
 
 ### Other Changes
 ### Other Changes
 
 

+ 4 - 2
netbox/dcim/api/nested_serializers.py

@@ -47,10 +47,11 @@ __all__ = [
 class NestedRegionSerializer(WritableNestedSerializer):
 class NestedRegionSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
     site_count = serializers.IntegerField(read_only=True)
     site_count = serializers.IntegerField(read_only=True)
+    _depth = serializers.IntegerField(source='level', read_only=True)
 
 
     class Meta:
     class Meta:
         model = models.Region
         model = models.Region
-        fields = ['id', 'url', 'name', 'slug', 'site_count']
+        fields = ['id', 'url', 'name', 'slug', 'site_count', '_depth']
 
 
 
 
 class NestedSiteSerializer(WritableNestedSerializer):
 class NestedSiteSerializer(WritableNestedSerializer):
@@ -68,10 +69,11 @@ class NestedSiteSerializer(WritableNestedSerializer):
 class NestedRackGroupSerializer(WritableNestedSerializer):
 class NestedRackGroupSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackgroup-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackgroup-detail')
     rack_count = serializers.IntegerField(read_only=True)
     rack_count = serializers.IntegerField(read_only=True)
+    _depth = serializers.IntegerField(source='level', read_only=True)
 
 
     class Meta:
     class Meta:
         model = models.RackGroup
         model = models.RackGroup
-        fields = ['id', 'url', 'name', 'slug', 'rack_count']
+        fields = ['id', 'url', 'name', 'slug', 'rack_count', '_depth']
 
 
 
 
 class NestedRackRoleSerializer(WritableNestedSerializer):
 class NestedRackRoleSerializer(WritableNestedSerializer):

+ 4 - 2
netbox/dcim/api/serializers.py

@@ -63,10 +63,11 @@ class RegionSerializer(serializers.ModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
     parent = NestedRegionSerializer(required=False, allow_null=True)
     parent = NestedRegionSerializer(required=False, allow_null=True)
     site_count = serializers.IntegerField(read_only=True)
     site_count = serializers.IntegerField(read_only=True)
+    _depth = serializers.IntegerField(source='level', read_only=True)
 
 
     class Meta:
     class Meta:
         model = Region
         model = Region
-        fields = ['id', 'url', 'name', 'slug', 'parent', 'description', 'site_count']
+        fields = ['id', 'url', 'name', 'slug', 'parent', 'description', 'site_count', '_depth']
 
 
 
 
 class SiteSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
 class SiteSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
@@ -101,10 +102,11 @@ class RackGroupSerializer(ValidatedModelSerializer):
     site = NestedSiteSerializer()
     site = NestedSiteSerializer()
     parent = NestedRackGroupSerializer(required=False, allow_null=True)
     parent = NestedRackGroupSerializer(required=False, allow_null=True)
     rack_count = serializers.IntegerField(read_only=True)
     rack_count = serializers.IntegerField(read_only=True)
+    _depth = serializers.IntegerField(source='level', read_only=True)
 
 
     class Meta:
     class Meta:
         model = RackGroup
         model = RackGroup
-        fields = ['id', 'url', 'name', 'slug', 'site', 'parent', 'description', 'rack_count']
+        fields = ['id', 'url', 'name', 'slug', 'site', 'parent', 'description', 'rack_count', '_depth']
 
 
 
 
 class RackRoleSerializer(ValidatedModelSerializer):
 class RackRoleSerializer(ValidatedModelSerializer):

+ 6 - 10
netbox/dcim/forms.py

@@ -6,7 +6,6 @@ from django.contrib.contenttypes.models import ContentType
 from django.contrib.postgres.forms.array import SimpleArrayField
 from django.contrib.postgres.forms.array import SimpleArrayField
 from django.core.exceptions import ObjectDoesNotExist
 from django.core.exceptions import ObjectDoesNotExist
 from django.utils.safestring import mark_safe
 from django.utils.safestring import mark_safe
-from mptt.forms import TreeNodeChoiceField
 from netaddr import EUI
 from netaddr import EUI
 from netaddr.core import AddrFormatError
 from netaddr.core import AddrFormatError
 from timezone_field import TimeZoneFormField
 from timezone_field import TimeZoneFormField
@@ -179,10 +178,9 @@ class MACAddressField(forms.Field):
 #
 #
 
 
 class RegionForm(BootstrapMixin, forms.ModelForm):
 class RegionForm(BootstrapMixin, forms.ModelForm):
-    parent = TreeNodeChoiceField(
+    parent = DynamicModelChoiceField(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        required=False,
-        widget=StaticSelect2()
+        required=False
     )
     )
     slug = SlugField()
     slug = SlugField()
 
 
@@ -219,10 +217,9 @@ class RegionFilterForm(BootstrapMixin, forms.Form):
 #
 #
 
 
 class SiteForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
 class SiteForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
-    region = TreeNodeChoiceField(
+    region = DynamicModelChoiceField(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        required=False,
-        widget=StaticSelect2()
+        required=False
     )
     )
     slug = SlugField()
     slug = SlugField()
     comments = CommentField()
     comments = CommentField()
@@ -305,10 +302,9 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
         initial='',
         initial='',
         widget=StaticSelect2()
         widget=StaticSelect2()
     )
     )
-    region = TreeNodeChoiceField(
+    region = DynamicModelChoiceField(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        required=False,
-        widget=StaticSelect2()
+        required=False
     )
     )
     tenant = DynamicModelChoiceField(
     tenant = DynamicModelChoiceField(
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),

+ 3 - 5
netbox/extras/forms.py

@@ -2,14 +2,13 @@ from django import forms
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 from django.utils.safestring import mark_safe
 from django.utils.safestring import mark_safe
-from mptt.forms import TreeNodeMultipleChoiceField
 
 
 from dcim.models import DeviceRole, Platform, Region, Site
 from dcim.models import DeviceRole, Platform, Region, Site
 from tenancy.models import Tenant, TenantGroup
 from tenancy.models import Tenant, TenantGroup
 from utilities.forms import (
 from utilities.forms import (
     add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect,
     add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect,
     ContentTypeSelect, CSVModelForm, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField,
     ContentTypeSelect, CSVModelForm, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField,
-    StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES,
+    StaticSelect2, BOOLEAN_WITH_BLANK_CHOICES,
 )
 )
 from virtualization.models import Cluster, ClusterGroup
 from virtualization.models import Cluster, ClusterGroup
 from .choices import *
 from .choices import *
@@ -211,10 +210,9 @@ class TagBulkEditForm(BootstrapMixin, BulkEditForm):
 #
 #
 
 
 class ConfigContextForm(BootstrapMixin, forms.ModelForm):
 class ConfigContextForm(BootstrapMixin, forms.ModelForm):
-    regions = TreeNodeMultipleChoiceField(
+    regions = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        required=False,
-        widget=StaticSelect2Multiple()
+        required=False
     )
     )
     sites = DynamicModelMultipleChoiceField(
     sites = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),

+ 0 - 12
netbox/extras/scripts.py

@@ -3,7 +3,6 @@ import json
 import logging
 import logging
 import os
 import os
 import pkgutil
 import pkgutil
-import time
 import traceback
 import traceback
 from collections import OrderedDict
 from collections import OrderedDict
 
 
@@ -12,11 +11,8 @@ from django import forms
 from django.conf import settings
 from django.conf import settings
 from django.core.validators import RegexValidator
 from django.core.validators import RegexValidator
 from django.db import transaction
 from django.db import transaction
-from django.utils import timezone
 from django.utils.decorators import classproperty
 from django.utils.decorators import classproperty
 from django_rq import job
 from django_rq import job
-from mptt.forms import TreeNodeChoiceField, TreeNodeMultipleChoiceField
-from mptt.models import MPTTModel
 
 
 from extras.api.serializers import ScriptOutputSerializer
 from extras.api.serializers import ScriptOutputSerializer
 from extras.choices import JobResultStatusChoices, LogLevelChoices
 from extras.choices import JobResultStatusChoices, LogLevelChoices
@@ -182,10 +178,6 @@ class ObjectVar(ScriptVariable):
         # Queryset for field choices
         # Queryset for field choices
         self.field_attrs['queryset'] = queryset
         self.field_attrs['queryset'] = queryset
 
 
-        # Update form field for MPTT (nested) objects
-        if issubclass(queryset.model, MPTTModel):
-            self.form_field = TreeNodeChoiceField
-
 
 
 class MultiObjectVar(ScriptVariable):
 class MultiObjectVar(ScriptVariable):
     """
     """
@@ -199,10 +191,6 @@ class MultiObjectVar(ScriptVariable):
         # Queryset for field choices
         # Queryset for field choices
         self.field_attrs['queryset'] = queryset
         self.field_attrs['queryset'] = queryset
 
 
-        # Update form field for MPTT (nested) objects
-        if issubclass(queryset.model, MPTTModel):
-            self.form_field = TreeNodeMultipleChoiceField
-
 
 
 class FileVar(ScriptVariable):
 class FileVar(ScriptVariable):
     """
     """

+ 4 - 0
netbox/project-static/js/forms.js

@@ -222,6 +222,10 @@ $(document).ready(function() {
 
 
                 results = results.reduce((results,record,idx) => {
                 results = results.reduce((results,record,idx) => {
                     record.text = record[element.getAttribute('display-field')] || record.name;
                     record.text = record[element.getAttribute('display-field')] || record.name;
+                    if (record._depth) {
+                        // Annotate hierarchical depth for MPTT objects
+                        record.text = '--'.repeat(record._depth) + ' ' + record.text;
+                    }
                     record.id = record[element.getAttribute('value-field')] || record.id;
                     record.id = record[element.getAttribute('value-field')] || record.id;
                     if(element.getAttribute('disabled-indicator') && record[element.getAttribute('disabled-indicator')]) {
                     if(element.getAttribute('disabled-indicator') && record[element.getAttribute('disabled-indicator')]) {
                         // The disabled-indicator equated to true, so we disable this option
                         // The disabled-indicator equated to true, so we disable this option

+ 2 - 1
netbox/tenancy/api/nested_serializers.py

@@ -16,10 +16,11 @@ __all__ = [
 class NestedTenantGroupSerializer(WritableNestedSerializer):
 class NestedTenantGroupSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenantgroup-detail')
     url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenantgroup-detail')
     tenant_count = serializers.IntegerField(read_only=True)
     tenant_count = serializers.IntegerField(read_only=True)
+    _depth = serializers.IntegerField(source='level', read_only=True)
 
 
     class Meta:
     class Meta:
         model = TenantGroup
         model = TenantGroup
-        fields = ['id', 'url', 'name', 'slug', 'tenant_count']
+        fields = ['id', 'url', 'name', 'slug', 'tenant_count', '_depth']
 
 
 
 
 class NestedTenantSerializer(WritableNestedSerializer):
 class NestedTenantSerializer(WritableNestedSerializer):

+ 2 - 1
netbox/tenancy/api/serializers.py

@@ -15,10 +15,11 @@ class TenantGroupSerializer(ValidatedModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenantgroup-detail')
     url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenantgroup-detail')
     parent = NestedTenantGroupSerializer(required=False, allow_null=True)
     parent = NestedTenantGroupSerializer(required=False, allow_null=True)
     tenant_count = serializers.IntegerField(read_only=True)
     tenant_count = serializers.IntegerField(read_only=True)
+    _depth = serializers.IntegerField(source='level', read_only=True)
 
 
     class Meta:
     class Meta:
         model = TenantGroup
         model = TenantGroup
-        fields = ['id', 'url', 'name', 'slug', 'parent', 'description', 'tenant_count']
+        fields = ['id', 'url', 'name', 'slug', 'parent', 'description', 'tenant_count', '_depth']
 
 
 
 
 class TenantSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
 class TenantSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):

+ 1 - 4
netbox/tenancy/forms.py

@@ -18,10 +18,7 @@ from .models import Tenant, TenantGroup
 class TenantGroupForm(BootstrapMixin, forms.ModelForm):
 class TenantGroupForm(BootstrapMixin, forms.ModelForm):
     parent = DynamicModelChoiceField(
     parent = DynamicModelChoiceField(
         queryset=TenantGroup.objects.all(),
         queryset=TenantGroup.objects.all(),
-        required=False,
-        widget=APISelect(
-            api_url="/api/tenancy/tenant-groups/"
-        )
+        required=False
     )
     )
     slug = SlugField()
     slug = SlugField()