mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-19 09:53:34 -06:00
Closes #4837: Use dynamic form widget for relationships to MPTT objects
This commit is contained in:
parent
b535608519
commit
15525392a2
@ -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
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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,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(),
|
||||||
|
@ -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(),
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user