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

This commit is contained in:
Jeremy Stretch 2020-07-09 09:50:01 -04:00
parent b535608519
commit 15525392a2
10 changed files with 28 additions and 37 deletions

View File

@ -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

View File

@ -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):

View File

@ -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):

View File

@ -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, required=False
widget=StaticSelect2()
) )
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, required=False
widget=StaticSelect2()
) )
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, required=False
widget=StaticSelect2()
) )
tenant = DynamicModelChoiceField( tenant = DynamicModelChoiceField(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),

View File

@ -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, required=False
widget=StaticSelect2Multiple()
) )
sites = DynamicModelMultipleChoiceField( sites = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),

View File

@ -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):
""" """

View File

@ -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

View File

@ -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):

View File

@ -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):

View File

@ -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, required=False
widget=APISelect(
api_url="/api/tenancy/tenant-groups/"
)
) )
slug = SlugField() slug = SlugField()