From 15525392a27f8663feec7d08e7008907d3adf074 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 9 Jul 2020 09:50:01 -0400 Subject: [PATCH] Closes #4837: Use dynamic form widget for relationships to MPTT objects --- docs/release-notes/version-2.9.md | 2 ++ netbox/dcim/api/nested_serializers.py | 6 ++++-- netbox/dcim/api/serializers.py | 6 ++++-- netbox/dcim/forms.py | 16 ++++++---------- netbox/extras/forms.py | 8 +++----- netbox/extras/scripts.py | 12 ------------ netbox/project-static/js/forms.js | 4 ++++ netbox/tenancy/api/nested_serializers.py | 3 ++- netbox/tenancy/api/serializers.py | 3 ++- netbox/tenancy/forms.py | 5 +---- 10 files changed, 28 insertions(+), 37 deletions(-) diff --git a/docs/release-notes/version-2.9.md b/docs/release-notes/version-2.9.md index ffae2f5de..21470aaa1 100644 --- a/docs/release-notes/version-2.9.md +++ b/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 * [#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 +* [#4837](https://github.com/netbox-community/netbox/issues/4837) - Use dynamic form widget for relationships to MPTT objects (e.g. regions) ### 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.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 `_depth` field has been added to all objects which feature a self-recursive hierarchy (namely regions, rack groups, and tenant groups). ### Other Changes diff --git a/netbox/dcim/api/nested_serializers.py b/netbox/dcim/api/nested_serializers.py index 5d9380c00..141aca013 100644 --- a/netbox/dcim/api/nested_serializers.py +++ b/netbox/dcim/api/nested_serializers.py @@ -47,10 +47,11 @@ __all__ = [ class NestedRegionSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail') site_count = serializers.IntegerField(read_only=True) + _depth = serializers.IntegerField(source='level', read_only=True) class Meta: model = models.Region - fields = ['id', 'url', 'name', 'slug', 'site_count'] + fields = ['id', 'url', 'name', 'slug', 'site_count', '_depth'] class NestedSiteSerializer(WritableNestedSerializer): @@ -68,10 +69,11 @@ class NestedSiteSerializer(WritableNestedSerializer): class NestedRackGroupSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackgroup-detail') rack_count = serializers.IntegerField(read_only=True) + _depth = serializers.IntegerField(source='level', read_only=True) class Meta: model = models.RackGroup - fields = ['id', 'url', 'name', 'slug', 'rack_count'] + fields = ['id', 'url', 'name', 'slug', 'rack_count', '_depth'] class NestedRackRoleSerializer(WritableNestedSerializer): diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 916f86487..c97298b2b 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -63,10 +63,11 @@ class RegionSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail') parent = NestedRegionSerializer(required=False, allow_null=True) site_count = serializers.IntegerField(read_only=True) + _depth = serializers.IntegerField(source='level', read_only=True) class Meta: 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): @@ -101,10 +102,11 @@ class RackGroupSerializer(ValidatedModelSerializer): site = NestedSiteSerializer() parent = NestedRackGroupSerializer(required=False, allow_null=True) rack_count = serializers.IntegerField(read_only=True) + _depth = serializers.IntegerField(source='level', read_only=True) class Meta: 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): diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index dcc1f6418..b0c6f15d9 100644 --- a/netbox/dcim/forms.py +++ b/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.core.exceptions import ObjectDoesNotExist from django.utils.safestring import mark_safe -from mptt.forms import TreeNodeChoiceField from netaddr import EUI from netaddr.core import AddrFormatError from timezone_field import TimeZoneFormField @@ -179,10 +178,9 @@ class MACAddressField(forms.Field): # class RegionForm(BootstrapMixin, forms.ModelForm): - parent = TreeNodeChoiceField( + parent = DynamicModelChoiceField( queryset=Region.objects.all(), - required=False, - widget=StaticSelect2() + required=False ) slug = SlugField() @@ -219,10 +217,9 @@ class RegionFilterForm(BootstrapMixin, forms.Form): # class SiteForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): - region = TreeNodeChoiceField( + region = DynamicModelChoiceField( queryset=Region.objects.all(), - required=False, - widget=StaticSelect2() + required=False ) slug = SlugField() comments = CommentField() @@ -305,10 +302,9 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor initial='', widget=StaticSelect2() ) - region = TreeNodeChoiceField( + region = DynamicModelChoiceField( queryset=Region.objects.all(), - required=False, - widget=StaticSelect2() + required=False ) tenant = DynamicModelChoiceField( queryset=Tenant.objects.all(), diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index 4b2c1844c..b61a44f60 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -2,14 +2,13 @@ from django import forms from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from django.utils.safestring import mark_safe -from mptt.forms import TreeNodeMultipleChoiceField from dcim.models import DeviceRole, Platform, Region, Site from tenancy.models import Tenant, TenantGroup from utilities.forms import ( add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect, ContentTypeSelect, CSVModelForm, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField, - StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES, + StaticSelect2, BOOLEAN_WITH_BLANK_CHOICES, ) from virtualization.models import Cluster, ClusterGroup from .choices import * @@ -211,10 +210,9 @@ class TagBulkEditForm(BootstrapMixin, BulkEditForm): # class ConfigContextForm(BootstrapMixin, forms.ModelForm): - regions = TreeNodeMultipleChoiceField( + regions = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), - required=False, - widget=StaticSelect2Multiple() + required=False ) sites = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), diff --git a/netbox/extras/scripts.py b/netbox/extras/scripts.py index 018963bd8..dd096c392 100644 --- a/netbox/extras/scripts.py +++ b/netbox/extras/scripts.py @@ -3,7 +3,6 @@ import json import logging import os import pkgutil -import time import traceback from collections import OrderedDict @@ -12,11 +11,8 @@ from django import forms from django.conf import settings from django.core.validators import RegexValidator from django.db import transaction -from django.utils import timezone from django.utils.decorators import classproperty from django_rq import job -from mptt.forms import TreeNodeChoiceField, TreeNodeMultipleChoiceField -from mptt.models import MPTTModel from extras.api.serializers import ScriptOutputSerializer from extras.choices import JobResultStatusChoices, LogLevelChoices @@ -182,10 +178,6 @@ class ObjectVar(ScriptVariable): # Queryset for field choices self.field_attrs['queryset'] = queryset - # Update form field for MPTT (nested) objects - if issubclass(queryset.model, MPTTModel): - self.form_field = TreeNodeChoiceField - class MultiObjectVar(ScriptVariable): """ @@ -199,10 +191,6 @@ class MultiObjectVar(ScriptVariable): # Queryset for field choices self.field_attrs['queryset'] = queryset - # Update form field for MPTT (nested) objects - if issubclass(queryset.model, MPTTModel): - self.form_field = TreeNodeMultipleChoiceField - class FileVar(ScriptVariable): """ diff --git a/netbox/project-static/js/forms.js b/netbox/project-static/js/forms.js index b97981f0e..26c8338c2 100644 --- a/netbox/project-static/js/forms.js +++ b/netbox/project-static/js/forms.js @@ -222,6 +222,10 @@ $(document).ready(function() { results = results.reduce((results,record,idx) => { 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; if(element.getAttribute('disabled-indicator') && record[element.getAttribute('disabled-indicator')]) { // The disabled-indicator equated to true, so we disable this option diff --git a/netbox/tenancy/api/nested_serializers.py b/netbox/tenancy/api/nested_serializers.py index 80780dba3..369d5eb1b 100644 --- a/netbox/tenancy/api/nested_serializers.py +++ b/netbox/tenancy/api/nested_serializers.py @@ -16,10 +16,11 @@ __all__ = [ class NestedTenantGroupSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenantgroup-detail') tenant_count = serializers.IntegerField(read_only=True) + _depth = serializers.IntegerField(source='level', read_only=True) class Meta: model = TenantGroup - fields = ['id', 'url', 'name', 'slug', 'tenant_count'] + fields = ['id', 'url', 'name', 'slug', 'tenant_count', '_depth'] class NestedTenantSerializer(WritableNestedSerializer): diff --git a/netbox/tenancy/api/serializers.py b/netbox/tenancy/api/serializers.py index 4589878e6..4467b050b 100644 --- a/netbox/tenancy/api/serializers.py +++ b/netbox/tenancy/api/serializers.py @@ -15,10 +15,11 @@ class TenantGroupSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenantgroup-detail') parent = NestedTenantGroupSerializer(required=False, allow_null=True) tenant_count = serializers.IntegerField(read_only=True) + _depth = serializers.IntegerField(source='level', read_only=True) class Meta: 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): diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index 5bd0657b6..04a3980a1 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -18,10 +18,7 @@ from .models import Tenant, TenantGroup class TenantGroupForm(BootstrapMixin, forms.ModelForm): parent = DynamicModelChoiceField( queryset=TenantGroup.objects.all(), - required=False, - widget=APISelect( - api_url="/api/tenancy/tenant-groups/" - ) + required=False ) slug = SlugField()