mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-16 04:02:52 -06:00
Merge branch 'develop' into 3995-navbar-overflow
This commit is contained in:
commit
5befa533c6
@ -4,7 +4,7 @@ The following sections detail how to set up a new instance of NetBox:
|
|||||||
|
|
||||||
1. [PostgreSQL database](1-postgresql.md)
|
1. [PostgreSQL database](1-postgresql.md)
|
||||||
2. [NetBox components](2-netbox.md)
|
2. [NetBox components](2-netbox.md)
|
||||||
3. [HTTP dameon](3-http-daemon.md)
|
3. [HTTP daemon](3-http-daemon.md)
|
||||||
4. [LDAP authentication](4-ldap.md) (optional)
|
4. [LDAP authentication](4-ldap.md) (optional)
|
||||||
|
|
||||||
# Upgrading
|
# Upgrading
|
||||||
|
@ -88,7 +88,7 @@ Finally, restart the WSGI services to run the new code. If you followed this gui
|
|||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# sudo systemctl restart netbox
|
# sudo systemctl restart netbox
|
||||||
# sudo systemctl restart netbox-rqworker
|
# sudo systemctl restart netbox-rq
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
|
@ -2,15 +2,26 @@
|
|||||||
|
|
||||||
## Enhancements
|
## Enhancements
|
||||||
|
|
||||||
* [#3995](https://github.com/netbox-community/netbox/issues/3995) - Make dropdown menus in the navigation bar scrollable
|
* [#3799](https://github.com/netbox-community/netbox/issues/3799) - Greatly improve performance when ordering device components
|
||||||
|
* [#4100](https://github.com/netbox-community/netbox/issues/4100) - Add device filter to component list views
|
||||||
* [#4113](https://github.com/netbox-community/netbox/issues/4113) - Add bulk edit functionality for device type components
|
* [#4113](https://github.com/netbox-community/netbox/issues/4113) - Add bulk edit functionality for device type components
|
||||||
|
* [#4116](https://github.com/netbox-community/netbox/issues/4116) - Enable bulk edit and delete functions for device component list views
|
||||||
|
* [#4129](https://github.com/netbox-community/netbox/issues/4129) - Add buttons to delete individual device type components
|
||||||
|
|
||||||
## Bug Fixes
|
## Bug Fixes
|
||||||
|
|
||||||
|
* [#3507](https://github.com/netbox-community/netbox/issues/3507) - Fix filtering IPaddress by multiple devices
|
||||||
|
* [#3995](https://github.com/netbox-community/netbox/issues/3995) - Make dropdown menus in the navigation bar scrollable
|
||||||
|
* [#4083](https://github.com/netbox-community/netbox/issues/4083) - Permit nullifying applicable choice fields via API requests
|
||||||
* [#4089](https://github.com/netbox-community/netbox/issues/4089) - Selection of power outlet type during bulk update is optional
|
* [#4089](https://github.com/netbox-community/netbox/issues/4089) - Selection of power outlet type during bulk update is optional
|
||||||
* [#4090](https://github.com/netbox-community/netbox/issues/4090) - Render URL custom fields as links under object view
|
* [#4090](https://github.com/netbox-community/netbox/issues/4090) - Render URL custom fields as links under object view
|
||||||
* [#4091](https://github.com/netbox-community/netbox/issues/4091) - Fix filtering of objects by custom fields using UI search form
|
* [#4091](https://github.com/netbox-community/netbox/issues/4091) - Fix filtering of objects by custom fields using UI search form
|
||||||
* [#4099](https://github.com/netbox-community/netbox/issues/4099) - Linkify interfaces on global interfaces list
|
* [#4099](https://github.com/netbox-community/netbox/issues/4099) - Linkify interfaces on global interfaces list
|
||||||
|
* [#4108](https://github.com/netbox-community/netbox/issues/4108) - Avoid extraneous database queries when rendering search forms
|
||||||
|
* [#4134](https://github.com/netbox-community/netbox/issues/4134) - Device power ports and outlets should inherit type from the parent device type
|
||||||
|
* [#4137](https://github.com/netbox-community/netbox/issues/4137) - Disable occupied terminations when connecting a cable to a circuit
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# v2.7.4 (2020-02-04)
|
# v2.7.4 (2020-02-04)
|
||||||
|
|
||||||
|
@ -9,7 +9,8 @@ from tenancy.forms import TenancyFilterForm, TenancyForm
|
|||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, DatePicker,
|
APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, DatePicker,
|
||||||
FilterChoiceField, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField
|
DynamicModelChoiceField, DynamicModelMultipleChoiceField, SmallTextarea, SlugField, StaticSelect2,
|
||||||
|
StaticSelect2Multiple, TagFilterField,
|
||||||
)
|
)
|
||||||
from .choices import CircuitStatusChoices
|
from .choices import CircuitStatusChoices
|
||||||
from .models import Circuit, CircuitTermination, CircuitType, Provider
|
from .models import Circuit, CircuitTermination, CircuitType, Provider
|
||||||
@ -107,7 +108,7 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
required=False,
|
required=False,
|
||||||
label='Search'
|
label='Search'
|
||||||
)
|
)
|
||||||
region = FilterChoiceField(
|
region = DynamicModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
required=False,
|
required=False,
|
||||||
@ -119,9 +120,10 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
site = FilterChoiceField(
|
site = DynamicModelMultipleChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
|
required=False,
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url="/api/dcim/sites/",
|
api_url="/api/dcim/sites/",
|
||||||
value_field="slug",
|
value_field="slug",
|
||||||
@ -164,6 +166,18 @@ class CircuitTypeCSVForm(forms.ModelForm):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||||
|
provider = DynamicModelChoiceField(
|
||||||
|
queryset=Provider.objects.all(),
|
||||||
|
widget=APISelect(
|
||||||
|
api_url="/api/circuits/providers/"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
type = DynamicModelChoiceField(
|
||||||
|
queryset=CircuitType.objects.all(),
|
||||||
|
widget=APISelect(
|
||||||
|
api_url="/api/circuits/circuit-types/"
|
||||||
|
)
|
||||||
|
)
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
tags = TagField(
|
tags = TagField(
|
||||||
required=False
|
required=False
|
||||||
@ -180,12 +194,6 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
'commit_rate': "Committed rate",
|
'commit_rate': "Committed rate",
|
||||||
}
|
}
|
||||||
widgets = {
|
widgets = {
|
||||||
'provider': APISelect(
|
|
||||||
api_url="/api/circuits/providers/"
|
|
||||||
),
|
|
||||||
'type': APISelect(
|
|
||||||
api_url="/api/circuits/circuit-types/"
|
|
||||||
),
|
|
||||||
'status': StaticSelect2(),
|
'status': StaticSelect2(),
|
||||||
'install_date': DatePicker(),
|
'install_date': DatePicker(),
|
||||||
}
|
}
|
||||||
@ -235,14 +243,14 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
|
|||||||
queryset=Circuit.objects.all(),
|
queryset=Circuit.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput
|
widget=forms.MultipleHiddenInput
|
||||||
)
|
)
|
||||||
type = forms.ModelChoiceField(
|
type = DynamicModelChoiceField(
|
||||||
queryset=CircuitType.objects.all(),
|
queryset=CircuitType.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url="/api/circuits/circuit-types/"
|
api_url="/api/circuits/circuit-types/"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
provider = forms.ModelChoiceField(
|
provider = DynamicModelChoiceField(
|
||||||
queryset=Provider.objects.all(),
|
queryset=Provider.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -255,7 +263,7 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
|
|||||||
initial='',
|
initial='',
|
||||||
widget=StaticSelect2()
|
widget=StaticSelect2()
|
||||||
)
|
)
|
||||||
tenant = forms.ModelChoiceField(
|
tenant = DynamicModelChoiceField(
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -290,17 +298,19 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
|
|||||||
required=False,
|
required=False,
|
||||||
label='Search'
|
label='Search'
|
||||||
)
|
)
|
||||||
type = FilterChoiceField(
|
type = DynamicModelMultipleChoiceField(
|
||||||
queryset=CircuitType.objects.all(),
|
queryset=CircuitType.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
|
required=False,
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url="/api/circuits/circuit-types/",
|
api_url="/api/circuits/circuit-types/",
|
||||||
value_field="slug",
|
value_field="slug",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
provider = FilterChoiceField(
|
provider = DynamicModelMultipleChoiceField(
|
||||||
queryset=Provider.objects.all(),
|
queryset=Provider.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
|
required=False,
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url="/api/circuits/providers/",
|
api_url="/api/circuits/providers/",
|
||||||
value_field="slug",
|
value_field="slug",
|
||||||
@ -311,7 +321,7 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
|
|||||||
required=False,
|
required=False,
|
||||||
widget=StaticSelect2Multiple()
|
widget=StaticSelect2Multiple()
|
||||||
)
|
)
|
||||||
region = forms.ModelMultipleChoiceField(
|
region = DynamicModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
required=False,
|
required=False,
|
||||||
@ -323,9 +333,10 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
site = FilterChoiceField(
|
site = DynamicModelMultipleChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
|
required=False,
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url="/api/dcim/sites/",
|
api_url="/api/dcim/sites/",
|
||||||
value_field="slug",
|
value_field="slug",
|
||||||
|
@ -117,9 +117,9 @@ class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
|||||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
status = ChoiceField(choices=RackStatusChoices, required=False)
|
status = ChoiceField(choices=RackStatusChoices, required=False)
|
||||||
role = NestedRackRoleSerializer(required=False, allow_null=True)
|
role = NestedRackRoleSerializer(required=False, allow_null=True)
|
||||||
type = ChoiceField(choices=RackTypeChoices, required=False, allow_null=True)
|
type = ChoiceField(choices=RackTypeChoices, allow_blank=True, required=False)
|
||||||
width = ChoiceField(choices=RackWidthChoices, required=False)
|
width = ChoiceField(choices=RackWidthChoices, required=False)
|
||||||
outer_unit = ChoiceField(choices=RackDimensionUnitChoices, required=False)
|
outer_unit = ChoiceField(choices=RackDimensionUnitChoices, allow_blank=True, required=False)
|
||||||
tags = TagListSerializerField(required=False)
|
tags = TagListSerializerField(required=False)
|
||||||
device_count = serializers.IntegerField(read_only=True)
|
device_count = serializers.IntegerField(read_only=True)
|
||||||
powerfeed_count = serializers.IntegerField(read_only=True)
|
powerfeed_count = serializers.IntegerField(read_only=True)
|
||||||
@ -212,7 +212,7 @@ class ManufacturerSerializer(ValidatedModelSerializer):
|
|||||||
|
|
||||||
class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||||
manufacturer = NestedManufacturerSerializer()
|
manufacturer = NestedManufacturerSerializer()
|
||||||
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, required=False, allow_null=True)
|
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False)
|
||||||
tags = TagListSerializerField(required=False)
|
tags = TagListSerializerField(required=False)
|
||||||
device_count = serializers.IntegerField(read_only=True)
|
device_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
@ -228,6 +228,7 @@ class ConsolePortTemplateSerializer(ValidatedModelSerializer):
|
|||||||
device_type = NestedDeviceTypeSerializer()
|
device_type = NestedDeviceTypeSerializer()
|
||||||
type = ChoiceField(
|
type = ChoiceField(
|
||||||
choices=ConsolePortTypeChoices,
|
choices=ConsolePortTypeChoices,
|
||||||
|
allow_blank=True,
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -240,6 +241,7 @@ class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
|
|||||||
device_type = NestedDeviceTypeSerializer()
|
device_type = NestedDeviceTypeSerializer()
|
||||||
type = ChoiceField(
|
type = ChoiceField(
|
||||||
choices=ConsolePortTypeChoices,
|
choices=ConsolePortTypeChoices,
|
||||||
|
allow_blank=True,
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -252,6 +254,7 @@ class PowerPortTemplateSerializer(ValidatedModelSerializer):
|
|||||||
device_type = NestedDeviceTypeSerializer()
|
device_type = NestedDeviceTypeSerializer()
|
||||||
type = ChoiceField(
|
type = ChoiceField(
|
||||||
choices=PowerPortTypeChoices,
|
choices=PowerPortTypeChoices,
|
||||||
|
allow_blank=True,
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -264,6 +267,7 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
|
|||||||
device_type = NestedDeviceTypeSerializer()
|
device_type = NestedDeviceTypeSerializer()
|
||||||
type = ChoiceField(
|
type = ChoiceField(
|
||||||
choices=PowerOutletTypeChoices,
|
choices=PowerOutletTypeChoices,
|
||||||
|
allow_blank=True,
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
power_port = PowerPortTemplateSerializer(
|
power_port = PowerPortTemplateSerializer(
|
||||||
@ -271,8 +275,8 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
|
|||||||
)
|
)
|
||||||
feed_leg = ChoiceField(
|
feed_leg = ChoiceField(
|
||||||
choices=PowerOutletFeedLegChoices,
|
choices=PowerOutletFeedLegChoices,
|
||||||
required=False,
|
allow_blank=True,
|
||||||
allow_null=True
|
required=False
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -351,7 +355,7 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
|||||||
platform = NestedPlatformSerializer(required=False, allow_null=True)
|
platform = NestedPlatformSerializer(required=False, allow_null=True)
|
||||||
site = NestedSiteSerializer()
|
site = NestedSiteSerializer()
|
||||||
rack = NestedRackSerializer(required=False, allow_null=True)
|
rack = NestedRackSerializer(required=False, allow_null=True)
|
||||||
face = ChoiceField(choices=DeviceFaceChoices, required=False, allow_null=True)
|
face = ChoiceField(choices=DeviceFaceChoices, allow_blank=True, required=False)
|
||||||
status = ChoiceField(choices=DeviceStatusChoices, required=False)
|
status = ChoiceField(choices=DeviceStatusChoices, required=False)
|
||||||
primary_ip = NestedIPAddressSerializer(read_only=True)
|
primary_ip = NestedIPAddressSerializer(read_only=True)
|
||||||
primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
|
primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||||
@ -420,6 +424,7 @@ class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer)
|
|||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
type = ChoiceField(
|
type = ChoiceField(
|
||||||
choices=ConsolePortTypeChoices,
|
choices=ConsolePortTypeChoices,
|
||||||
|
allow_blank=True,
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
cable = NestedCableSerializer(read_only=True)
|
cable = NestedCableSerializer(read_only=True)
|
||||||
@ -437,6 +442,7 @@ class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
|||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
type = ChoiceField(
|
type = ChoiceField(
|
||||||
choices=ConsolePortTypeChoices,
|
choices=ConsolePortTypeChoices,
|
||||||
|
allow_blank=True,
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
cable = NestedCableSerializer(read_only=True)
|
cable = NestedCableSerializer(read_only=True)
|
||||||
@ -454,6 +460,7 @@ class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
|||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
type = ChoiceField(
|
type = ChoiceField(
|
||||||
choices=PowerOutletTypeChoices,
|
choices=PowerOutletTypeChoices,
|
||||||
|
allow_blank=True,
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
power_port = NestedPowerPortSerializer(
|
power_port = NestedPowerPortSerializer(
|
||||||
@ -461,8 +468,8 @@ class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
|||||||
)
|
)
|
||||||
feed_leg = ChoiceField(
|
feed_leg = ChoiceField(
|
||||||
choices=PowerOutletFeedLegChoices,
|
choices=PowerOutletFeedLegChoices,
|
||||||
required=False,
|
allow_blank=True,
|
||||||
allow_null=True
|
required=False
|
||||||
)
|
)
|
||||||
cable = NestedCableSerializer(
|
cable = NestedCableSerializer(
|
||||||
read_only=True
|
read_only=True
|
||||||
@ -483,6 +490,7 @@ class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
|||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
type = ChoiceField(
|
type = ChoiceField(
|
||||||
choices=PowerPortTypeChoices,
|
choices=PowerPortTypeChoices,
|
||||||
|
allow_blank=True,
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
cable = NestedCableSerializer(read_only=True)
|
cable = NestedCableSerializer(read_only=True)
|
||||||
@ -500,7 +508,7 @@ class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
|||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
type = ChoiceField(choices=InterfaceTypeChoices, required=False)
|
type = ChoiceField(choices=InterfaceTypeChoices, required=False)
|
||||||
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||||
mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_null=True)
|
mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
|
||||||
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
|
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
|
||||||
tagged_vlans = SerializedPKRelatedField(
|
tagged_vlans = SerializedPKRelatedField(
|
||||||
queryset=VLAN.objects.all(),
|
queryset=VLAN.objects.all(),
|
||||||
@ -617,7 +625,7 @@ class CableSerializer(ValidatedModelSerializer):
|
|||||||
termination_a = serializers.SerializerMethodField(read_only=True)
|
termination_a = serializers.SerializerMethodField(read_only=True)
|
||||||
termination_b = serializers.SerializerMethodField(read_only=True)
|
termination_b = serializers.SerializerMethodField(read_only=True)
|
||||||
status = ChoiceField(choices=CableStatusChoices, required=False)
|
status = ChoiceField(choices=CableStatusChoices, required=False)
|
||||||
length_unit = ChoiceField(choices=CableLengthUnitChoices, required=False, allow_null=True)
|
length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Cable
|
model = Cable
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,18 +1,7 @@
|
|||||||
from django.db.models import Manager, QuerySet
|
from django.db.models import Manager, QuerySet
|
||||||
from django.db.models.expressions import RawSQL
|
|
||||||
|
|
||||||
from .constants import NONCONNECTABLE_IFACE_TYPES
|
from .constants import NONCONNECTABLE_IFACE_TYPES
|
||||||
|
|
||||||
# Regular expressions for parsing Interface names
|
|
||||||
TYPE_RE = r"SUBSTRING({} FROM '^([^0-9\.:]+)')"
|
|
||||||
SLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(\d{{1,9}})/') AS integer), NULL)"
|
|
||||||
SUBSLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9\.:]+)?\d{{1,9}}/(\d{{1,9}})') AS integer), NULL)"
|
|
||||||
POSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}/){{2}}(\d{{1,9}})') AS integer), NULL)"
|
|
||||||
SUBPOSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}/){{3}}(\d{{1,9}})') AS integer), NULL)"
|
|
||||||
ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9\.:]+)?(\d{{1,9}})([^/]|$)') AS integer)"
|
|
||||||
CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^.*:(\d{{1,9}})(\.\d{{1,9}})?$') AS integer), 0)"
|
|
||||||
VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^.*\.(\d{{1,9}})$') AS integer), 0)"
|
|
||||||
|
|
||||||
|
|
||||||
class InterfaceQuerySet(QuerySet):
|
class InterfaceQuerySet(QuerySet):
|
||||||
|
|
||||||
@ -27,47 +16,4 @@ class InterfaceQuerySet(QuerySet):
|
|||||||
class InterfaceManager(Manager):
|
class InterfaceManager(Manager):
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""
|
return InterfaceQuerySet(self.model, using=self._db)
|
||||||
Naturally order interfaces by their type and numeric position. To order interfaces naturally, the `name` field
|
|
||||||
is split into eight distinct components: leading text (type), slot, subslot, position, subposition, ID, channel,
|
|
||||||
and virtual circuit:
|
|
||||||
|
|
||||||
{type}{slot or ID}/{subslot}/{position}/{subposition}:{channel}.{vc}
|
|
||||||
|
|
||||||
Components absent from the interface name are coalesced to zero or null. For example, an interface named
|
|
||||||
GigabitEthernet1/2/3 would be parsed as follows:
|
|
||||||
|
|
||||||
type = 'GigabitEthernet'
|
|
||||||
slot = 1
|
|
||||||
subslot = 2
|
|
||||||
position = 3
|
|
||||||
subposition = None
|
|
||||||
id = None
|
|
||||||
channel = 0
|
|
||||||
vc = 0
|
|
||||||
|
|
||||||
The original `name` field is considered in its entirety to serve as a fallback in the event interfaces do not
|
|
||||||
match any of the prescribed fields.
|
|
||||||
|
|
||||||
The `id` field is included to enforce deterministic ordering of interfaces in similar vein of other device
|
|
||||||
components.
|
|
||||||
"""
|
|
||||||
|
|
||||||
sql_col = '{}.name'.format(self.model._meta.db_table)
|
|
||||||
ordering = [
|
|
||||||
'_slot', '_subslot', '_position', '_subposition', '_type', '_id', '_channel', '_vc', 'name', 'pk'
|
|
||||||
|
|
||||||
]
|
|
||||||
|
|
||||||
fields = {
|
|
||||||
'_type': RawSQL(TYPE_RE.format(sql_col), []),
|
|
||||||
'_id': RawSQL(ID_RE.format(sql_col), []),
|
|
||||||
'_slot': RawSQL(SLOT_RE.format(sql_col), []),
|
|
||||||
'_subslot': RawSQL(SUBSLOT_RE.format(sql_col), []),
|
|
||||||
'_position': RawSQL(POSITION_RE.format(sql_col), []),
|
|
||||||
'_subposition': RawSQL(SUBPOSITION_RE.format(sql_col), []),
|
|
||||||
'_channel': RawSQL(CHANNEL_RE.format(sql_col), []),
|
|
||||||
'_vc': RawSQL(VC_RE.format(sql_col), []),
|
|
||||||
}
|
|
||||||
|
|
||||||
return InterfaceQuerySet(self.model, using=self._db).annotate(**fields).order_by(*ordering)
|
|
||||||
|
147
netbox/dcim/migrations/0093_device_component_ordering.py
Normal file
147
netbox/dcim/migrations/0093_device_component_ordering.py
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
import utilities.fields
|
||||||
|
import utilities.ordering
|
||||||
|
|
||||||
|
|
||||||
|
def _update_model_names(model):
|
||||||
|
# Update each unique field value in bulk
|
||||||
|
for name in model.objects.values_list('name', flat=True).order_by('name').distinct():
|
||||||
|
model.objects.filter(name=name).update(_name=utilities.ordering.naturalize(name))
|
||||||
|
|
||||||
|
|
||||||
|
def naturalize_consoleports(apps, schema_editor):
|
||||||
|
_update_model_names(apps.get_model('dcim', 'ConsolePort'))
|
||||||
|
|
||||||
|
|
||||||
|
def naturalize_consoleserverports(apps, schema_editor):
|
||||||
|
_update_model_names(apps.get_model('dcim', 'ConsoleServerPort'))
|
||||||
|
|
||||||
|
|
||||||
|
def naturalize_powerports(apps, schema_editor):
|
||||||
|
_update_model_names(apps.get_model('dcim', 'PowerPort'))
|
||||||
|
|
||||||
|
|
||||||
|
def naturalize_poweroutlets(apps, schema_editor):
|
||||||
|
_update_model_names(apps.get_model('dcim', 'PowerOutlet'))
|
||||||
|
|
||||||
|
|
||||||
|
def naturalize_frontports(apps, schema_editor):
|
||||||
|
_update_model_names(apps.get_model('dcim', 'FrontPort'))
|
||||||
|
|
||||||
|
|
||||||
|
def naturalize_rearports(apps, schema_editor):
|
||||||
|
_update_model_names(apps.get_model('dcim', 'RearPort'))
|
||||||
|
|
||||||
|
|
||||||
|
def naturalize_devicebays(apps, schema_editor):
|
||||||
|
_update_model_names(apps.get_model('dcim', 'DeviceBay'))
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0092_fix_rack_outer_unit'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='consoleport',
|
||||||
|
options={'ordering': ('device', '_name')},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='consoleserverport',
|
||||||
|
options={'ordering': ('device', '_name')},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='devicebay',
|
||||||
|
options={'ordering': ('device', '_name')},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='frontport',
|
||||||
|
options={'ordering': ('device', '_name')},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='inventoryitem',
|
||||||
|
options={'ordering': ('device__id', 'parent__id', '_name')},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='poweroutlet',
|
||||||
|
options={'ordering': ('device', '_name')},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='powerport',
|
||||||
|
options={'ordering': ('device', '_name')},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='rearport',
|
||||||
|
options={'ordering': ('device', '_name')},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='consoleport',
|
||||||
|
name='_name',
|
||||||
|
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='consoleserverport',
|
||||||
|
name='_name',
|
||||||
|
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='devicebay',
|
||||||
|
name='_name',
|
||||||
|
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='frontport',
|
||||||
|
name='_name',
|
||||||
|
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='inventoryitem',
|
||||||
|
name='_name',
|
||||||
|
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='poweroutlet',
|
||||||
|
name='_name',
|
||||||
|
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='powerport',
|
||||||
|
name='_name',
|
||||||
|
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='rearport',
|
||||||
|
name='_name',
|
||||||
|
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=naturalize_consoleports,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=naturalize_consoleserverports,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=naturalize_powerports,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=naturalize_poweroutlets,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=naturalize_frontports,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=naturalize_rearports,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=naturalize_devicebays,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,138 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
import utilities.fields
|
||||||
|
import utilities.ordering
|
||||||
|
|
||||||
|
|
||||||
|
def _update_model_names(model):
|
||||||
|
# Update each unique field value in bulk
|
||||||
|
for name in model.objects.values_list('name', flat=True).order_by('name').distinct():
|
||||||
|
model.objects.filter(name=name).update(_name=utilities.ordering.naturalize(name))
|
||||||
|
|
||||||
|
|
||||||
|
def naturalize_consoleporttemplates(apps, schema_editor):
|
||||||
|
_update_model_names(apps.get_model('dcim', 'ConsolePortTemplate'))
|
||||||
|
|
||||||
|
|
||||||
|
def naturalize_consoleserverporttemplates(apps, schema_editor):
|
||||||
|
_update_model_names(apps.get_model('dcim', 'ConsoleServerPortTemplate'))
|
||||||
|
|
||||||
|
|
||||||
|
def naturalize_powerporttemplates(apps, schema_editor):
|
||||||
|
_update_model_names(apps.get_model('dcim', 'PowerPortTemplate'))
|
||||||
|
|
||||||
|
|
||||||
|
def naturalize_poweroutlettemplates(apps, schema_editor):
|
||||||
|
_update_model_names(apps.get_model('dcim', 'PowerOutletTemplate'))
|
||||||
|
|
||||||
|
|
||||||
|
def naturalize_frontporttemplates(apps, schema_editor):
|
||||||
|
_update_model_names(apps.get_model('dcim', 'FrontPortTemplate'))
|
||||||
|
|
||||||
|
|
||||||
|
def naturalize_rearporttemplates(apps, schema_editor):
|
||||||
|
_update_model_names(apps.get_model('dcim', 'RearPortTemplate'))
|
||||||
|
|
||||||
|
|
||||||
|
def naturalize_devicebaytemplates(apps, schema_editor):
|
||||||
|
_update_model_names(apps.get_model('dcim', 'DeviceBayTemplate'))
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0093_device_component_ordering'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='consoleporttemplate',
|
||||||
|
options={'ordering': ('device_type', '_name')},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='consoleserverporttemplate',
|
||||||
|
options={'ordering': ('device_type', '_name')},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='devicebaytemplate',
|
||||||
|
options={'ordering': ('device_type', '_name')},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='frontporttemplate',
|
||||||
|
options={'ordering': ('device_type', '_name')},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='poweroutlettemplate',
|
||||||
|
options={'ordering': ('device_type', '_name')},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='powerporttemplate',
|
||||||
|
options={'ordering': ('device_type', '_name')},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='rearporttemplate',
|
||||||
|
options={'ordering': ('device_type', '_name')},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='consoleporttemplate',
|
||||||
|
name='_name',
|
||||||
|
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='consoleserverporttemplate',
|
||||||
|
name='_name',
|
||||||
|
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='devicebaytemplate',
|
||||||
|
name='_name',
|
||||||
|
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='frontporttemplate',
|
||||||
|
name='_name',
|
||||||
|
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='poweroutlettemplate',
|
||||||
|
name='_name',
|
||||||
|
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='powerporttemplate',
|
||||||
|
name='_name',
|
||||||
|
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='rearporttemplate',
|
||||||
|
name='_name',
|
||||||
|
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=naturalize_consoleporttemplates,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=naturalize_consoleserverporttemplates,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=naturalize_powerporttemplates,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=naturalize_poweroutlettemplates,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=naturalize_frontporttemplates,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=naturalize_rearporttemplates,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=naturalize_devicebaytemplates,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
]
|
70
netbox/dcim/migrations/0095_primary_model_ordering.py
Normal file
70
netbox/dcim/migrations/0095_primary_model_ordering.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
import utilities.fields
|
||||||
|
import utilities.ordering
|
||||||
|
|
||||||
|
|
||||||
|
def _update_model_names(model):
|
||||||
|
# Update each unique field value in bulk
|
||||||
|
for name in model.objects.values_list('name', flat=True).order_by('name').distinct():
|
||||||
|
model.objects.filter(name=name).update(_name=utilities.ordering.naturalize(name))
|
||||||
|
|
||||||
|
|
||||||
|
def naturalize_sites(apps, schema_editor):
|
||||||
|
_update_model_names(apps.get_model('dcim', 'Site'))
|
||||||
|
|
||||||
|
|
||||||
|
def naturalize_racks(apps, schema_editor):
|
||||||
|
_update_model_names(apps.get_model('dcim', 'Rack'))
|
||||||
|
|
||||||
|
|
||||||
|
def naturalize_devices(apps, schema_editor):
|
||||||
|
_update_model_names(apps.get_model('dcim', 'Device'))
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0094_device_component_template_ordering'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='device',
|
||||||
|
options={'ordering': ('_name', 'pk'), 'permissions': (('napalm_read', 'Read-only access to devices via NAPALM'), ('napalm_write', 'Read/write access to devices via NAPALM'))},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='rack',
|
||||||
|
options={'ordering': ('site', 'group', '_name', 'pk')},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='site',
|
||||||
|
options={'ordering': ('_name',)},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='device',
|
||||||
|
name='_name',
|
||||||
|
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='rack',
|
||||||
|
name='_name',
|
||||||
|
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='site',
|
||||||
|
name='_name',
|
||||||
|
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=naturalize_sites,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=naturalize_racks,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=naturalize_devices,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
]
|
53
netbox/dcim/migrations/0096_interface_ordering.py
Normal file
53
netbox/dcim/migrations/0096_interface_ordering.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
import utilities.fields
|
||||||
|
import utilities.ordering
|
||||||
|
|
||||||
|
|
||||||
|
def _update_model_names(model):
|
||||||
|
# Update each unique field value in bulk
|
||||||
|
for name in model.objects.values_list('name', flat=True).order_by('name').distinct():
|
||||||
|
model.objects.filter(name=name).update(_name=utilities.ordering.naturalize_interface(name))
|
||||||
|
|
||||||
|
|
||||||
|
def naturalize_interfacetemplates(apps, schema_editor):
|
||||||
|
_update_model_names(apps.get_model('dcim', 'InterfaceTemplate'))
|
||||||
|
|
||||||
|
|
||||||
|
def naturalize_interfaces(apps, schema_editor):
|
||||||
|
_update_model_names(apps.get_model('dcim', 'Interface'))
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0095_primary_model_ordering'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='interface',
|
||||||
|
options={'ordering': ('device', '_name')},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='interfacetemplate',
|
||||||
|
options={'ordering': ('device_type', '_name')},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='interface',
|
||||||
|
name='_name',
|
||||||
|
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize_interface),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='interfacetemplate',
|
||||||
|
name='_name',
|
||||||
|
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize_interface),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=naturalize_interfacetemplates,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=naturalize_interfaces,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
]
|
@ -22,8 +22,7 @@ from dcim.choices import *
|
|||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.fields import ASNField
|
from dcim.fields import ASNField
|
||||||
from extras.models import ConfigContextModel, CustomFieldModel, TaggedItem
|
from extras.models import ConfigContextModel, CustomFieldModel, TaggedItem
|
||||||
from utilities.fields import ColorField
|
from utilities.fields import ColorField, NaturalOrderingField
|
||||||
from utilities.managers import NaturalOrderingManager
|
|
||||||
from utilities.models import ChangeLoggedModel
|
from utilities.models import ChangeLoggedModel
|
||||||
from utilities.utils import foreground_color, to_meters
|
from utilities.utils import foreground_color, to_meters
|
||||||
from .device_component_templates import (
|
from .device_component_templates import (
|
||||||
@ -134,6 +133,11 @@ class Site(ChangeLoggedModel, CustomFieldModel):
|
|||||||
max_length=50,
|
max_length=50,
|
||||||
unique=True
|
unique=True
|
||||||
)
|
)
|
||||||
|
_name = NaturalOrderingField(
|
||||||
|
target_field='name',
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
slug = models.SlugField(
|
slug = models.SlugField(
|
||||||
unique=True
|
unique=True
|
||||||
)
|
)
|
||||||
@ -215,8 +219,6 @@ class Site(ChangeLoggedModel, CustomFieldModel):
|
|||||||
images = GenericRelation(
|
images = GenericRelation(
|
||||||
to='extras.ImageAttachment'
|
to='extras.ImageAttachment'
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = NaturalOrderingManager()
|
|
||||||
tags = TaggableManager(through=TaggedItem)
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = [
|
csv_headers = [
|
||||||
@ -235,7 +237,7 @@ class Site(ChangeLoggedModel, CustomFieldModel):
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['name']
|
ordering = ('_name',)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@ -516,6 +518,11 @@ class Rack(ChangeLoggedModel, CustomFieldModel, RackElevationHelperMixin):
|
|||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=50
|
max_length=50
|
||||||
)
|
)
|
||||||
|
_name = NaturalOrderingField(
|
||||||
|
target_field='name',
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
facility_id = models.CharField(
|
facility_id = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
blank=True,
|
blank=True,
|
||||||
@ -612,8 +619,6 @@ class Rack(ChangeLoggedModel, CustomFieldModel, RackElevationHelperMixin):
|
|||||||
images = GenericRelation(
|
images = GenericRelation(
|
||||||
to='extras.ImageAttachment'
|
to='extras.ImageAttachment'
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = NaturalOrderingManager()
|
|
||||||
tags = TaggableManager(through=TaggedItem)
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = [
|
csv_headers = [
|
||||||
@ -634,12 +639,12 @@ class Rack(ChangeLoggedModel, CustomFieldModel, RackElevationHelperMixin):
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('site', 'group', 'name', 'pk') # (site, group, name) may be non-unique
|
ordering = ('site', 'group', '_name', 'pk') # (site, group, name) may be non-unique
|
||||||
unique_together = [
|
unique_together = (
|
||||||
# Name and facility_id must be unique *only* within a RackGroup
|
# Name and facility_id must be unique *only* within a RackGroup
|
||||||
['group', 'name'],
|
('group', 'name'),
|
||||||
['group', 'facility_id'],
|
('group', 'facility_id'),
|
||||||
]
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.display_name or super().__str__()
|
return self.display_name or super().__str__()
|
||||||
@ -1313,6 +1318,12 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
|||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
|
_name = NaturalOrderingField(
|
||||||
|
target_field='name',
|
||||||
|
max_length=100,
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
serial = models.CharField(
|
serial = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
blank=True,
|
blank=True,
|
||||||
@ -1407,8 +1418,6 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
|||||||
images = GenericRelation(
|
images = GenericRelation(
|
||||||
to='extras.ImageAttachment'
|
to='extras.ImageAttachment'
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = NaturalOrderingManager()
|
|
||||||
tags = TaggableManager(through=TaggedItem)
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = [
|
csv_headers = [
|
||||||
@ -1430,12 +1439,12 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('name', 'pk') # Name may be NULL
|
ordering = ('_name', 'pk') # Name may be null
|
||||||
unique_together = [
|
unique_together = (
|
||||||
['site', 'tenant', 'name'], # See validate_unique below
|
('site', 'tenant', 'name'), # See validate_unique below
|
||||||
['rack', 'position', 'face'],
|
('rack', 'position', 'face'),
|
||||||
['virtual_chassis', 'vc_position'],
|
('virtual_chassis', 'vc_position'),
|
||||||
]
|
)
|
||||||
permissions = (
|
permissions = (
|
||||||
('napalm_read', 'Read-only access to devices via NAPALM'),
|
('napalm_read', 'Read-only access to devices via NAPALM'),
|
||||||
('napalm_write', 'Read/write access to devices via NAPALM'),
|
('napalm_write', 'Read/write access to devices via NAPALM'),
|
||||||
|
@ -4,9 +4,9 @@ from django.db import models
|
|||||||
|
|
||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.managers import InterfaceManager
|
|
||||||
from extras.models import ObjectChange
|
from extras.models import ObjectChange
|
||||||
from utilities.managers import NaturalOrderingManager
|
from utilities.fields import NaturalOrderingField
|
||||||
|
from utilities.ordering import naturalize_interface
|
||||||
from utilities.utils import serialize_object
|
from utilities.utils import serialize_object
|
||||||
from .device_components import (
|
from .device_components import (
|
||||||
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, PowerOutlet, PowerPort, RearPort,
|
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, PowerOutlet, PowerPort, RearPort,
|
||||||
@ -58,17 +58,20 @@ class ConsolePortTemplate(ComponentTemplateModel):
|
|||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=50
|
max_length=50
|
||||||
)
|
)
|
||||||
|
_name = NaturalOrderingField(
|
||||||
|
target_field='name',
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
type = models.CharField(
|
type = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
choices=ConsolePortTypeChoices,
|
choices=ConsolePortTypeChoices,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = NaturalOrderingManager()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['device_type', 'name']
|
ordering = ('device_type', '_name')
|
||||||
unique_together = ['device_type', 'name']
|
unique_together = ('device_type', 'name')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@ -93,17 +96,20 @@ class ConsoleServerPortTemplate(ComponentTemplateModel):
|
|||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=50
|
max_length=50
|
||||||
)
|
)
|
||||||
|
_name = NaturalOrderingField(
|
||||||
|
target_field='name',
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
type = models.CharField(
|
type = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
choices=ConsolePortTypeChoices,
|
choices=ConsolePortTypeChoices,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = NaturalOrderingManager()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['device_type', 'name']
|
ordering = ('device_type', '_name')
|
||||||
unique_together = ['device_type', 'name']
|
unique_together = ('device_type', 'name')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@ -128,6 +134,11 @@ class PowerPortTemplate(ComponentTemplateModel):
|
|||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=50
|
max_length=50
|
||||||
)
|
)
|
||||||
|
_name = NaturalOrderingField(
|
||||||
|
target_field='name',
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
type = models.CharField(
|
type = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
choices=PowerPortTypeChoices,
|
choices=PowerPortTypeChoices,
|
||||||
@ -146,11 +157,9 @@ class PowerPortTemplate(ComponentTemplateModel):
|
|||||||
help_text="Allocated power draw (watts)"
|
help_text="Allocated power draw (watts)"
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = NaturalOrderingManager()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['device_type', 'name']
|
ordering = ('device_type', '_name')
|
||||||
unique_together = ['device_type', 'name']
|
unique_together = ('device_type', 'name')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@ -159,6 +168,7 @@ class PowerPortTemplate(ComponentTemplateModel):
|
|||||||
return PowerPort(
|
return PowerPort(
|
||||||
device=device,
|
device=device,
|
||||||
name=self.name,
|
name=self.name,
|
||||||
|
type=self.type,
|
||||||
maximum_draw=self.maximum_draw,
|
maximum_draw=self.maximum_draw,
|
||||||
allocated_draw=self.allocated_draw
|
allocated_draw=self.allocated_draw
|
||||||
)
|
)
|
||||||
@ -176,6 +186,11 @@ class PowerOutletTemplate(ComponentTemplateModel):
|
|||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=50
|
max_length=50
|
||||||
)
|
)
|
||||||
|
_name = NaturalOrderingField(
|
||||||
|
target_field='name',
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
type = models.CharField(
|
type = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
choices=PowerOutletTypeChoices,
|
choices=PowerOutletTypeChoices,
|
||||||
@ -195,11 +210,9 @@ class PowerOutletTemplate(ComponentTemplateModel):
|
|||||||
help_text="Phase (for three-phase feeds)"
|
help_text="Phase (for three-phase feeds)"
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = NaturalOrderingManager()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['device_type', 'name']
|
ordering = ('device_type', '_name')
|
||||||
unique_together = ['device_type', 'name']
|
unique_together = ('device_type', 'name')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@ -220,6 +233,7 @@ class PowerOutletTemplate(ComponentTemplateModel):
|
|||||||
return PowerOutlet(
|
return PowerOutlet(
|
||||||
device=device,
|
device=device,
|
||||||
name=self.name,
|
name=self.name,
|
||||||
|
type=self.type,
|
||||||
power_port=power_port,
|
power_port=power_port,
|
||||||
feed_leg=self.feed_leg
|
feed_leg=self.feed_leg
|
||||||
)
|
)
|
||||||
@ -237,6 +251,12 @@ class InterfaceTemplate(ComponentTemplateModel):
|
|||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=64
|
max_length=64
|
||||||
)
|
)
|
||||||
|
_name = NaturalOrderingField(
|
||||||
|
target_field='name',
|
||||||
|
naturalize_function=naturalize_interface,
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
type = models.CharField(
|
type = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
choices=InterfaceTypeChoices
|
choices=InterfaceTypeChoices
|
||||||
@ -246,11 +266,9 @@ class InterfaceTemplate(ComponentTemplateModel):
|
|||||||
verbose_name='Management only'
|
verbose_name='Management only'
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = InterfaceManager()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['device_type', 'name']
|
ordering = ('device_type', '_name')
|
||||||
unique_together = ['device_type', 'name']
|
unique_together = ('device_type', 'name')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@ -276,6 +294,11 @@ class FrontPortTemplate(ComponentTemplateModel):
|
|||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=64
|
max_length=64
|
||||||
)
|
)
|
||||||
|
_name = NaturalOrderingField(
|
||||||
|
target_field='name',
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
type = models.CharField(
|
type = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
choices=PortTypeChoices
|
choices=PortTypeChoices
|
||||||
@ -290,14 +313,12 @@ class FrontPortTemplate(ComponentTemplateModel):
|
|||||||
validators=[MinValueValidator(1), MaxValueValidator(64)]
|
validators=[MinValueValidator(1), MaxValueValidator(64)]
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = NaturalOrderingManager()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['device_type', 'name']
|
ordering = ('device_type', '_name')
|
||||||
unique_together = [
|
unique_together = (
|
||||||
['device_type', 'name'],
|
('device_type', 'name'),
|
||||||
['rear_port', 'rear_port_position'],
|
('rear_port', 'rear_port_position'),
|
||||||
]
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@ -344,6 +365,11 @@ class RearPortTemplate(ComponentTemplateModel):
|
|||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=64
|
max_length=64
|
||||||
)
|
)
|
||||||
|
_name = NaturalOrderingField(
|
||||||
|
target_field='name',
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
type = models.CharField(
|
type = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
choices=PortTypeChoices
|
choices=PortTypeChoices
|
||||||
@ -353,11 +379,9 @@ class RearPortTemplate(ComponentTemplateModel):
|
|||||||
validators=[MinValueValidator(1), MaxValueValidator(64)]
|
validators=[MinValueValidator(1), MaxValueValidator(64)]
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = NaturalOrderingManager()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['device_type', 'name']
|
ordering = ('device_type', '_name')
|
||||||
unique_together = ['device_type', 'name']
|
unique_together = ('device_type', 'name')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@ -383,12 +407,15 @@ class DeviceBayTemplate(ComponentTemplateModel):
|
|||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=50
|
max_length=50
|
||||||
)
|
)
|
||||||
|
_name = NaturalOrderingField(
|
||||||
objects = NaturalOrderingManager()
|
target_field='name',
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['device_type', 'name']
|
ordering = ('device_type', '_name')
|
||||||
unique_together = ['device_type', 'name']
|
unique_together = ('device_type', 'name')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
@ -10,9 +10,9 @@ from dcim.choices import *
|
|||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.exceptions import LoopDetected
|
from dcim.exceptions import LoopDetected
|
||||||
from dcim.fields import MACAddressField
|
from dcim.fields import MACAddressField
|
||||||
from dcim.managers import InterfaceManager
|
|
||||||
from extras.models import ObjectChange, TaggedItem
|
from extras.models import ObjectChange, TaggedItem
|
||||||
from utilities.managers import NaturalOrderingManager
|
from utilities.fields import NaturalOrderingField
|
||||||
|
from utilities.ordering import naturalize_interface
|
||||||
from utilities.utils import serialize_object
|
from utilities.utils import serialize_object
|
||||||
from virtualization.choices import VMInterfaceTypeChoices
|
from virtualization.choices import VMInterfaceTypeChoices
|
||||||
|
|
||||||
@ -181,6 +181,11 @@ class ConsolePort(CableTermination, ComponentModel):
|
|||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=50
|
max_length=50
|
||||||
)
|
)
|
||||||
|
_name = NaturalOrderingField(
|
||||||
|
target_field='name',
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
type = models.CharField(
|
type = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
choices=ConsolePortTypeChoices,
|
choices=ConsolePortTypeChoices,
|
||||||
@ -197,15 +202,13 @@ class ConsolePort(CableTermination, ComponentModel):
|
|||||||
choices=CONNECTION_STATUS_CHOICES,
|
choices=CONNECTION_STATUS_CHOICES,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = NaturalOrderingManager()
|
|
||||||
tags = TaggableManager(through=TaggedItem)
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = ['device', 'name', 'type', 'description']
|
csv_headers = ['device', 'name', 'type', 'description']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['device', 'name']
|
ordering = ('device', '_name')
|
||||||
unique_together = ['device', 'name']
|
unique_together = ('device', 'name')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@ -238,6 +241,11 @@ class ConsoleServerPort(CableTermination, ComponentModel):
|
|||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=50
|
max_length=50
|
||||||
)
|
)
|
||||||
|
_name = NaturalOrderingField(
|
||||||
|
target_field='name',
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
type = models.CharField(
|
type = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
choices=ConsolePortTypeChoices,
|
choices=ConsolePortTypeChoices,
|
||||||
@ -247,14 +255,13 @@ class ConsoleServerPort(CableTermination, ComponentModel):
|
|||||||
choices=CONNECTION_STATUS_CHOICES,
|
choices=CONNECTION_STATUS_CHOICES,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = NaturalOrderingManager()
|
|
||||||
tags = TaggableManager(through=TaggedItem)
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = ['device', 'name', 'type', 'description']
|
csv_headers = ['device', 'name', 'type', 'description']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ['device', 'name']
|
ordering = ('device', '_name')
|
||||||
|
unique_together = ('device', 'name')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@ -287,6 +294,11 @@ class PowerPort(CableTermination, ComponentModel):
|
|||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=50
|
max_length=50
|
||||||
)
|
)
|
||||||
|
_name = NaturalOrderingField(
|
||||||
|
target_field='name',
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
type = models.CharField(
|
type = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
choices=PowerPortTypeChoices,
|
choices=PowerPortTypeChoices,
|
||||||
@ -322,15 +334,13 @@ class PowerPort(CableTermination, ComponentModel):
|
|||||||
choices=CONNECTION_STATUS_CHOICES,
|
choices=CONNECTION_STATUS_CHOICES,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = NaturalOrderingManager()
|
|
||||||
tags = TaggableManager(through=TaggedItem)
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = ['device', 'name', 'type', 'maximum_draw', 'allocated_draw', 'description']
|
csv_headers = ['device', 'name', 'type', 'maximum_draw', 'allocated_draw', 'description']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['device', 'name']
|
ordering = ('device', '_name')
|
||||||
unique_together = ['device', 'name']
|
unique_together = ('device', 'name')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@ -433,6 +443,11 @@ class PowerOutlet(CableTermination, ComponentModel):
|
|||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=50
|
max_length=50
|
||||||
)
|
)
|
||||||
|
_name = NaturalOrderingField(
|
||||||
|
target_field='name',
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
type = models.CharField(
|
type = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
choices=PowerOutletTypeChoices,
|
choices=PowerOutletTypeChoices,
|
||||||
@ -455,14 +470,13 @@ class PowerOutlet(CableTermination, ComponentModel):
|
|||||||
choices=CONNECTION_STATUS_CHOICES,
|
choices=CONNECTION_STATUS_CHOICES,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = NaturalOrderingManager()
|
|
||||||
tags = TaggableManager(through=TaggedItem)
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = ['device', 'name', 'type', 'power_port', 'feed_leg', 'description']
|
csv_headers = ['device', 'name', 'type', 'power_port', 'feed_leg', 'description']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ['device', 'name']
|
ordering = ('device', '_name')
|
||||||
|
unique_together = ('device', 'name')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@ -515,6 +529,12 @@ class Interface(CableTermination, ComponentModel):
|
|||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=64
|
max_length=64
|
||||||
)
|
)
|
||||||
|
_name = NaturalOrderingField(
|
||||||
|
target_field='name',
|
||||||
|
naturalize_function=naturalize_interface,
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
_connected_interface = models.OneToOneField(
|
_connected_interface = models.OneToOneField(
|
||||||
to='self',
|
to='self',
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
@ -583,8 +603,6 @@ class Interface(CableTermination, ComponentModel):
|
|||||||
blank=True,
|
blank=True,
|
||||||
verbose_name='Tagged VLANs'
|
verbose_name='Tagged VLANs'
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = InterfaceManager()
|
|
||||||
tags = TaggableManager(through=TaggedItem)
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = [
|
csv_headers = [
|
||||||
@ -593,8 +611,9 @@ class Interface(CableTermination, ComponentModel):
|
|||||||
]
|
]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['device', 'name']
|
# TODO: ordering and unique_together should include virtual_machine
|
||||||
unique_together = ['device', 'name']
|
ordering = ('device', '_name')
|
||||||
|
unique_together = ('device', 'name')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@ -761,6 +780,11 @@ class FrontPort(CableTermination, ComponentModel):
|
|||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=64
|
max_length=64
|
||||||
)
|
)
|
||||||
|
_name = NaturalOrderingField(
|
||||||
|
target_field='name',
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
type = models.CharField(
|
type = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
choices=PortTypeChoices
|
choices=PortTypeChoices
|
||||||
@ -774,20 +798,17 @@ class FrontPort(CableTermination, ComponentModel):
|
|||||||
default=1,
|
default=1,
|
||||||
validators=[MinValueValidator(1), MaxValueValidator(64)]
|
validators=[MinValueValidator(1), MaxValueValidator(64)]
|
||||||
)
|
)
|
||||||
|
|
||||||
is_path_endpoint = False
|
|
||||||
|
|
||||||
objects = NaturalOrderingManager()
|
|
||||||
tags = TaggableManager(through=TaggedItem)
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'description']
|
csv_headers = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'description']
|
||||||
|
is_path_endpoint = False
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['device', 'name']
|
ordering = ('device', '_name')
|
||||||
unique_together = [
|
unique_together = (
|
||||||
['device', 'name'],
|
('device', 'name'),
|
||||||
['rear_port', 'rear_port_position'],
|
('rear_port', 'rear_port_position'),
|
||||||
]
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@ -831,6 +852,11 @@ class RearPort(CableTermination, ComponentModel):
|
|||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=64
|
max_length=64
|
||||||
)
|
)
|
||||||
|
_name = NaturalOrderingField(
|
||||||
|
target_field='name',
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
type = models.CharField(
|
type = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
choices=PortTypeChoices
|
choices=PortTypeChoices
|
||||||
@ -839,17 +865,14 @@ class RearPort(CableTermination, ComponentModel):
|
|||||||
default=1,
|
default=1,
|
||||||
validators=[MinValueValidator(1), MaxValueValidator(64)]
|
validators=[MinValueValidator(1), MaxValueValidator(64)]
|
||||||
)
|
)
|
||||||
|
|
||||||
is_path_endpoint = False
|
|
||||||
|
|
||||||
objects = NaturalOrderingManager()
|
|
||||||
tags = TaggableManager(through=TaggedItem)
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = ['device', 'name', 'type', 'positions', 'description']
|
csv_headers = ['device', 'name', 'type', 'positions', 'description']
|
||||||
|
is_path_endpoint = False
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['device', 'name']
|
ordering = ('device', '_name')
|
||||||
unique_together = ['device', 'name']
|
unique_together = ('device', 'name')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@ -881,6 +904,11 @@ class DeviceBay(ComponentModel):
|
|||||||
max_length=50,
|
max_length=50,
|
||||||
verbose_name='Name'
|
verbose_name='Name'
|
||||||
)
|
)
|
||||||
|
_name = NaturalOrderingField(
|
||||||
|
target_field='name',
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
installed_device = models.OneToOneField(
|
installed_device = models.OneToOneField(
|
||||||
to='dcim.Device',
|
to='dcim.Device',
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
@ -888,15 +916,13 @@ class DeviceBay(ComponentModel):
|
|||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = NaturalOrderingManager()
|
|
||||||
tags = TaggableManager(through=TaggedItem)
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = ['device', 'name', 'installed_device', 'description']
|
csv_headers = ['device', 'name', 'installed_device', 'description']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['device', 'name']
|
ordering = ('device', '_name')
|
||||||
unique_together = ['device', 'name']
|
unique_together = ('device', 'name')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '{} - {}'.format(self.device.name, self.name)
|
return '{} - {}'.format(self.device.name, self.name)
|
||||||
@ -960,6 +986,11 @@ class InventoryItem(ComponentModel):
|
|||||||
max_length=50,
|
max_length=50,
|
||||||
verbose_name='Name'
|
verbose_name='Name'
|
||||||
)
|
)
|
||||||
|
_name = NaturalOrderingField(
|
||||||
|
target_field='name',
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
manufacturer = models.ForeignKey(
|
manufacturer = models.ForeignKey(
|
||||||
to='dcim.Manufacturer',
|
to='dcim.Manufacturer',
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
@ -997,8 +1028,8 @@ class InventoryItem(ComponentModel):
|
|||||||
]
|
]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['device__id', 'parent__id', 'name']
|
ordering = ('device__id', 'parent__id', '_name')
|
||||||
unique_together = ['device', 'parent', 'name']
|
unique_together = ('device', 'parent', 'name')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
@ -200,6 +200,11 @@ def get_component_template_actions(model_name):
|
|||||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{{% endif %}}
|
{{% endif %}}
|
||||||
|
{{% if perms.dcim.delete_{model_name} %}}
|
||||||
|
<a href="{{% url 'dcim:{model_name}_delete' pk=record.pk %}}?return_url={{{{ request.path }}}}" class="btn btn-xs btn-danger">
|
||||||
|
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
{{% endif %}}
|
||||||
""".format(model_name=model_name).strip()
|
""".format(model_name=model_name).strip()
|
||||||
|
|
||||||
|
|
||||||
@ -229,7 +234,7 @@ class RegionTable(BaseTable):
|
|||||||
|
|
||||||
class SiteTable(BaseTable):
|
class SiteTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
name = tables.LinkColumn(order_by=('_nat1', '_nat2', '_nat3'))
|
name = tables.LinkColumn(order_by=('_name',))
|
||||||
status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
|
status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
|
||||||
region = tables.TemplateColumn(template_code=SITE_REGION_LINK)
|
region = tables.TemplateColumn(template_code=SITE_REGION_LINK)
|
||||||
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
||||||
@ -291,7 +296,7 @@ class RackRoleTable(BaseTable):
|
|||||||
|
|
||||||
class RackTable(BaseTable):
|
class RackTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
name = tables.LinkColumn(order_by=('_nat1', '_nat2', '_nat3'))
|
name = tables.LinkColumn(order_by=('_name',))
|
||||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
||||||
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
|
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
|
||||||
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
||||||
@ -409,6 +414,7 @@ class DeviceTypeTable(BaseTable):
|
|||||||
|
|
||||||
class ConsolePortTemplateTable(BaseTable):
|
class ConsolePortTemplateTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
|
name = tables.Column(order_by=('_name',))
|
||||||
actions = tables.TemplateColumn(
|
actions = tables.TemplateColumn(
|
||||||
template_code=get_component_template_actions('consoleporttemplate'),
|
template_code=get_component_template_actions('consoleporttemplate'),
|
||||||
attrs={'td': {'class': 'text-right noprint'}},
|
attrs={'td': {'class': 'text-right noprint'}},
|
||||||
@ -432,6 +438,7 @@ class ConsolePortImportTable(BaseTable):
|
|||||||
|
|
||||||
class ConsoleServerPortTemplateTable(BaseTable):
|
class ConsoleServerPortTemplateTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
|
name = tables.Column(order_by=('_name',))
|
||||||
actions = tables.TemplateColumn(
|
actions = tables.TemplateColumn(
|
||||||
template_code=get_component_template_actions('consoleserverporttemplate'),
|
template_code=get_component_template_actions('consoleserverporttemplate'),
|
||||||
attrs={'td': {'class': 'text-right noprint'}},
|
attrs={'td': {'class': 'text-right noprint'}},
|
||||||
@ -455,6 +462,7 @@ class ConsoleServerPortImportTable(BaseTable):
|
|||||||
|
|
||||||
class PowerPortTemplateTable(BaseTable):
|
class PowerPortTemplateTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
|
name = tables.Column(order_by=('_name',))
|
||||||
actions = tables.TemplateColumn(
|
actions = tables.TemplateColumn(
|
||||||
template_code=get_component_template_actions('powerporttemplate'),
|
template_code=get_component_template_actions('powerporttemplate'),
|
||||||
attrs={'td': {'class': 'text-right noprint'}},
|
attrs={'td': {'class': 'text-right noprint'}},
|
||||||
@ -478,6 +486,7 @@ class PowerPortImportTable(BaseTable):
|
|||||||
|
|
||||||
class PowerOutletTemplateTable(BaseTable):
|
class PowerOutletTemplateTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
|
name = tables.Column(order_by=('_name',))
|
||||||
actions = tables.TemplateColumn(
|
actions = tables.TemplateColumn(
|
||||||
template_code=get_component_template_actions('poweroutlettemplate'),
|
template_code=get_component_template_actions('poweroutlettemplate'),
|
||||||
attrs={'td': {'class': 'text-right noprint'}},
|
attrs={'td': {'class': 'text-right noprint'}},
|
||||||
@ -526,6 +535,7 @@ class InterfaceImportTable(BaseTable):
|
|||||||
|
|
||||||
class FrontPortTemplateTable(BaseTable):
|
class FrontPortTemplateTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
|
name = tables.Column(order_by=('_name',))
|
||||||
rear_port_position = tables.Column(
|
rear_port_position = tables.Column(
|
||||||
verbose_name='Position'
|
verbose_name='Position'
|
||||||
)
|
)
|
||||||
@ -552,6 +562,7 @@ class FrontPortImportTable(BaseTable):
|
|||||||
|
|
||||||
class RearPortTemplateTable(BaseTable):
|
class RearPortTemplateTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
|
name = tables.Column(order_by=('_name',))
|
||||||
actions = tables.TemplateColumn(
|
actions = tables.TemplateColumn(
|
||||||
template_code=get_component_template_actions('rearporttemplate'),
|
template_code=get_component_template_actions('rearporttemplate'),
|
||||||
attrs={'td': {'class': 'text-right noprint'}},
|
attrs={'td': {'class': 'text-right noprint'}},
|
||||||
@ -575,6 +586,7 @@ class RearPortImportTable(BaseTable):
|
|||||||
|
|
||||||
class DeviceBayTemplateTable(BaseTable):
|
class DeviceBayTemplateTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
|
name = tables.Column(order_by=('_name',))
|
||||||
actions = tables.TemplateColumn(
|
actions = tables.TemplateColumn(
|
||||||
template_code=get_component_template_actions('devicebaytemplate'),
|
template_code=get_component_template_actions('devicebaytemplate'),
|
||||||
attrs={'td': {'class': 'text-right noprint'}},
|
attrs={'td': {'class': 'text-right noprint'}},
|
||||||
@ -654,7 +666,7 @@ class PlatformTable(BaseTable):
|
|||||||
class DeviceTable(BaseTable):
|
class DeviceTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
name = tables.TemplateColumn(
|
name = tables.TemplateColumn(
|
||||||
order_by=('_nat1', '_nat2', '_nat3'),
|
order_by=('_name',),
|
||||||
template_code=DEVICE_LINK
|
template_code=DEVICE_LINK
|
||||||
)
|
)
|
||||||
status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
|
status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
|
||||||
@ -704,6 +716,7 @@ class DeviceImportTable(BaseTable):
|
|||||||
|
|
||||||
class DeviceComponentDetailTable(BaseTable):
|
class DeviceComponentDetailTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
|
name = tables.Column(order_by=('_name',))
|
||||||
cable = tables.LinkColumn()
|
cable = tables.LinkColumn()
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
@ -713,6 +726,7 @@ class DeviceComponentDetailTable(BaseTable):
|
|||||||
|
|
||||||
|
|
||||||
class ConsolePortTable(BaseTable):
|
class ConsolePortTable(BaseTable):
|
||||||
|
name = tables.Column(order_by=('_name',))
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = ConsolePort
|
model = ConsolePort
|
||||||
@ -727,6 +741,7 @@ class ConsolePortDetailTable(DeviceComponentDetailTable):
|
|||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortTable(BaseTable):
|
class ConsoleServerPortTable(BaseTable):
|
||||||
|
name = tables.Column(order_by=('_name',))
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = ConsoleServerPort
|
model = ConsoleServerPort
|
||||||
@ -741,6 +756,7 @@ class ConsoleServerPortDetailTable(DeviceComponentDetailTable):
|
|||||||
|
|
||||||
|
|
||||||
class PowerPortTable(BaseTable):
|
class PowerPortTable(BaseTable):
|
||||||
|
name = tables.Column(order_by=('_name',))
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = PowerPort
|
model = PowerPort
|
||||||
@ -755,6 +771,7 @@ class PowerPortDetailTable(DeviceComponentDetailTable):
|
|||||||
|
|
||||||
|
|
||||||
class PowerOutletTable(BaseTable):
|
class PowerOutletTable(BaseTable):
|
||||||
|
name = tables.Column(order_by=('_name',))
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = PowerOutlet
|
model = PowerOutlet
|
||||||
@ -786,6 +803,7 @@ class InterfaceDetailTable(DeviceComponentDetailTable):
|
|||||||
|
|
||||||
|
|
||||||
class FrontPortTable(BaseTable):
|
class FrontPortTable(BaseTable):
|
||||||
|
name = tables.Column(order_by=('_name',))
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = FrontPort
|
model = FrontPort
|
||||||
@ -801,6 +819,7 @@ class FrontPortDetailTable(DeviceComponentDetailTable):
|
|||||||
|
|
||||||
|
|
||||||
class RearPortTable(BaseTable):
|
class RearPortTable(BaseTable):
|
||||||
|
name = tables.Column(order_by=('_name',))
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = RearPort
|
model = RearPort
|
||||||
@ -816,6 +835,7 @@ class RearPortDetailTable(DeviceComponentDetailTable):
|
|||||||
|
|
||||||
|
|
||||||
class DeviceBayTable(BaseTable):
|
class DeviceBayTable(BaseTable):
|
||||||
|
name = tables.Column(order_by=('_name',))
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = DeviceBay
|
model = DeviceBay
|
||||||
|
@ -535,7 +535,6 @@ class ConsolePortTemplateTestCase(StandardTestCases.Views):
|
|||||||
test_get_object = None
|
test_get_object = None
|
||||||
test_list_objects = None
|
test_list_objects = None
|
||||||
test_create_object = None
|
test_create_object = None
|
||||||
test_delete_object = None
|
|
||||||
test_import_objects = None
|
test_import_objects = None
|
||||||
|
|
||||||
def test_bulk_create_objects(self):
|
def test_bulk_create_objects(self):
|
||||||
@ -580,7 +579,6 @@ class ConsoleServerPortTemplateTestCase(StandardTestCases.Views):
|
|||||||
test_get_object = None
|
test_get_object = None
|
||||||
test_list_objects = None
|
test_list_objects = None
|
||||||
test_create_object = None
|
test_create_object = None
|
||||||
test_delete_object = None
|
|
||||||
test_import_objects = None
|
test_import_objects = None
|
||||||
|
|
||||||
def test_bulk_create_objects(self):
|
def test_bulk_create_objects(self):
|
||||||
@ -625,7 +623,6 @@ class PowerPortTemplateTestCase(StandardTestCases.Views):
|
|||||||
test_get_object = None
|
test_get_object = None
|
||||||
test_list_objects = None
|
test_list_objects = None
|
||||||
test_create_object = None
|
test_create_object = None
|
||||||
test_delete_object = None
|
|
||||||
test_import_objects = None
|
test_import_objects = None
|
||||||
|
|
||||||
def test_bulk_create_objects(self):
|
def test_bulk_create_objects(self):
|
||||||
@ -676,7 +673,6 @@ class PowerOutletTemplateTestCase(StandardTestCases.Views):
|
|||||||
test_get_object = None
|
test_get_object = None
|
||||||
test_list_objects = None
|
test_list_objects = None
|
||||||
test_create_object = None
|
test_create_object = None
|
||||||
test_delete_object = None
|
|
||||||
test_import_objects = None
|
test_import_objects = None
|
||||||
|
|
||||||
def test_bulk_create_objects(self):
|
def test_bulk_create_objects(self):
|
||||||
@ -727,7 +723,6 @@ class InterfaceTemplateTestCase(StandardTestCases.Views):
|
|||||||
test_get_object = None
|
test_get_object = None
|
||||||
test_list_objects = None
|
test_list_objects = None
|
||||||
test_create_object = None
|
test_create_object = None
|
||||||
test_delete_object = None
|
|
||||||
test_import_objects = None
|
test_import_objects = None
|
||||||
|
|
||||||
def test_bulk_create_objects(self):
|
def test_bulk_create_objects(self):
|
||||||
@ -775,7 +770,6 @@ class FrontPortTemplateTestCase(StandardTestCases.Views):
|
|||||||
test_get_object = None
|
test_get_object = None
|
||||||
test_list_objects = None
|
test_list_objects = None
|
||||||
test_create_object = None
|
test_create_object = None
|
||||||
test_delete_object = None
|
|
||||||
test_import_objects = None
|
test_import_objects = None
|
||||||
|
|
||||||
def test_bulk_create_objects(self):
|
def test_bulk_create_objects(self):
|
||||||
@ -831,7 +825,6 @@ class RearPortTemplateTestCase(StandardTestCases.Views):
|
|||||||
test_get_object = None
|
test_get_object = None
|
||||||
test_list_objects = None
|
test_list_objects = None
|
||||||
test_create_object = None
|
test_create_object = None
|
||||||
test_delete_object = None
|
|
||||||
test_import_objects = None
|
test_import_objects = None
|
||||||
|
|
||||||
def test_bulk_create_objects(self):
|
def test_bulk_create_objects(self):
|
||||||
@ -878,7 +871,6 @@ class DeviceBayTemplateTestCase(StandardTestCases.Views):
|
|||||||
test_get_object = None
|
test_get_object = None
|
||||||
test_list_objects = None
|
test_list_objects = None
|
||||||
test_create_object = None
|
test_create_object = None
|
||||||
test_delete_object = None
|
|
||||||
test_import_objects = None
|
test_import_objects = None
|
||||||
test_bulk_edit_objects = None
|
test_bulk_edit_objects = None
|
||||||
|
|
||||||
@ -1070,7 +1062,6 @@ class ConsolePortTestCase(StandardTestCases.Views):
|
|||||||
# Disable inapplicable views
|
# Disable inapplicable views
|
||||||
test_get_object = None
|
test_get_object = None
|
||||||
test_create_object = None
|
test_create_object = None
|
||||||
test_bulk_edit_objects = None
|
|
||||||
|
|
||||||
def test_bulk_create_objects(self):
|
def test_bulk_create_objects(self):
|
||||||
return self._test_bulk_create_objects(expected_count=3)
|
return self._test_bulk_create_objects(expected_count=3)
|
||||||
@ -1101,6 +1092,11 @@ class ConsolePortTestCase(StandardTestCases.Views):
|
|||||||
'tags': 'Alpha,Bravo,Charlie',
|
'tags': 'Alpha,Bravo,Charlie',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cls.bulk_edit_data = {
|
||||||
|
'type': ConsolePortTypeChoices.TYPE_RJ45,
|
||||||
|
'description': 'New description',
|
||||||
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
"device,name",
|
"device,name",
|
||||||
"Device 1,Console Port 4",
|
"Device 1,Console Port 4",
|
||||||
@ -1164,7 +1160,6 @@ class PowerPortTestCase(StandardTestCases.Views):
|
|||||||
|
|
||||||
# Disable inapplicable views
|
# Disable inapplicable views
|
||||||
test_get_object = None
|
test_get_object = None
|
||||||
test_bulk_edit_objects = None
|
|
||||||
test_create_object = None
|
test_create_object = None
|
||||||
|
|
||||||
def test_bulk_create_objects(self):
|
def test_bulk_create_objects(self):
|
||||||
@ -1200,6 +1195,13 @@ class PowerPortTestCase(StandardTestCases.Views):
|
|||||||
'tags': 'Alpha,Bravo,Charlie',
|
'tags': 'Alpha,Bravo,Charlie',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cls.bulk_edit_data = {
|
||||||
|
'type': PowerPortTypeChoices.TYPE_IEC_C14,
|
||||||
|
'maximum_draw': 100,
|
||||||
|
'allocated_draw': 50,
|
||||||
|
'description': 'New description',
|
||||||
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
"device,name",
|
"device,name",
|
||||||
"Device 1,Power Port 4",
|
"Device 1,Power Port 4",
|
||||||
|
@ -95,48 +95,56 @@ urlpatterns = [
|
|||||||
path('console-port-templates/edit/', views.ConsolePortTemplateBulkEditView.as_view(), name='consoleporttemplate_bulk_edit'),
|
path('console-port-templates/edit/', views.ConsolePortTemplateBulkEditView.as_view(), name='consoleporttemplate_bulk_edit'),
|
||||||
path('console-port-templates/delete/', views.ConsolePortTemplateBulkDeleteView.as_view(), name='consoleporttemplate_bulk_delete'),
|
path('console-port-templates/delete/', views.ConsolePortTemplateBulkDeleteView.as_view(), name='consoleporttemplate_bulk_delete'),
|
||||||
path('console-port-templates/<int:pk>/edit/', views.ConsolePortTemplateEditView.as_view(), name='consoleporttemplate_edit'),
|
path('console-port-templates/<int:pk>/edit/', views.ConsolePortTemplateEditView.as_view(), name='consoleporttemplate_edit'),
|
||||||
|
path('console-port-templates/<int:pk>/delete/', views.ConsolePortTemplateDeleteView.as_view(), name='consoleporttemplate_delete'),
|
||||||
|
|
||||||
# Console server port templates
|
# Console server port templates
|
||||||
path('console-server-port-templates/add/', views.ConsoleServerPortTemplateCreateView.as_view(), name='consoleserverporttemplate_add'),
|
path('console-server-port-templates/add/', views.ConsoleServerPortTemplateCreateView.as_view(), name='consoleserverporttemplate_add'),
|
||||||
path('console-server-port-templates/edit/', views.ConsoleServerPortTemplateBulkEditView.as_view(), name='consoleserverporttemplate_bulk_edit'),
|
path('console-server-port-templates/edit/', views.ConsoleServerPortTemplateBulkEditView.as_view(), name='consoleserverporttemplate_bulk_edit'),
|
||||||
path('console-server-port-templates/delete/', views.ConsoleServerPortTemplateBulkDeleteView.as_view(), name='consoleserverporttemplate_bulk_delete'),
|
path('console-server-port-templates/delete/', views.ConsoleServerPortTemplateBulkDeleteView.as_view(), name='consoleserverporttemplate_bulk_delete'),
|
||||||
path('console-server-port-templates/<int:pk>/edit/', views.ConsoleServerPortTemplateEditView.as_view(), name='consoleserverporttemplate_edit'),
|
path('console-server-port-templates/<int:pk>/edit/', views.ConsoleServerPortTemplateEditView.as_view(), name='consoleserverporttemplate_edit'),
|
||||||
|
path('console-server-port-templates/<int:pk>/delete/', views.ConsoleServerPortTemplateDeleteView.as_view(), name='consoleserverporttemplate_delete'),
|
||||||
|
|
||||||
# Power port templates
|
# Power port templates
|
||||||
path('power-port-templates/add/', views.PowerPortTemplateCreateView.as_view(), name='powerporttemplate_add'),
|
path('power-port-templates/add/', views.PowerPortTemplateCreateView.as_view(), name='powerporttemplate_add'),
|
||||||
path('power-port-templates/edit/', views.PowerPortTemplateBulkEditView.as_view(), name='powerporttemplate_bulk_edit'),
|
path('power-port-templates/edit/', views.PowerPortTemplateBulkEditView.as_view(), name='powerporttemplate_bulk_edit'),
|
||||||
path('power-port-templates/delete/', views.PowerPortTemplateBulkDeleteView.as_view(), name='powerporttemplate_bulk_delete'),
|
path('power-port-templates/delete/', views.PowerPortTemplateBulkDeleteView.as_view(), name='powerporttemplate_bulk_delete'),
|
||||||
path('power-port-templates/<int:pk>/edit/', views.PowerPortTemplateEditView.as_view(), name='powerporttemplate_edit'),
|
path('power-port-templates/<int:pk>/edit/', views.PowerPortTemplateEditView.as_view(), name='powerporttemplate_edit'),
|
||||||
|
path('power-port-templates/<int:pk>/delete/', views.PowerPortTemplateDeleteView.as_view(), name='powerporttemplate_delete'),
|
||||||
|
|
||||||
# Power outlet templates
|
# Power outlet templates
|
||||||
path('power-outlet-templates/add/', views.PowerOutletTemplateCreateView.as_view(), name='poweroutlettemplate_add'),
|
path('power-outlet-templates/add/', views.PowerOutletTemplateCreateView.as_view(), name='poweroutlettemplate_add'),
|
||||||
path('power-outlet-templates/edit/', views.PowerOutletTemplateBulkEditView.as_view(), name='poweroutlettemplate_bulk_edit'),
|
path('power-outlet-templates/edit/', views.PowerOutletTemplateBulkEditView.as_view(), name='poweroutlettemplate_bulk_edit'),
|
||||||
path('power-outlet-templates/delete/', views.PowerOutletTemplateBulkDeleteView.as_view(), name='poweroutlettemplate_bulk_delete'),
|
path('power-outlet-templates/delete/', views.PowerOutletTemplateBulkDeleteView.as_view(), name='poweroutlettemplate_bulk_delete'),
|
||||||
path('power-outlet-templates/<int:pk>/edit/', views.PowerOutletTemplateEditView.as_view(), name='poweroutlettemplate_edit'),
|
path('power-outlet-templates/<int:pk>/edit/', views.PowerOutletTemplateEditView.as_view(), name='poweroutlettemplate_edit'),
|
||||||
|
path('power-outlet-templates/<int:pk>/delete/', views.PowerOutletTemplateDeleteView.as_view(), name='poweroutlettemplate_delete'),
|
||||||
|
|
||||||
# Interface templates
|
# Interface templates
|
||||||
path('interface-templates/add/', views.InterfaceTemplateCreateView.as_view(), name='interfacetemplate_add'),
|
path('interface-templates/add/', views.InterfaceTemplateCreateView.as_view(), name='interfacetemplate_add'),
|
||||||
path('interface-templates/edit/', views.InterfaceTemplateBulkEditView.as_view(), name='interfacetemplate_bulk_edit'),
|
path('interface-templates/edit/', views.InterfaceTemplateBulkEditView.as_view(), name='interfacetemplate_bulk_edit'),
|
||||||
path('interface-templates/delete/', views.InterfaceTemplateBulkDeleteView.as_view(), name='interfacetemplate_bulk_delete'),
|
path('interface-templates/delete/', views.InterfaceTemplateBulkDeleteView.as_view(), name='interfacetemplate_bulk_delete'),
|
||||||
path('interface-templates/<int:pk>/edit/', views.InterfaceTemplateEditView.as_view(), name='interfacetemplate_edit'),
|
path('interface-templates/<int:pk>/edit/', views.InterfaceTemplateEditView.as_view(), name='interfacetemplate_edit'),
|
||||||
|
path('interface-templates/<int:pk>/delete/', views.InterfaceTemplateDeleteView.as_view(), name='interfacetemplate_delete'),
|
||||||
|
|
||||||
# Front port templates
|
# Front port templates
|
||||||
path('front-port-templates/add/', views.FrontPortTemplateCreateView.as_view(), name='frontporttemplate_add'),
|
path('front-port-templates/add/', views.FrontPortTemplateCreateView.as_view(), name='frontporttemplate_add'),
|
||||||
path('front-port-templates/edit/', views.FrontPortTemplateBulkEditView.as_view(), name='frontporttemplate_bulk_edit'),
|
path('front-port-templates/edit/', views.FrontPortTemplateBulkEditView.as_view(), name='frontporttemplate_bulk_edit'),
|
||||||
path('front-port-templates/delete/', views.FrontPortTemplateBulkDeleteView.as_view(), name='frontporttemplate_bulk_delete'),
|
path('front-port-templates/delete/', views.FrontPortTemplateBulkDeleteView.as_view(), name='frontporttemplate_bulk_delete'),
|
||||||
path('front-port-templates/<int:pk>/edit/', views.FrontPortTemplateEditView.as_view(), name='frontporttemplate_edit'),
|
path('front-port-templates/<int:pk>/edit/', views.FrontPortTemplateEditView.as_view(), name='frontporttemplate_edit'),
|
||||||
|
path('front-port-templates/<int:pk>/delete/', views.FrontPortTemplateDeleteView.as_view(), name='frontporttemplate_delete'),
|
||||||
|
|
||||||
# Rear port templates
|
# Rear port templates
|
||||||
path('rear-port-templates/add/', views.RearPortTemplateCreateView.as_view(), name='rearporttemplate_add'),
|
path('rear-port-templates/add/', views.RearPortTemplateCreateView.as_view(), name='rearporttemplate_add'),
|
||||||
path('rear-port-templates/edit/', views.RearPortTemplateBulkEditView.as_view(), name='rearporttemplate_bulk_edit'),
|
path('rear-port-templates/edit/', views.RearPortTemplateBulkEditView.as_view(), name='rearporttemplate_bulk_edit'),
|
||||||
path('rear-port-templates/delete/', views.RearPortTemplateBulkDeleteView.as_view(), name='rearporttemplate_bulk_delete'),
|
path('rear-port-templates/delete/', views.RearPortTemplateBulkDeleteView.as_view(), name='rearporttemplate_bulk_delete'),
|
||||||
path('rear-port-templates/<int:pk>/edit/', views.RearPortTemplateEditView.as_view(), name='rearporttemplate_edit'),
|
path('rear-port-templates/<int:pk>/edit/', views.RearPortTemplateEditView.as_view(), name='rearporttemplate_edit'),
|
||||||
|
path('rear-port-templates/<int:pk>/delete/', views.RearPortTemplateDeleteView.as_view(), name='rearporttemplate_delete'),
|
||||||
|
|
||||||
# Device bay templates
|
# Device bay templates
|
||||||
path('device-bay-templates/add/', views.DeviceBayTemplateCreateView.as_view(), name='devicebaytemplate_add'),
|
path('device-bay-templates/add/', views.DeviceBayTemplateCreateView.as_view(), name='devicebaytemplate_add'),
|
||||||
# path('device-bay-templates/edit/', views.DeviceBayTemplateBulkEditView.as_view(), name='devicebaytemplate_bulk_edit'),
|
# path('device-bay-templates/edit/', views.DeviceBayTemplateBulkEditView.as_view(), name='devicebaytemplate_bulk_edit'),
|
||||||
path('device-bay-templates/delete/', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicebaytemplate_bulk_delete'),
|
path('device-bay-templates/delete/', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicebaytemplate_bulk_delete'),
|
||||||
path('device-bay-templates/<int:pk>/edit/', views.DeviceBayTemplateEditView.as_view(), name='devicebaytemplate_edit'),
|
path('device-bay-templates/<int:pk>/edit/', views.DeviceBayTemplateEditView.as_view(), name='devicebaytemplate_edit'),
|
||||||
|
path('device-bay-templates/<int:pk>/delete/', views.DeviceBayTemplateDeleteView.as_view(), name='devicebaytemplate_delete'),
|
||||||
|
|
||||||
# Device roles
|
# Device roles
|
||||||
path('device-roles/', views.DeviceRoleListView.as_view(), name='devicerole_list'),
|
path('device-roles/', views.DeviceRoleListView.as_view(), name='devicerole_list'),
|
||||||
@ -178,7 +186,8 @@ urlpatterns = [
|
|||||||
path('console-ports/', views.ConsolePortListView.as_view(), name='consoleport_list'),
|
path('console-ports/', views.ConsolePortListView.as_view(), name='consoleport_list'),
|
||||||
path('console-ports/add/', views.ConsolePortCreateView.as_view(), name='consoleport_add'),
|
path('console-ports/add/', views.ConsolePortCreateView.as_view(), name='consoleport_add'),
|
||||||
path('console-ports/import/', views.ConsolePortBulkImportView.as_view(), name='consoleport_import'),
|
path('console-ports/import/', views.ConsolePortBulkImportView.as_view(), name='consoleport_import'),
|
||||||
# TODO: Bulk edit, rename, disconnect views for ConsolePorts
|
path('console-ports/edit/', views.ConsolePortBulkEditView.as_view(), name='consoleport_bulk_edit'),
|
||||||
|
# TODO: Bulk rename, disconnect views for ConsolePorts
|
||||||
path('console-ports/delete/', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'),
|
path('console-ports/delete/', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'),
|
||||||
path('console-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}),
|
path('console-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}),
|
||||||
path('console-ports/<int:pk>/edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'),
|
path('console-ports/<int:pk>/edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'),
|
||||||
@ -204,7 +213,8 @@ urlpatterns = [
|
|||||||
path('power-ports/', views.PowerPortListView.as_view(), name='powerport_list'),
|
path('power-ports/', views.PowerPortListView.as_view(), name='powerport_list'),
|
||||||
path('power-ports/add/', views.PowerPortCreateView.as_view(), name='powerport_add'),
|
path('power-ports/add/', views.PowerPortCreateView.as_view(), name='powerport_add'),
|
||||||
path('power-ports/import/', views.PowerPortBulkImportView.as_view(), name='powerport_import'),
|
path('power-ports/import/', views.PowerPortBulkImportView.as_view(), name='powerport_import'),
|
||||||
# TODO: Bulk edit, rename, disconnect views for PowerPorts
|
path('power-ports/edit/', views.PowerPortBulkEditView.as_view(), name='powerport_bulk_edit'),
|
||||||
|
# TODO: Bulk rename, disconnect views for PowerPorts
|
||||||
path('power-ports/delete/', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'),
|
path('power-ports/delete/', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'),
|
||||||
path('power-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}),
|
path('power-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}),
|
||||||
path('power-ports/<int:pk>/edit/', views.PowerPortEditView.as_view(), name='powerport_edit'),
|
path('power-ports/<int:pk>/edit/', views.PowerPortEditView.as_view(), name='powerport_edit'),
|
||||||
|
@ -700,7 +700,7 @@ class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Device type components
|
# Console port templates
|
||||||
#
|
#
|
||||||
|
|
||||||
class ConsolePortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
class ConsolePortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
@ -717,6 +717,11 @@ class ConsolePortTemplateEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
model_form = forms.ConsolePortTemplateForm
|
model_form = forms.ConsolePortTemplateForm
|
||||||
|
|
||||||
|
|
||||||
|
class ConsolePortTemplateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
|
permission_required = 'dcim.delete_consoleporttemplate'
|
||||||
|
model = ConsolePortTemplate
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
class ConsolePortTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
permission_required = 'dcim.change_consoleporttemplate'
|
permission_required = 'dcim.change_consoleporttemplate'
|
||||||
queryset = ConsolePortTemplate.objects.all()
|
queryset = ConsolePortTemplate.objects.all()
|
||||||
@ -730,6 +735,10 @@ class ConsolePortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView)
|
|||||||
table = tables.ConsolePortTemplateTable
|
table = tables.ConsolePortTemplateTable
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Console server port templates
|
||||||
|
#
|
||||||
|
|
||||||
class ConsoleServerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
class ConsoleServerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
permission_required = 'dcim.add_consoleserverporttemplate'
|
permission_required = 'dcim.add_consoleserverporttemplate'
|
||||||
model = ConsoleServerPortTemplate
|
model = ConsoleServerPortTemplate
|
||||||
@ -744,6 +753,11 @@ class ConsoleServerPortTemplateEditView(PermissionRequiredMixin, ObjectEditView)
|
|||||||
model_form = forms.ConsoleServerPortTemplateForm
|
model_form = forms.ConsoleServerPortTemplateForm
|
||||||
|
|
||||||
|
|
||||||
|
class ConsoleServerPortTemplateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
|
permission_required = 'dcim.delete_consoleserverporttemplate'
|
||||||
|
model = ConsoleServerPortTemplate
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
class ConsoleServerPortTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
permission_required = 'dcim.change_consoleserverporttemplate'
|
permission_required = 'dcim.change_consoleserverporttemplate'
|
||||||
queryset = ConsoleServerPortTemplate.objects.all()
|
queryset = ConsoleServerPortTemplate.objects.all()
|
||||||
@ -757,6 +771,10 @@ class ConsoleServerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDelet
|
|||||||
table = tables.ConsoleServerPortTemplateTable
|
table = tables.ConsoleServerPortTemplateTable
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Power port templates
|
||||||
|
#
|
||||||
|
|
||||||
class PowerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
class PowerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
permission_required = 'dcim.add_powerporttemplate'
|
permission_required = 'dcim.add_powerporttemplate'
|
||||||
model = PowerPortTemplate
|
model = PowerPortTemplate
|
||||||
@ -771,6 +789,11 @@ class PowerPortTemplateEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
model_form = forms.PowerPortTemplateForm
|
model_form = forms.PowerPortTemplateForm
|
||||||
|
|
||||||
|
|
||||||
|
class PowerPortTemplateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
|
permission_required = 'dcim.delete_powerporttemplate'
|
||||||
|
model = PowerPortTemplate
|
||||||
|
|
||||||
|
|
||||||
class PowerPortTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
class PowerPortTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
permission_required = 'dcim.change_powerporttemplate'
|
permission_required = 'dcim.change_powerporttemplate'
|
||||||
queryset = PowerPortTemplate.objects.all()
|
queryset = PowerPortTemplate.objects.all()
|
||||||
@ -784,6 +807,10 @@ class PowerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
table = tables.PowerPortTemplateTable
|
table = tables.PowerPortTemplateTable
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Power outlet templates
|
||||||
|
#
|
||||||
|
|
||||||
class PowerOutletTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
class PowerOutletTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
permission_required = 'dcim.add_poweroutlettemplate'
|
permission_required = 'dcim.add_poweroutlettemplate'
|
||||||
model = PowerOutletTemplate
|
model = PowerOutletTemplate
|
||||||
@ -798,6 +825,11 @@ class PowerOutletTemplateEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
model_form = forms.PowerOutletTemplateForm
|
model_form = forms.PowerOutletTemplateForm
|
||||||
|
|
||||||
|
|
||||||
|
class PowerOutletTemplateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
|
permission_required = 'dcim.delete_poweroutlettemplate'
|
||||||
|
model = PowerOutletTemplate
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
class PowerOutletTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
permission_required = 'dcim.change_poweroutlettemplate'
|
permission_required = 'dcim.change_poweroutlettemplate'
|
||||||
queryset = PowerOutletTemplate.objects.all()
|
queryset = PowerOutletTemplate.objects.all()
|
||||||
@ -811,6 +843,10 @@ class PowerOutletTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView)
|
|||||||
table = tables.PowerOutletTemplateTable
|
table = tables.PowerOutletTemplateTable
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Interface templates
|
||||||
|
#
|
||||||
|
|
||||||
class InterfaceTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
class InterfaceTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
permission_required = 'dcim.add_interfacetemplate'
|
permission_required = 'dcim.add_interfacetemplate'
|
||||||
model = InterfaceTemplate
|
model = InterfaceTemplate
|
||||||
@ -825,6 +861,11 @@ class InterfaceTemplateEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
model_form = forms.InterfaceTemplateForm
|
model_form = forms.InterfaceTemplateForm
|
||||||
|
|
||||||
|
|
||||||
|
class InterfaceTemplateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
|
permission_required = 'dcim.delete_interfacetemplate'
|
||||||
|
model = InterfaceTemplate
|
||||||
|
|
||||||
|
|
||||||
class InterfaceTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
class InterfaceTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
permission_required = 'dcim.change_interfacetemplate'
|
permission_required = 'dcim.change_interfacetemplate'
|
||||||
queryset = InterfaceTemplate.objects.all()
|
queryset = InterfaceTemplate.objects.all()
|
||||||
@ -838,6 +879,10 @@ class InterfaceTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
table = tables.InterfaceTemplateTable
|
table = tables.InterfaceTemplateTable
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Front port templates
|
||||||
|
#
|
||||||
|
|
||||||
class FrontPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
class FrontPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
permission_required = 'dcim.add_frontporttemplate'
|
permission_required = 'dcim.add_frontporttemplate'
|
||||||
model = FrontPortTemplate
|
model = FrontPortTemplate
|
||||||
@ -852,6 +897,11 @@ class FrontPortTemplateEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
model_form = forms.FrontPortTemplateForm
|
model_form = forms.FrontPortTemplateForm
|
||||||
|
|
||||||
|
|
||||||
|
class FrontPortTemplateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
|
permission_required = 'dcim.delete_frontporttemplate'
|
||||||
|
model = FrontPortTemplate
|
||||||
|
|
||||||
|
|
||||||
class FrontPortTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
class FrontPortTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
permission_required = 'dcim.change_frontporttemplate'
|
permission_required = 'dcim.change_frontporttemplate'
|
||||||
queryset = FrontPortTemplate.objects.all()
|
queryset = FrontPortTemplate.objects.all()
|
||||||
@ -865,6 +915,10 @@ class FrontPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
table = tables.FrontPortTemplateTable
|
table = tables.FrontPortTemplateTable
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Rear port templates
|
||||||
|
#
|
||||||
|
|
||||||
class RearPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
class RearPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
permission_required = 'dcim.add_rearporttemplate'
|
permission_required = 'dcim.add_rearporttemplate'
|
||||||
model = RearPortTemplate
|
model = RearPortTemplate
|
||||||
@ -879,6 +933,11 @@ class RearPortTemplateEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
model_form = forms.RearPortTemplateForm
|
model_form = forms.RearPortTemplateForm
|
||||||
|
|
||||||
|
|
||||||
|
class RearPortTemplateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
|
permission_required = 'dcim.delete_rearporttemplate'
|
||||||
|
model = RearPortTemplate
|
||||||
|
|
||||||
|
|
||||||
class RearPortTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
class RearPortTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
permission_required = 'dcim.change_rearporttemplate'
|
permission_required = 'dcim.change_rearporttemplate'
|
||||||
queryset = RearPortTemplate.objects.all()
|
queryset = RearPortTemplate.objects.all()
|
||||||
@ -892,6 +951,10 @@ class RearPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
table = tables.RearPortTemplateTable
|
table = tables.RearPortTemplateTable
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Device bay templates
|
||||||
|
#
|
||||||
|
|
||||||
class DeviceBayTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
class DeviceBayTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
permission_required = 'dcim.add_devicebaytemplate'
|
permission_required = 'dcim.add_devicebaytemplate'
|
||||||
model = DeviceBayTemplate
|
model = DeviceBayTemplate
|
||||||
@ -906,6 +969,11 @@ class DeviceBayTemplateEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
model_form = forms.DeviceBayTemplateForm
|
model_form = forms.DeviceBayTemplateForm
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceBayTemplateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
|
permission_required = 'dcim.delete_devicebaytemplate'
|
||||||
|
model = DeviceBayTemplate
|
||||||
|
|
||||||
|
|
||||||
# class DeviceBayTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
# class DeviceBayTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
# permission_required = 'dcim.change_devicebaytemplate'
|
# permission_required = 'dcim.change_devicebaytemplate'
|
||||||
# queryset = DeviceBayTemplate.objects.all()
|
# queryset = DeviceBayTemplate.objects.all()
|
||||||
@ -1224,7 +1292,7 @@ class ConsolePortListView(PermissionRequiredMixin, ObjectListView):
|
|||||||
filterset = filters.ConsolePortFilterSet
|
filterset = filters.ConsolePortFilterSet
|
||||||
filterset_form = forms.ConsolePortFilterForm
|
filterset_form = forms.ConsolePortFilterForm
|
||||||
table = tables.ConsolePortDetailTable
|
table = tables.ConsolePortDetailTable
|
||||||
template_name = 'dcim/device_component_list.html'
|
template_name = 'dcim/consoleport_list.html'
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortCreateView(PermissionRequiredMixin, ComponentCreateView):
|
class ConsolePortCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
@ -1253,6 +1321,13 @@ class ConsolePortBulkImportView(PermissionRequiredMixin, BulkImportView):
|
|||||||
default_return_url = 'dcim:consoleport_list'
|
default_return_url = 'dcim:consoleport_list'
|
||||||
|
|
||||||
|
|
||||||
|
class ConsolePortBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
|
permission_required = 'dcim.change_consoleport'
|
||||||
|
queryset = ConsolePort.objects.all()
|
||||||
|
table = tables.ConsolePortTable
|
||||||
|
form = forms.ConsolePortBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'dcim.delete_consoleport'
|
permission_required = 'dcim.delete_consoleport'
|
||||||
queryset = ConsolePort.objects.all()
|
queryset = ConsolePort.objects.all()
|
||||||
@ -1270,7 +1345,7 @@ class ConsoleServerPortListView(PermissionRequiredMixin, ObjectListView):
|
|||||||
filterset = filters.ConsoleServerPortFilterSet
|
filterset = filters.ConsoleServerPortFilterSet
|
||||||
filterset_form = forms.ConsoleServerPortFilterForm
|
filterset_form = forms.ConsoleServerPortFilterForm
|
||||||
table = tables.ConsoleServerPortDetailTable
|
table = tables.ConsoleServerPortDetailTable
|
||||||
template_name = 'dcim/device_component_list.html'
|
template_name = 'dcim/consoleserverport_list.html'
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
|
class ConsoleServerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
@ -1335,7 +1410,7 @@ class PowerPortListView(PermissionRequiredMixin, ObjectListView):
|
|||||||
filterset = filters.PowerPortFilterSet
|
filterset = filters.PowerPortFilterSet
|
||||||
filterset_form = forms.PowerPortFilterForm
|
filterset_form = forms.PowerPortFilterForm
|
||||||
table = tables.PowerPortDetailTable
|
table = tables.PowerPortDetailTable
|
||||||
template_name = 'dcim/device_component_list.html'
|
template_name = 'dcim/powerport_list.html'
|
||||||
|
|
||||||
|
|
||||||
class PowerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
|
class PowerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
@ -1364,6 +1439,13 @@ class PowerPortBulkImportView(PermissionRequiredMixin, BulkImportView):
|
|||||||
default_return_url = 'dcim:powerport_list'
|
default_return_url = 'dcim:powerport_list'
|
||||||
|
|
||||||
|
|
||||||
|
class PowerPortBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
|
permission_required = 'dcim.change_powerport'
|
||||||
|
queryset = PowerPort.objects.all()
|
||||||
|
table = tables.PowerPortTable
|
||||||
|
form = forms.PowerPortBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'dcim.delete_powerport'
|
permission_required = 'dcim.delete_powerport'
|
||||||
queryset = PowerPort.objects.all()
|
queryset = PowerPort.objects.all()
|
||||||
@ -1381,7 +1463,7 @@ class PowerOutletListView(PermissionRequiredMixin, ObjectListView):
|
|||||||
filterset = filters.PowerOutletFilterSet
|
filterset = filters.PowerOutletFilterSet
|
||||||
filterset_form = forms.PowerOutletFilterForm
|
filterset_form = forms.PowerOutletFilterForm
|
||||||
table = tables.PowerOutletDetailTable
|
table = tables.PowerOutletDetailTable
|
||||||
template_name = 'dcim/device_component_list.html'
|
template_name = 'dcim/poweroutlet_list.html'
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletCreateView(PermissionRequiredMixin, ComponentCreateView):
|
class PowerOutletCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
@ -1446,7 +1528,7 @@ class InterfaceListView(PermissionRequiredMixin, ObjectListView):
|
|||||||
filterset = filters.InterfaceFilterSet
|
filterset = filters.InterfaceFilterSet
|
||||||
filterset_form = forms.InterfaceFilterForm
|
filterset_form = forms.InterfaceFilterForm
|
||||||
table = tables.InterfaceDetailTable
|
table = tables.InterfaceDetailTable
|
||||||
template_name = 'dcim/device_component_list.html'
|
template_name = 'dcim/interface_list.html'
|
||||||
|
|
||||||
|
|
||||||
class InterfaceView(PermissionRequiredMixin, View):
|
class InterfaceView(PermissionRequiredMixin, View):
|
||||||
@ -1548,7 +1630,7 @@ class FrontPortListView(PermissionRequiredMixin, ObjectListView):
|
|||||||
filterset = filters.FrontPortFilterSet
|
filterset = filters.FrontPortFilterSet
|
||||||
filterset_form = forms.FrontPortFilterForm
|
filterset_form = forms.FrontPortFilterForm
|
||||||
table = tables.FrontPortDetailTable
|
table = tables.FrontPortDetailTable
|
||||||
template_name = 'dcim/device_component_list.html'
|
template_name = 'dcim/frontport_list.html'
|
||||||
|
|
||||||
|
|
||||||
class FrontPortCreateView(PermissionRequiredMixin, ComponentCreateView):
|
class FrontPortCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
@ -1613,7 +1695,7 @@ class RearPortListView(PermissionRequiredMixin, ObjectListView):
|
|||||||
filterset = filters.RearPortFilterSet
|
filterset = filters.RearPortFilterSet
|
||||||
filterset_form = forms.RearPortFilterForm
|
filterset_form = forms.RearPortFilterForm
|
||||||
table = tables.RearPortDetailTable
|
table = tables.RearPortDetailTable
|
||||||
template_name = 'dcim/device_component_list.html'
|
template_name = 'dcim/rearport_list.html'
|
||||||
|
|
||||||
|
|
||||||
class RearPortCreateView(PermissionRequiredMixin, ComponentCreateView):
|
class RearPortCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
@ -1680,7 +1762,7 @@ class DeviceBayListView(PermissionRequiredMixin, ObjectListView):
|
|||||||
filterset = filters.DeviceBayFilterSet
|
filterset = filters.DeviceBayFilterSet
|
||||||
filterset_form = forms.DeviceBayFilterForm
|
filterset_form = forms.DeviceBayFilterForm
|
||||||
table = tables.DeviceBayDetailTable
|
table = tables.DeviceBayDetailTable
|
||||||
template_name = 'dcim/device_component_list.html'
|
template_name = 'dcim/devicebay_list.html'
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayCreateView(PermissionRequiredMixin, ComponentCreateView):
|
class DeviceBayCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
from django import forms
|
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 mptt.forms import TreeNodeMultipleChoiceField
|
||||||
from taggit.forms import TagField
|
from taggit.forms import TagField
|
||||||
|
|
||||||
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,
|
||||||
CommentField, ContentTypeSelect, DateTimePicker, FilterChoiceField, JSONField, SlugField, StaticSelect2,
|
CommentField, ContentTypeSelect, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField,
|
||||||
BOOLEAN_WITH_BLANK_CHOICES,
|
StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES,
|
||||||
)
|
)
|
||||||
from virtualization.models import Cluster, ClusterGroup
|
from virtualization.models import Cluster, ClusterGroup
|
||||||
from .choices import *
|
from .choices import *
|
||||||
@ -190,7 +191,61 @@ class TagBulkEditForm(BootstrapMixin, BulkEditForm):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class ConfigContextForm(BootstrapMixin, forms.ModelForm):
|
class ConfigContextForm(BootstrapMixin, forms.ModelForm):
|
||||||
tags = forms.ModelMultipleChoiceField(
|
regions = TreeNodeMultipleChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
required=False,
|
||||||
|
widget=StaticSelect2Multiple()
|
||||||
|
)
|
||||||
|
sites = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
required=False,
|
||||||
|
widget=APISelectMultiple(
|
||||||
|
api_url="/api/dcim/sites/"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
roles = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=DeviceRole.objects.all(),
|
||||||
|
required=False,
|
||||||
|
widget=APISelectMultiple(
|
||||||
|
api_url="/api/dcim/device-roles/"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
platforms = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Platform.objects.all(),
|
||||||
|
required=False,
|
||||||
|
widget=APISelectMultiple(
|
||||||
|
api_url="/api/dcim/platforms/"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
cluster_groups = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=ClusterGroup.objects.all(),
|
||||||
|
required=False,
|
||||||
|
widget=APISelectMultiple(
|
||||||
|
api_url="/api/virtualization/cluster-groups/"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
clusters = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Cluster.objects.all(),
|
||||||
|
required=False,
|
||||||
|
widget=APISelectMultiple(
|
||||||
|
api_url="/api/virtualization/clusters/"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
tenant_groups = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=TenantGroup.objects.all(),
|
||||||
|
required=False,
|
||||||
|
widget=APISelectMultiple(
|
||||||
|
api_url="/api/tenancy/tenant-groups/"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
tenants = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tenant.objects.all(),
|
||||||
|
required=False,
|
||||||
|
widget=APISelectMultiple(
|
||||||
|
api_url="/api/tenancy/tenants/"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
queryset=Tag.objects.all(),
|
queryset=Tag.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
required=False,
|
required=False,
|
||||||
@ -204,36 +259,10 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConfigContext
|
model = ConfigContext
|
||||||
fields = [
|
fields = (
|
||||||
'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'cluster_groups',
|
'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'cluster_groups',
|
||||||
'clusters', 'tenant_groups', 'tenants', 'tags', 'data',
|
'clusters', 'tenant_groups', 'tenants', 'tags', 'data',
|
||||||
]
|
)
|
||||||
widgets = {
|
|
||||||
'regions': APISelectMultiple(
|
|
||||||
api_url="/api/dcim/regions/"
|
|
||||||
),
|
|
||||||
'sites': APISelectMultiple(
|
|
||||||
api_url="/api/dcim/sites/"
|
|
||||||
),
|
|
||||||
'roles': APISelectMultiple(
|
|
||||||
api_url="/api/dcim/device-roles/"
|
|
||||||
),
|
|
||||||
'platforms': APISelectMultiple(
|
|
||||||
api_url="/api/dcim/platforms/"
|
|
||||||
),
|
|
||||||
'cluster_groups': APISelectMultiple(
|
|
||||||
api_url="/api/virtualization/cluster-groups/"
|
|
||||||
),
|
|
||||||
'clusters': APISelectMultiple(
|
|
||||||
api_url="/api/virtualization/clusters/"
|
|
||||||
),
|
|
||||||
'tenant_groups': APISelectMultiple(
|
|
||||||
api_url="/api/tenancy/tenant-groups/"
|
|
||||||
),
|
|
||||||
'tenants': APISelectMultiple(
|
|
||||||
api_url="/api/tenancy/tenants/"
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm):
|
class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||||
@ -265,72 +294,81 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
|
|||||||
required=False,
|
required=False,
|
||||||
label='Search'
|
label='Search'
|
||||||
)
|
)
|
||||||
region = FilterChoiceField(
|
region = DynamicModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
|
required=False,
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url="/api/dcim/regions/",
|
api_url="/api/dcim/regions/",
|
||||||
value_field="slug",
|
value_field="slug",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
site = FilterChoiceField(
|
site = DynamicModelMultipleChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
|
required=False,
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url="/api/dcim/sites/",
|
api_url="/api/dcim/sites/",
|
||||||
value_field="slug",
|
value_field="slug",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
role = FilterChoiceField(
|
role = DynamicModelMultipleChoiceField(
|
||||||
queryset=DeviceRole.objects.all(),
|
queryset=DeviceRole.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
|
required=False,
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url="/api/dcim/device-roles/",
|
api_url="/api/dcim/device-roles/",
|
||||||
value_field="slug",
|
value_field="slug",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
platform = FilterChoiceField(
|
platform = DynamicModelMultipleChoiceField(
|
||||||
queryset=Platform.objects.all(),
|
queryset=Platform.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
|
required=False,
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url="/api/dcim/platforms/",
|
api_url="/api/dcim/platforms/",
|
||||||
value_field="slug",
|
value_field="slug",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
cluster_group = FilterChoiceField(
|
cluster_group = DynamicModelMultipleChoiceField(
|
||||||
queryset=ClusterGroup.objects.all(),
|
queryset=ClusterGroup.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
|
required=False,
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url="/api/virtualization/cluster-groups/",
|
api_url="/api/virtualization/cluster-groups/",
|
||||||
value_field="slug",
|
value_field="slug",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
cluster_id = FilterChoiceField(
|
cluster_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Cluster.objects.all(),
|
queryset=Cluster.objects.all(),
|
||||||
|
required=False,
|
||||||
label='Cluster',
|
label='Cluster',
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url="/api/virtualization/clusters/",
|
api_url="/api/virtualization/clusters/",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
tenant_group = FilterChoiceField(
|
tenant_group = DynamicModelMultipleChoiceField(
|
||||||
queryset=TenantGroup.objects.all(),
|
queryset=TenantGroup.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
|
required=False,
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url="/api/tenancy/tenant-groups/",
|
api_url="/api/tenancy/tenant-groups/",
|
||||||
value_field="slug",
|
value_field="slug",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
tenant = FilterChoiceField(
|
tenant = DynamicModelMultipleChoiceField(
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
|
required=False,
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url="/api/tenancy/tenants/",
|
api_url="/api/tenancy/tenants/",
|
||||||
value_field="slug",
|
value_field="slug",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
tag = FilterChoiceField(
|
tag = DynamicModelMultipleChoiceField(
|
||||||
queryset=Tag.objects.all(),
|
queryset=Tag.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
|
required=False,
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url="/api/extras/tags/",
|
api_url="/api/extras/tags/",
|
||||||
value_field="slug",
|
value_field="slug",
|
||||||
@ -387,11 +425,14 @@ class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
|
|||||||
)
|
)
|
||||||
action = forms.ChoiceField(
|
action = forms.ChoiceField(
|
||||||
choices=add_blank_choice(ObjectChangeActionChoices),
|
choices=add_blank_choice(ObjectChangeActionChoices),
|
||||||
required=False
|
required=False,
|
||||||
|
widget=StaticSelect2()
|
||||||
)
|
)
|
||||||
|
# TODO: Convert to DynamicModelMultipleChoiceField once we have an API endpoint for users
|
||||||
user = forms.ModelChoiceField(
|
user = forms.ModelChoiceField(
|
||||||
queryset=User.objects.order_by('username'),
|
queryset=User.objects.order_by('username'),
|
||||||
required=False
|
required=False,
|
||||||
|
widget=StaticSelect2()
|
||||||
)
|
)
|
||||||
changed_object_type = forms.ModelChoiceField(
|
changed_object_type = forms.ModelChoiceField(
|
||||||
queryset=ContentType.objects.order_by('model'),
|
queryset=ContentType.objects.order_by('model'),
|
||||||
|
@ -202,7 +202,7 @@ class IPAddressSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
|||||||
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
||||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
status = ChoiceField(choices=IPAddressStatusChoices, required=False)
|
status = ChoiceField(choices=IPAddressStatusChoices, required=False)
|
||||||
role = ChoiceField(choices=IPAddressRoleChoices, required=False, allow_null=True)
|
role = ChoiceField(choices=IPAddressRoleChoices, allow_blank=True, required=False)
|
||||||
interface = IPAddressInterfaceSerializer(required=False, allow_null=True)
|
interface = IPAddressInterfaceSerializer(required=False, allow_null=True)
|
||||||
nat_inside = NestedIPAddressSerializer(required=False, allow_null=True)
|
nat_inside = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||||
nat_outside = NestedIPAddressSerializer(read_only=True)
|
nat_outside = NestedIPAddressSerializer(read_only=True)
|
||||||
@ -240,7 +240,7 @@ class AvailableIPSerializer(serializers.Serializer):
|
|||||||
class ServiceSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
class ServiceSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||||
device = NestedDeviceSerializer(required=False, allow_null=True)
|
device = NestedDeviceSerializer(required=False, allow_null=True)
|
||||||
virtual_machine = NestedVirtualMachineSerializer(required=False, allow_null=True)
|
virtual_machine = NestedVirtualMachineSerializer(required=False, allow_null=True)
|
||||||
protocol = ChoiceField(choices=ServiceProtocolChoices)
|
protocol = ChoiceField(choices=ServiceProtocolChoices, required=False)
|
||||||
ipaddresses = SerializedPKRelatedField(
|
ipaddresses = SerializedPKRelatedField(
|
||||||
queryset=IPAddress.objects.all(),
|
queryset=IPAddress.objects.all(),
|
||||||
serializer=NestedIPAddressSerializer,
|
serializer=NestedIPAddressSerializer,
|
||||||
|
@ -8,7 +8,7 @@ from dcim.models import Device, Interface, Region, Site
|
|||||||
from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
|
from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
|
||||||
from tenancy.filters import TenancyFilterSet
|
from tenancy.filters import TenancyFilterSet
|
||||||
from utilities.filters import (
|
from utilities.filters import (
|
||||||
MultiValueCharFilter, NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter,
|
MultiValueCharFilter, MultiValueNumberFilter, NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter,
|
||||||
)
|
)
|
||||||
from virtualization.models import VirtualMachine
|
from virtualization.models import VirtualMachine
|
||||||
from .choices import *
|
from .choices import *
|
||||||
@ -304,12 +304,12 @@ class IPAddressFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedF
|
|||||||
to_field_name='rd',
|
to_field_name='rd',
|
||||||
label='VRF (RD)',
|
label='VRF (RD)',
|
||||||
)
|
)
|
||||||
device = django_filters.CharFilter(
|
device = MultiValueCharFilter(
|
||||||
method='filter_device',
|
method='filter_device',
|
||||||
field_name='name',
|
field_name='name',
|
||||||
label='Device',
|
label='Device (name)',
|
||||||
)
|
)
|
||||||
device_id = django_filters.NumberFilter(
|
device_id = MultiValueNumberFilter(
|
||||||
method='filter_device',
|
method='filter_device',
|
||||||
field_name='pk',
|
field_name='pk',
|
||||||
label='Device (ID)',
|
label='Device (ID)',
|
||||||
@ -385,8 +385,10 @@ class IPAddressFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedF
|
|||||||
|
|
||||||
def filter_device(self, queryset, name, value):
|
def filter_device(self, queryset, name, value):
|
||||||
try:
|
try:
|
||||||
device = Device.objects.prefetch_related('device_type').get(**{name: value})
|
devices = Device.objects.prefetch_related('device_type').filter(**{'{}__in'.format(name): value})
|
||||||
vc_interface_ids = [i['id'] for i in device.vc_interfaces.values('id')]
|
vc_interface_ids = []
|
||||||
|
for device in devices:
|
||||||
|
vc_interface_ids.extend([i['id'] for i in device.vc_interfaces.values('id')])
|
||||||
return queryset.filter(interface_id__in=vc_interface_ids)
|
return queryset.filter(interface_id__in=vc_interface_ids)
|
||||||
except Device.DoesNotExist:
|
except Device.DoesNotExist:
|
||||||
return queryset.none()
|
return queryset.none()
|
||||||
|
@ -10,9 +10,10 @@ from extras.forms import (
|
|||||||
from tenancy.forms import TenancyFilterForm, TenancyForm
|
from tenancy.forms import TenancyFilterForm, TenancyForm
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField,
|
add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, CSVChoiceField,
|
||||||
CSVChoiceField, DatePicker, ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, ReturnURLForm,
|
DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableIPAddressField,
|
||||||
SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES
|
FlexibleModelChoiceField, ReturnURLForm, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField,
|
||||||
|
BOOLEAN_WITH_BLANK_CHOICES,
|
||||||
)
|
)
|
||||||
from virtualization.models import VirtualMachine
|
from virtualization.models import VirtualMachine
|
||||||
from .constants import *
|
from .constants import *
|
||||||
@ -75,7 +76,7 @@ class VRFBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm
|
|||||||
queryset=VRF.objects.all(),
|
queryset=VRF.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput()
|
widget=forms.MultipleHiddenInput()
|
||||||
)
|
)
|
||||||
tenant = forms.ModelChoiceField(
|
tenant = DynamicModelChoiceField(
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -148,6 +149,12 @@ class RIRFilterForm(BootstrapMixin, forms.Form):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class AggregateForm(BootstrapMixin, CustomFieldModelForm):
|
class AggregateForm(BootstrapMixin, CustomFieldModelForm):
|
||||||
|
rir = DynamicModelChoiceField(
|
||||||
|
queryset=RIR.objects.all(),
|
||||||
|
widget=APISelect(
|
||||||
|
api_url="/api/ipam/rirs/"
|
||||||
|
)
|
||||||
|
)
|
||||||
tags = TagField(
|
tags = TagField(
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
@ -162,9 +169,6 @@ class AggregateForm(BootstrapMixin, CustomFieldModelForm):
|
|||||||
'rir': "Regional Internet Registry responsible for this prefix",
|
'rir': "Regional Internet Registry responsible for this prefix",
|
||||||
}
|
}
|
||||||
widgets = {
|
widgets = {
|
||||||
'rir': APISelect(
|
|
||||||
api_url="/api/ipam/rirs/"
|
|
||||||
),
|
|
||||||
'date_added': DatePicker(),
|
'date_added': DatePicker(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,7 +193,7 @@ class AggregateBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
|
|||||||
queryset=Aggregate.objects.all(),
|
queryset=Aggregate.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput()
|
widget=forms.MultipleHiddenInput()
|
||||||
)
|
)
|
||||||
rir = forms.ModelChoiceField(
|
rir = DynamicModelChoiceField(
|
||||||
queryset=RIR.objects.all(),
|
queryset=RIR.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
label='RIR',
|
label='RIR',
|
||||||
@ -226,9 +230,10 @@ class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
label='Address family',
|
label='Address family',
|
||||||
widget=StaticSelect2()
|
widget=StaticSelect2()
|
||||||
)
|
)
|
||||||
rir = FilterChoiceField(
|
rir = DynamicModelMultipleChoiceField(
|
||||||
queryset=RIR.objects.all(),
|
queryset=RIR.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
|
required=False,
|
||||||
label='RIR',
|
label='RIR',
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url="/api/ipam/rirs/",
|
api_url="/api/ipam/rirs/",
|
||||||
@ -268,10 +273,16 @@ class RoleCSVForm(forms.ModelForm):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||||
site = forms.ModelChoiceField(
|
vrf = DynamicModelChoiceField(
|
||||||
|
queryset=VRF.objects.all(),
|
||||||
|
required=False,
|
||||||
|
widget=APISelect(
|
||||||
|
api_url="/api/ipam/vrfs/",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
site = DynamicModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
label='Site',
|
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url="/api/dcim/sites/",
|
api_url="/api/dcim/sites/",
|
||||||
filter_for={
|
filter_for={
|
||||||
@ -283,11 +294,8 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
vlan_group = ChainedModelChoiceField(
|
vlan_group = DynamicModelChoiceField(
|
||||||
queryset=VLANGroup.objects.all(),
|
queryset=VLANGroup.objects.all(),
|
||||||
chains=(
|
|
||||||
('site', 'site'),
|
|
||||||
),
|
|
||||||
required=False,
|
required=False,
|
||||||
label='VLAN group',
|
label='VLAN group',
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -300,12 +308,8 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
vlan = ChainedModelChoiceField(
|
vlan = DynamicModelChoiceField(
|
||||||
queryset=VLAN.objects.all(),
|
queryset=VLAN.objects.all(),
|
||||||
chains=(
|
|
||||||
('site', 'site'),
|
|
||||||
('group', 'vlan_group'),
|
|
||||||
),
|
|
||||||
required=False,
|
required=False,
|
||||||
label='VLAN',
|
label='VLAN',
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -313,6 +317,13 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
display_field='display_name'
|
display_field='display_name'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
role = DynamicModelChoiceField(
|
||||||
|
queryset=Role.objects.all(),
|
||||||
|
required=False,
|
||||||
|
widget=APISelect(
|
||||||
|
api_url="/api/ipam/roles/"
|
||||||
|
)
|
||||||
|
)
|
||||||
tags = TagField(required=False)
|
tags = TagField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -322,13 +333,7 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
'tags',
|
'tags',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'vrf': APISelect(
|
|
||||||
api_url="/api/ipam/vrfs/"
|
|
||||||
),
|
|
||||||
'status': StaticSelect2(),
|
'status': StaticSelect2(),
|
||||||
'role': APISelect(
|
|
||||||
api_url="/api/ipam/roles/"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -439,14 +444,14 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
|
|||||||
queryset=Prefix.objects.all(),
|
queryset=Prefix.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput()
|
widget=forms.MultipleHiddenInput()
|
||||||
)
|
)
|
||||||
site = forms.ModelChoiceField(
|
site = DynamicModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url="/api/dcim/sites/"
|
api_url="/api/dcim/sites/"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
vrf = forms.ModelChoiceField(
|
vrf = DynamicModelChoiceField(
|
||||||
queryset=VRF.objects.all(),
|
queryset=VRF.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
label='VRF',
|
label='VRF',
|
||||||
@ -459,7 +464,7 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
|
|||||||
max_value=PREFIX_LENGTH_MAX,
|
max_value=PREFIX_LENGTH_MAX,
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
tenant = forms.ModelChoiceField(
|
tenant = DynamicModelChoiceField(
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -471,7 +476,7 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
|
|||||||
required=False,
|
required=False,
|
||||||
widget=StaticSelect2()
|
widget=StaticSelect2()
|
||||||
)
|
)
|
||||||
role = forms.ModelChoiceField(
|
role = DynamicModelChoiceField(
|
||||||
queryset=Role.objects.all(),
|
queryset=Role.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -525,10 +530,10 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
|
|||||||
label='Mask length',
|
label='Mask length',
|
||||||
widget=StaticSelect2()
|
widget=StaticSelect2()
|
||||||
)
|
)
|
||||||
vrf_id = FilterChoiceField(
|
vrf_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=VRF.objects.all(),
|
queryset=VRF.objects.all(),
|
||||||
|
required=False,
|
||||||
label='VRF',
|
label='VRF',
|
||||||
null_label='-- Global --',
|
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url="/api/ipam/vrfs/",
|
api_url="/api/ipam/vrfs/",
|
||||||
null_option=True,
|
null_option=True,
|
||||||
@ -539,7 +544,7 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
|
|||||||
required=False,
|
required=False,
|
||||||
widget=StaticSelect2Multiple()
|
widget=StaticSelect2Multiple()
|
||||||
)
|
)
|
||||||
region = FilterChoiceField(
|
region = DynamicModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
required=False,
|
required=False,
|
||||||
@ -551,20 +556,20 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
site = FilterChoiceField(
|
site = DynamicModelMultipleChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
null_label='-- None --',
|
required=False,
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url="/api/dcim/sites/",
|
api_url="/api/dcim/sites/",
|
||||||
value_field="slug",
|
value_field="slug",
|
||||||
null_option=True,
|
null_option=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
role = FilterChoiceField(
|
role = DynamicModelMultipleChoiceField(
|
||||||
queryset=Role.objects.all(),
|
queryset=Role.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
null_label='-- None --',
|
required=False,
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url="/api/ipam/roles/",
|
api_url="/api/ipam/roles/",
|
||||||
value_field="slug",
|
value_field="slug",
|
||||||
@ -594,7 +599,15 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
|
|||||||
queryset=Interface.objects.all(),
|
queryset=Interface.objects.all(),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
nat_site = forms.ModelChoiceField(
|
vrf = DynamicModelChoiceField(
|
||||||
|
queryset=VRF.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label='VRF',
|
||||||
|
widget=APISelect(
|
||||||
|
api_url="/api/ipam/vrfs/"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
nat_site = DynamicModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
label='Site',
|
label='Site',
|
||||||
@ -606,11 +619,8 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
nat_rack = ChainedModelChoiceField(
|
nat_rack = DynamicModelChoiceField(
|
||||||
queryset=Rack.objects.all(),
|
queryset=Rack.objects.all(),
|
||||||
chains=(
|
|
||||||
('site', 'nat_site'),
|
|
||||||
),
|
|
||||||
required=False,
|
required=False,
|
||||||
label='Rack',
|
label='Rack',
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -624,12 +634,8 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
nat_device = ChainedModelChoiceField(
|
nat_device = DynamicModelChoiceField(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
chains=(
|
|
||||||
('site', 'nat_site'),
|
|
||||||
('rack', 'nat_rack'),
|
|
||||||
),
|
|
||||||
required=False,
|
required=False,
|
||||||
label='Device',
|
label='Device',
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -651,11 +657,8 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
nat_inside = ChainedModelChoiceField(
|
nat_inside = DynamicModelChoiceField(
|
||||||
queryset=IPAddress.objects.all(),
|
queryset=IPAddress.objects.all(),
|
||||||
chains=(
|
|
||||||
('interface__device', 'nat_device'),
|
|
||||||
),
|
|
||||||
required=False,
|
required=False,
|
||||||
label='IP Address',
|
label='IP Address',
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -680,9 +683,6 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
|
|||||||
widgets = {
|
widgets = {
|
||||||
'status': StaticSelect2(),
|
'status': StaticSelect2(),
|
||||||
'role': StaticSelect2(),
|
'role': StaticSelect2(),
|
||||||
'vrf': APISelect(
|
|
||||||
api_url="/api/ipam/vrfs/"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -757,6 +757,14 @@ class IPAddressBulkCreateForm(BootstrapMixin, forms.Form):
|
|||||||
|
|
||||||
|
|
||||||
class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||||
|
vrf = DynamicModelChoiceField(
|
||||||
|
queryset=VRF.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label='VRF',
|
||||||
|
widget=APISelect(
|
||||||
|
api_url="/api/ipam/vrfs/"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
@ -766,9 +774,6 @@ class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
widgets = {
|
widgets = {
|
||||||
'status': StaticSelect2(),
|
'status': StaticSelect2(),
|
||||||
'role': StaticSelect2(),
|
'role': StaticSelect2(),
|
||||||
'vrf': APISelect(
|
|
||||||
api_url="/api/ipam/vrfs/"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -904,7 +909,7 @@ class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
|
|||||||
queryset=IPAddress.objects.all(),
|
queryset=IPAddress.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput()
|
widget=forms.MultipleHiddenInput()
|
||||||
)
|
)
|
||||||
vrf = forms.ModelChoiceField(
|
vrf = DynamicModelChoiceField(
|
||||||
queryset=VRF.objects.all(),
|
queryset=VRF.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
label='VRF',
|
label='VRF',
|
||||||
@ -917,7 +922,7 @@ class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
|
|||||||
max_value=IPADDRESS_MASK_LENGTH_MAX,
|
max_value=IPADDRESS_MASK_LENGTH_MAX,
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
tenant = forms.ModelChoiceField(
|
tenant = DynamicModelChoiceField(
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -950,7 +955,7 @@ class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
|
|||||||
|
|
||||||
|
|
||||||
class IPAddressAssignForm(BootstrapMixin, forms.Form):
|
class IPAddressAssignForm(BootstrapMixin, forms.Form):
|
||||||
vrf_id = forms.ModelChoiceField(
|
vrf_id = DynamicModelChoiceField(
|
||||||
queryset=VRF.objects.all(),
|
queryset=VRF.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
label='VRF',
|
label='VRF',
|
||||||
@ -996,10 +1001,10 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo
|
|||||||
label='Mask length',
|
label='Mask length',
|
||||||
widget=StaticSelect2()
|
widget=StaticSelect2()
|
||||||
)
|
)
|
||||||
vrf_id = FilterChoiceField(
|
vrf_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=VRF.objects.all(),
|
queryset=VRF.objects.all(),
|
||||||
|
required=False,
|
||||||
label='VRF',
|
label='VRF',
|
||||||
null_label='-- Global --',
|
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url="/api/ipam/vrfs/",
|
api_url="/api/ipam/vrfs/",
|
||||||
null_option=True,
|
null_option=True,
|
||||||
@ -1030,6 +1035,13 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo
|
|||||||
#
|
#
|
||||||
|
|
||||||
class VLANGroupForm(BootstrapMixin, forms.ModelForm):
|
class VLANGroupForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
site = DynamicModelChoiceField(
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
required=False,
|
||||||
|
widget=APISelect(
|
||||||
|
api_url="/api/dcim/sites/"
|
||||||
|
)
|
||||||
|
)
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -1037,11 +1049,6 @@ class VLANGroupForm(BootstrapMixin, forms.ModelForm):
|
|||||||
fields = [
|
fields = [
|
||||||
'site', 'name', 'slug',
|
'site', 'name', 'slug',
|
||||||
]
|
]
|
||||||
widgets = {
|
|
||||||
'site': APISelect(
|
|
||||||
api_url="/api/dcim/sites/"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class VLANGroupCSVForm(forms.ModelForm):
|
class VLANGroupCSVForm(forms.ModelForm):
|
||||||
@ -1065,7 +1072,7 @@ class VLANGroupCSVForm(forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class VLANGroupFilterForm(BootstrapMixin, forms.Form):
|
class VLANGroupFilterForm(BootstrapMixin, forms.Form):
|
||||||
region = FilterChoiceField(
|
region = DynamicModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
required=False,
|
required=False,
|
||||||
@ -1077,10 +1084,10 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
site = FilterChoiceField(
|
site = DynamicModelMultipleChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
null_label='-- Global --',
|
required=False,
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url="/api/dcim/sites/",
|
api_url="/api/dcim/sites/",
|
||||||
value_field="slug",
|
value_field="slug",
|
||||||
@ -1094,7 +1101,7 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||||
site = forms.ModelChoiceField(
|
site = DynamicModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -1107,17 +1114,20 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
group = ChainedModelChoiceField(
|
group = DynamicModelChoiceField(
|
||||||
queryset=VLANGroup.objects.all(),
|
queryset=VLANGroup.objects.all(),
|
||||||
chains=(
|
|
||||||
('site', 'site'),
|
|
||||||
),
|
|
||||||
required=False,
|
required=False,
|
||||||
label='Group',
|
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url='/api/ipam/vlan-groups/',
|
api_url='/api/ipam/vlan-groups/',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
role = DynamicModelChoiceField(
|
||||||
|
queryset=Role.objects.all(),
|
||||||
|
required=False,
|
||||||
|
widget=APISelect(
|
||||||
|
api_url="/api/ipam/roles/"
|
||||||
|
)
|
||||||
|
)
|
||||||
tags = TagField(required=False)
|
tags = TagField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -1135,9 +1145,6 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
}
|
}
|
||||||
widgets = {
|
widgets = {
|
||||||
'status': StaticSelect2(),
|
'status': StaticSelect2(),
|
||||||
'role': APISelect(
|
|
||||||
api_url="/api/ipam/roles/"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -1212,21 +1219,21 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
|
|||||||
queryset=VLAN.objects.all(),
|
queryset=VLAN.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput()
|
widget=forms.MultipleHiddenInput()
|
||||||
)
|
)
|
||||||
site = forms.ModelChoiceField(
|
site = DynamicModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url="/api/dcim/sites/"
|
api_url="/api/dcim/sites/"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
group = forms.ModelChoiceField(
|
group = DynamicModelChoiceField(
|
||||||
queryset=VLANGroup.objects.all(),
|
queryset=VLANGroup.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url="/api/ipam/vlan-groups/"
|
api_url="/api/ipam/vlan-groups/"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
tenant = forms.ModelChoiceField(
|
tenant = DynamicModelChoiceField(
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -1238,7 +1245,7 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
|
|||||||
required=False,
|
required=False,
|
||||||
widget=StaticSelect2()
|
widget=StaticSelect2()
|
||||||
)
|
)
|
||||||
role = forms.ModelChoiceField(
|
role = DynamicModelChoiceField(
|
||||||
queryset=Role.objects.all(),
|
queryset=Role.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -1263,7 +1270,7 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
|||||||
required=False,
|
required=False,
|
||||||
label='Search'
|
label='Search'
|
||||||
)
|
)
|
||||||
region = FilterChoiceField(
|
region = DynamicModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
required=False,
|
required=False,
|
||||||
@ -1276,20 +1283,20 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
site = FilterChoiceField(
|
site = DynamicModelMultipleChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
null_label='-- Global --',
|
required=False,
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url="/api/dcim/sites/",
|
api_url="/api/dcim/sites/",
|
||||||
value_field="slug",
|
value_field="slug",
|
||||||
null_option=True,
|
null_option=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
group_id = FilterChoiceField(
|
group_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=VLANGroup.objects.all(),
|
queryset=VLANGroup.objects.all(),
|
||||||
|
required=False,
|
||||||
label='VLAN group',
|
label='VLAN group',
|
||||||
null_label='-- None --',
|
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url="/api/ipam/vlan-groups/",
|
api_url="/api/ipam/vlan-groups/",
|
||||||
null_option=True,
|
null_option=True,
|
||||||
@ -1300,10 +1307,10 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
|||||||
required=False,
|
required=False,
|
||||||
widget=StaticSelect2Multiple()
|
widget=StaticSelect2Multiple()
|
||||||
)
|
)
|
||||||
role = FilterChoiceField(
|
role = DynamicModelMultipleChoiceField(
|
||||||
queryset=Role.objects.all(),
|
queryset=Role.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
null_label='-- None --',
|
required=False,
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url="/api/ipam/roles/",
|
api_url="/api/ipam/roles/",
|
||||||
value_field="slug",
|
value_field="slug",
|
||||||
|
@ -392,13 +392,12 @@ class IPAddressTestCase(TestCase):
|
|||||||
params = {'vrf': [vrfs[0].rd, vrfs[1].rd]}
|
params = {'vrf': [vrfs[0].rd, vrfs[1].rd]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
# TODO: Test for multiple values
|
|
||||||
def test_device(self):
|
def test_device(self):
|
||||||
device = Device.objects.first()
|
devices = Device.objects.all()[:2]
|
||||||
params = {'device_id': device.pk}
|
params = {'device_id': [devices[0].pk, devices[1].pk]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
params = {'device': device.name}
|
params = {'device': [devices[0].name, devices[1].name]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_virtual_machine(self):
|
def test_virtual_machine(self):
|
||||||
vms = VirtualMachine.objects.all()[:2]
|
vms = VirtualMachine.objects.all()[:2]
|
||||||
|
@ -220,19 +220,19 @@ $(document).ready(function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if( record.group !== undefined && record.group !== null && record.site !== undefined && record.site !== null ) {
|
if( record.group !== undefined && record.group !== null && record.site !== undefined && record.site !== null ) {
|
||||||
results[record.site.name + ":" + record.group.name] = results[record.site.name + ":" + record.group.name] || { text: record.site.name + " / " + record.group.name, children: [] }
|
results[record.site.name + ":" + record.group.name] = results[record.site.name + ":" + record.group.name] || { text: record.site.name + " / " + record.group.name, children: [] };
|
||||||
results[record.site.name + ":" + record.group.name].children.push(record);
|
results[record.site.name + ":" + record.group.name].children.push(record);
|
||||||
}
|
}
|
||||||
else if( record.group !== undefined && record.group !== null ) {
|
else if( record.group !== undefined && record.group !== null ) {
|
||||||
results[record.group.name] = results[record.group.name] || { text: record.group.name, children: [] }
|
results[record.group.name] = results[record.group.name] || { text: record.group.name, children: [] };
|
||||||
results[record.group.name].children.push(record);
|
results[record.group.name].children.push(record);
|
||||||
}
|
}
|
||||||
else if( record.site !== undefined && record.site !== null ) {
|
else if( record.site !== undefined && record.site !== null ) {
|
||||||
results[record.site.name] = results[record.site.name] || { text: record.site.name, children: [] }
|
results[record.site.name] = results[record.site.name] || { text: record.site.name, children: [] };
|
||||||
results[record.site.name].children.push(record);
|
results[record.site.name].children.push(record);
|
||||||
}
|
}
|
||||||
else if ( (record.group !== undefined || record.group == null) && (record.site !== undefined || record.site === null) ) {
|
else if ( (record.group !== undefined || record.group == null) && (record.site !== undefined || record.site === null) ) {
|
||||||
results['global'] = results['global'] || { text: 'Global', children: [] }
|
results['global'] = results['global'] || { text: 'Global', children: [] };
|
||||||
results['global'].children.push(record);
|
results['global'].children.push(record);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -246,10 +246,9 @@ $(document).ready(function() {
|
|||||||
|
|
||||||
// Handle the null option, but only add it once
|
// Handle the null option, but only add it once
|
||||||
if (element.getAttribute('data-null-option') && data.previous === null) {
|
if (element.getAttribute('data-null-option') && data.previous === null) {
|
||||||
var null_option = $(element).children()[0];
|
|
||||||
results.unshift({
|
results.unshift({
|
||||||
id: null_option.value,
|
id: 'null',
|
||||||
text: null_option.text
|
text: 'None'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,8 +8,8 @@ from extras.forms import (
|
|||||||
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm,
|
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm,
|
||||||
)
|
)
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
APISelect, APISelectMultiple, BootstrapMixin, FilterChoiceField, FlexibleModelChoiceField, SlugField,
|
APISelect, APISelectMultiple, BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
|
||||||
StaticSelect2Multiple, TagFilterField
|
FlexibleModelChoiceField, SlugField, StaticSelect2Multiple, TagFilterField,
|
||||||
)
|
)
|
||||||
from .constants import *
|
from .constants import *
|
||||||
from .models import Secret, SecretRole, UserKey
|
from .models import Secret, SecretRole, UserKey
|
||||||
@ -87,6 +87,12 @@ class SecretForm(BootstrapMixin, CustomFieldModelForm):
|
|||||||
label='Plaintext (verify)',
|
label='Plaintext (verify)',
|
||||||
widget=forms.PasswordInput()
|
widget=forms.PasswordInput()
|
||||||
)
|
)
|
||||||
|
role = DynamicModelChoiceField(
|
||||||
|
queryset=SecretRole.objects.all(),
|
||||||
|
widget=APISelect(
|
||||||
|
api_url="/api/secrets/secret-roles/"
|
||||||
|
)
|
||||||
|
)
|
||||||
tags = TagField(
|
tags = TagField(
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
@ -96,11 +102,6 @@ class SecretForm(BootstrapMixin, CustomFieldModelForm):
|
|||||||
fields = [
|
fields = [
|
||||||
'role', 'name', 'plaintext', 'plaintext2', 'tags',
|
'role', 'name', 'plaintext', 'plaintext2', 'tags',
|
||||||
]
|
]
|
||||||
widgets = {
|
|
||||||
'role': APISelect(
|
|
||||||
api_url="/api/secrets/secret-roles/"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@ -157,7 +158,7 @@ class SecretBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
|
|||||||
queryset=Secret.objects.all(),
|
queryset=Secret.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput()
|
widget=forms.MultipleHiddenInput()
|
||||||
)
|
)
|
||||||
role = forms.ModelChoiceField(
|
role = DynamicModelChoiceField(
|
||||||
queryset=SecretRole.objects.all(),
|
queryset=SecretRole.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -181,9 +182,10 @@ class SecretFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
required=False,
|
required=False,
|
||||||
label='Search'
|
label='Search'
|
||||||
)
|
)
|
||||||
role = FilterChoiceField(
|
role = DynamicModelMultipleChoiceField(
|
||||||
queryset=SecretRole.objects.all(),
|
queryset=SecretRole.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
|
required=True,
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url="/api/secrets/secret-roles/",
|
api_url="/api/secrets/secret-roles/",
|
||||||
value_field="slug",
|
value_field="slug",
|
||||||
|
@ -1,16 +1,14 @@
|
|||||||
{% extends '_base.html' %}
|
{% extends '_base.html' %}
|
||||||
{% load buttons %}
|
{% load buttons %}
|
||||||
{% load helpers %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="pull-right noprint">
|
<div class="pull-right noprint">
|
||||||
{% export_button content_type %}
|
{% export_button content_type %}
|
||||||
</div>
|
</div>
|
||||||
<h1>{% block title %}{{ table.Meta.model|model_name|capfirst }}s{% endblock %}</h1>
|
<h1>{% block title %}Console Ports{% endblock %}</h1>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
{% include 'responsive_table.html' %}
|
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:consoleport_bulk_edit' bulk_delete_url='dcim:consoleport_bulk_delete' %}
|
||||||
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 noprint">
|
<div class="col-md-3 noprint">
|
||||||
{% include 'inc/search_panel.html' %}
|
{% include 'inc/search_panel.html' %}
|
17
netbox/templates/dcim/consoleserverport_list.html
Normal file
17
netbox/templates/dcim/consoleserverport_list.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{% extends '_base.html' %}
|
||||||
|
{% load buttons %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="pull-right noprint">
|
||||||
|
{% export_button content_type %}
|
||||||
|
</div>
|
||||||
|
<h1>{% block title %}Console Server Ports{% endblock %}</h1>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-9">
|
||||||
|
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:consoleserverport_bulk_edit' bulk_delete_url='dcim:consoleserverport_bulk_delete' %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 noprint">
|
||||||
|
{% include 'inc/search_panel.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
17
netbox/templates/dcim/devicebay_list.html
Normal file
17
netbox/templates/dcim/devicebay_list.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{% extends '_base.html' %}
|
||||||
|
{% load buttons %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="pull-right noprint">
|
||||||
|
{% export_button content_type %}
|
||||||
|
</div>
|
||||||
|
<h1>{% block title %}Device Bays{% endblock %}</h1>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-9">
|
||||||
|
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:devicebay_bulk_delete' %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 noprint">
|
||||||
|
{% include 'inc/search_panel.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
17
netbox/templates/dcim/frontport_list.html
Normal file
17
netbox/templates/dcim/frontport_list.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{% extends '_base.html' %}
|
||||||
|
{% load buttons %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="pull-right noprint">
|
||||||
|
{% export_button content_type %}
|
||||||
|
</div>
|
||||||
|
<h1>{% block title %}Front Ports{% endblock %}</h1>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-9">
|
||||||
|
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:frontport_bulk_edit' bulk_delete_url='dcim:frontport_bulk_delete' %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 noprint">
|
||||||
|
{% include 'inc/search_panel.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
17
netbox/templates/dcim/interface_list.html
Normal file
17
netbox/templates/dcim/interface_list.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{% extends '_base.html' %}
|
||||||
|
{% load buttons %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="pull-right noprint">
|
||||||
|
{% export_button content_type %}
|
||||||
|
</div>
|
||||||
|
<h1>{% block title %}Interfaces{% endblock %}</h1>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-9">
|
||||||
|
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:interface_bulk_edit' bulk_delete_url='dcim:interface_bulk_delete' %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 noprint">
|
||||||
|
{% include 'inc/search_panel.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
17
netbox/templates/dcim/poweroutlet_list.html
Normal file
17
netbox/templates/dcim/poweroutlet_list.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{% extends '_base.html' %}
|
||||||
|
{% load buttons %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="pull-right noprint">
|
||||||
|
{% export_button content_type %}
|
||||||
|
</div>
|
||||||
|
<h1>{% block title %}Power Outlets{% endblock %}</h1>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-9">
|
||||||
|
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:poweroutlet_bulk_edit' bulk_delete_url='dcim:poweroutlet_bulk_delete' %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 noprint">
|
||||||
|
{% include 'inc/search_panel.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
17
netbox/templates/dcim/powerport_list.html
Normal file
17
netbox/templates/dcim/powerport_list.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{% extends '_base.html' %}
|
||||||
|
{% load buttons %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="pull-right noprint">
|
||||||
|
{% export_button content_type %}
|
||||||
|
</div>
|
||||||
|
<h1>{% block title %}Power Ports{% endblock %}</h1>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-9">
|
||||||
|
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:powerport_bulk_edit' bulk_delete_url='dcim:powerport_bulk_delete' %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 noprint">
|
||||||
|
{% include 'inc/search_panel.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
17
netbox/templates/dcim/rearport_list.html
Normal file
17
netbox/templates/dcim/rearport_list.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{% extends '_base.html' %}
|
||||||
|
{% load buttons %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="pull-right noprint">
|
||||||
|
{% export_button content_type %}
|
||||||
|
</div>
|
||||||
|
<h1>{% block title %}Rear Ports{% endblock %}</h1>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-9">
|
||||||
|
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:rearport_bulk_edit' bulk_delete_url='dcim:rearport_bulk_delete' %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 noprint">
|
||||||
|
{% include 'inc/search_panel.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -239,7 +239,7 @@
|
|||||||
<a href="{% url 'dcim:poweroutlet_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
|
<a href="{% url 'dcim:poweroutlet_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% url 'dcim:poweroutlet_list' %}">Power Outlet</a>
|
<a href="{% url 'dcim:poweroutlet_list' %}">Power Outlets</a>
|
||||||
</li>
|
</li>
|
||||||
<li{% if not perms.dcim.view_devicebay %} class="disabled"{% endif %}>
|
<li{% if not perms.dcim.view_devicebay %} class="disabled"{% endif %}>
|
||||||
{% if perms.dcim.add_devicebay %}
|
{% if perms.dcim.add_devicebay %}
|
||||||
@ -479,6 +479,11 @@
|
|||||||
<li class="dropdown-header">Miscellaneous</li>
|
<li class="dropdown-header">Miscellaneous</li>
|
||||||
<li{% if not perms.extras.view_configcontext %} class="disabled"{% endif %}>
|
<li{% if not perms.extras.view_configcontext %} class="disabled"{% endif %}>
|
||||||
<a href="{% url 'extras:configcontext_list' %}">Config Contexts</a>
|
<a href="{% url 'extras:configcontext_list' %}">Config Contexts</a>
|
||||||
|
{% if perms.extras.add_configcontext %}
|
||||||
|
<div class="buttons pull-right">
|
||||||
|
<a href="{% url 'extras:configcontext_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
<li{% if not perms.extras.view_script %} class="disabled"{% endif %}>
|
<li{% if not perms.extras.view_script %} class="disabled"{% endif %}>
|
||||||
<a href="{% url 'extras:script_list' %}">Scripts</a>
|
<a href="{% url 'extras:script_list' %}">Scripts</a>
|
||||||
|
@ -2,11 +2,11 @@ from django import forms
|
|||||||
from taggit.forms import TagField
|
from taggit.forms import TagField
|
||||||
|
|
||||||
from extras.forms import (
|
from extras.forms import (
|
||||||
AddRemoveTagsForm, CustomFieldModelForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldFilterForm,
|
AddRemoveTagsForm, CustomFieldModelForm, CustomFieldBulkEditForm, CustomFieldFilterForm,
|
||||||
)
|
)
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
APISelect, APISelectMultiple, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField,
|
APISelect, APISelectMultiple, BootstrapMixin, CommentField, DynamicModelChoiceField,
|
||||||
FilterChoiceField, SlugField, TagFilterField
|
DynamicModelMultipleChoiceField, SlugField, TagFilterField,
|
||||||
)
|
)
|
||||||
from .models import Tenant, TenantGroup
|
from .models import Tenant, TenantGroup
|
||||||
|
|
||||||
@ -42,6 +42,13 @@ class TenantGroupCSVForm(forms.ModelForm):
|
|||||||
|
|
||||||
class TenantForm(BootstrapMixin, CustomFieldModelForm):
|
class TenantForm(BootstrapMixin, CustomFieldModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
group = DynamicModelChoiceField(
|
||||||
|
queryset=TenantGroup.objects.all(),
|
||||||
|
required=False,
|
||||||
|
widget=APISelect(
|
||||||
|
api_url="/api/tenancy/tenant-groups/"
|
||||||
|
)
|
||||||
|
)
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
tags = TagField(
|
tags = TagField(
|
||||||
required=False
|
required=False
|
||||||
@ -49,14 +56,9 @@ class TenantForm(BootstrapMixin, CustomFieldModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Tenant
|
model = Tenant
|
||||||
fields = [
|
fields = (
|
||||||
'name', 'slug', 'group', 'description', 'comments', 'tags',
|
'name', 'slug', 'group', 'description', 'comments', 'tags',
|
||||||
]
|
)
|
||||||
widgets = {
|
|
||||||
'group': APISelect(
|
|
||||||
api_url="/api/tenancy/tenant-groups/"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TenantCSVForm(CustomFieldModelForm):
|
class TenantCSVForm(CustomFieldModelForm):
|
||||||
@ -85,7 +87,7 @@ class TenantBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
|
|||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput()
|
widget=forms.MultipleHiddenInput()
|
||||||
)
|
)
|
||||||
group = forms.ModelChoiceField(
|
group = DynamicModelChoiceField(
|
||||||
queryset=TenantGroup.objects.all(),
|
queryset=TenantGroup.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -105,10 +107,10 @@ class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
required=False,
|
required=False,
|
||||||
label='Search'
|
label='Search'
|
||||||
)
|
)
|
||||||
group = FilterChoiceField(
|
group = DynamicModelMultipleChoiceField(
|
||||||
queryset=TenantGroup.objects.all(),
|
queryset=TenantGroup.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
null_label='-- None --',
|
required=False,
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url="/api/tenancy/tenant-groups/",
|
api_url="/api/tenancy/tenant-groups/",
|
||||||
value_field="slug",
|
value_field="slug",
|
||||||
@ -122,8 +124,8 @@ class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
# Form extensions
|
# Form extensions
|
||||||
#
|
#
|
||||||
|
|
||||||
class TenancyForm(ChainedFieldsMixin, forms.Form):
|
class TenancyForm(forms.Form):
|
||||||
tenant_group = forms.ModelChoiceField(
|
tenant_group = DynamicModelChoiceField(
|
||||||
queryset=TenantGroup.objects.all(),
|
queryset=TenantGroup.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -136,11 +138,8 @@ class TenancyForm(ChainedFieldsMixin, forms.Form):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
tenant = ChainedModelChoiceField(
|
tenant = DynamicModelChoiceField(
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
chains=(
|
|
||||||
('group', 'tenant_group'),
|
|
||||||
),
|
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url='/api/tenancy/tenants/'
|
api_url='/api/tenancy/tenants/'
|
||||||
@ -160,10 +159,10 @@ class TenancyForm(ChainedFieldsMixin, forms.Form):
|
|||||||
|
|
||||||
|
|
||||||
class TenancyFilterForm(forms.Form):
|
class TenancyFilterForm(forms.Form):
|
||||||
tenant_group = FilterChoiceField(
|
tenant_group = DynamicModelMultipleChoiceField(
|
||||||
queryset=TenantGroup.objects.all(),
|
queryset=TenantGroup.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
null_label='-- None --',
|
required=False,
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url="/api/tenancy/tenant-groups/",
|
api_url="/api/tenancy/tenant-groups/",
|
||||||
value_field="slug",
|
value_field="slug",
|
||||||
@ -173,10 +172,10 @@ class TenancyFilterForm(forms.Form):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
tenant = FilterChoiceField(
|
tenant = DynamicModelMultipleChoiceField(
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
null_label='-- None --',
|
required=False,
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url="/api/tenancy/tenants/",
|
api_url="/api/tenancy/tenants/",
|
||||||
value_field="slug",
|
value_field="slug",
|
||||||
|
@ -61,10 +61,14 @@ class IsAuthenticatedOrLoginNotRequired(BasePermission):
|
|||||||
|
|
||||||
class ChoiceField(Field):
|
class ChoiceField(Field):
|
||||||
"""
|
"""
|
||||||
Represent a ChoiceField as {'value': <DB value>, 'label': <string>}.
|
Represent a ChoiceField as {'value': <DB value>, 'label': <string>}. Accepts a single value on write.
|
||||||
|
|
||||||
|
:param choices: An iterable of choices in the form (value, key).
|
||||||
|
:param allow_blank: Allow blank values in addition to the listed choices.
|
||||||
"""
|
"""
|
||||||
def __init__(self, choices, **kwargs):
|
def __init__(self, choices, allow_blank=False, **kwargs):
|
||||||
self.choiceset = choices
|
self.choiceset = choices
|
||||||
|
self.allow_blank = allow_blank
|
||||||
self._choices = dict()
|
self._choices = dict()
|
||||||
|
|
||||||
# Unpack grouped choices
|
# Unpack grouped choices
|
||||||
@ -77,6 +81,15 @@ class ChoiceField(Field):
|
|||||||
|
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
def validate_empty_values(self, data):
|
||||||
|
# Convert null to an empty string unless allow_null == True
|
||||||
|
if data is None:
|
||||||
|
if self.allow_null:
|
||||||
|
return True, None
|
||||||
|
else:
|
||||||
|
data = ''
|
||||||
|
return super().validate_empty_values(data)
|
||||||
|
|
||||||
def to_representation(self, obj):
|
def to_representation(self, obj):
|
||||||
if obj is '':
|
if obj is '':
|
||||||
return None
|
return None
|
||||||
@ -93,6 +106,10 @@ class ChoiceField(Field):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
|
if data is '':
|
||||||
|
if self.allow_blank:
|
||||||
|
return data
|
||||||
|
raise ValidationError("This field may not be blank.")
|
||||||
|
|
||||||
# Provide an explicit error message if the request is trying to write a dict or list
|
# Provide an explicit error message if the request is trying to write a dict or list
|
||||||
if isinstance(data, (dict, list)):
|
if isinstance(data, (dict, list)):
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from django.core.validators import RegexValidator
|
from django.core.validators import RegexValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
from utilities.ordering import naturalize
|
||||||
from .forms import ColorSelect
|
from .forms import ColorSelect
|
||||||
|
|
||||||
ColorValidator = RegexValidator(
|
ColorValidator = RegexValidator(
|
||||||
@ -35,3 +36,35 @@ class ColorField(models.CharField):
|
|||||||
def formfield(self, **kwargs):
|
def formfield(self, **kwargs):
|
||||||
kwargs['widget'] = ColorSelect
|
kwargs['widget'] = ColorSelect
|
||||||
return super().formfield(**kwargs)
|
return super().formfield(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class NaturalOrderingField(models.CharField):
|
||||||
|
"""
|
||||||
|
A field which stores a naturalized representation of its target field, to be used for ordering its parent model.
|
||||||
|
|
||||||
|
:param target_field: Name of the field of the parent model to be naturalized
|
||||||
|
:param naturalize_function: The function used to generate a naturalized value (optional)
|
||||||
|
"""
|
||||||
|
description = "Stores a representation of its target field suitable for natural ordering"
|
||||||
|
|
||||||
|
def __init__(self, target_field, naturalize_function=naturalize, *args, **kwargs):
|
||||||
|
self.target_field = target_field
|
||||||
|
self.naturalize_function = naturalize_function
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def pre_save(self, model_instance, add):
|
||||||
|
"""
|
||||||
|
Generate a naturalized value from the target field
|
||||||
|
"""
|
||||||
|
value = getattr(model_instance, self.target_field)
|
||||||
|
return self.naturalize_function(value, max_length=self.max_length)
|
||||||
|
|
||||||
|
def deconstruct(self):
|
||||||
|
kwargs = super().deconstruct()[3] # Pass kwargs from CharField
|
||||||
|
kwargs['naturalize_function'] = self.naturalize_function
|
||||||
|
return (
|
||||||
|
self.name,
|
||||||
|
'utilities.fields.NaturalOrderingField',
|
||||||
|
['target_field'],
|
||||||
|
kwargs,
|
||||||
|
)
|
||||||
|
@ -8,7 +8,7 @@ from django import forms
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.postgres.forms.jsonb import JSONField as _JSONField, InvalidJSONInput
|
from django.contrib.postgres.forms.jsonb import JSONField as _JSONField, InvalidJSONInput
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from mptt.forms import TreeNodeMultipleChoiceField
|
from django.forms import BoundField
|
||||||
|
|
||||||
from .choices import unpack_grouped_choices
|
from .choices import unpack_grouped_choices
|
||||||
from .constants import *
|
from .constants import *
|
||||||
@ -211,7 +211,7 @@ class SelectWithPK(StaticSelect2):
|
|||||||
option_template_name = 'widgets/select_option_with_pk.html'
|
option_template_name = 'widgets/select_option_with_pk.html'
|
||||||
|
|
||||||
|
|
||||||
class ContentTypeSelect(forms.Select):
|
class ContentTypeSelect(StaticSelect2):
|
||||||
"""
|
"""
|
||||||
Appends an `api-value` attribute equal to the slugified model name for each ContentType. For example:
|
Appends an `api-value` attribute equal to the slugified model name for each ContentType. For example:
|
||||||
<option value="37" api-value="console-server-port">console server port</option>
|
<option value="37" api-value="console-server-port">console server port</option>
|
||||||
@ -259,9 +259,6 @@ class APISelect(SelectWithDisabled):
|
|||||||
name of the query param and the value if the query param's value.
|
name of the query param and the value if the query param's value.
|
||||||
:param null_option: If true, include the static null option in the selection list.
|
:param null_option: If true, include the static null option in the selection list.
|
||||||
"""
|
"""
|
||||||
# Only preload the selected option(s); new options are dynamically displayed and added via the API
|
|
||||||
template_name = 'widgets/select_api.html'
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
api_url,
|
api_url,
|
||||||
@ -525,34 +522,6 @@ class FlexibleModelChoiceField(forms.ModelChoiceField):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
class ChainedModelChoiceField(forms.ModelChoiceField):
|
|
||||||
"""
|
|
||||||
A ModelChoiceField which is initialized based on the values of other fields within a form. `chains` is a dictionary
|
|
||||||
mapping of model fields to peer fields within the form. For example:
|
|
||||||
|
|
||||||
country1 = forms.ModelChoiceField(queryset=Country.objects.all())
|
|
||||||
city1 = ChainedModelChoiceField(queryset=City.objects.all(), chains={'country': 'country1'}
|
|
||||||
|
|
||||||
The queryset of the `city1` field will be modified as
|
|
||||||
|
|
||||||
.filter(country=<value>)
|
|
||||||
|
|
||||||
where <value> is the value of the `country1` field. (Note: The form must inherit from ChainedFieldsMixin.)
|
|
||||||
"""
|
|
||||||
def __init__(self, chains=None, *args, **kwargs):
|
|
||||||
self.chains = chains
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class ChainedModelMultipleChoiceField(forms.ModelMultipleChoiceField):
|
|
||||||
"""
|
|
||||||
See ChainedModelChoiceField
|
|
||||||
"""
|
|
||||||
def __init__(self, chains=None, *args, **kwargs):
|
|
||||||
self.chains = chains
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class SlugField(forms.SlugField):
|
class SlugField(forms.SlugField):
|
||||||
"""
|
"""
|
||||||
Extend the built-in SlugField to automatically populate from a field called `name` unless otherwise specified.
|
Extend the built-in SlugField to automatically populate from a field called `name` unless otherwise specified.
|
||||||
@ -581,46 +550,38 @@ class TagFilterField(forms.MultipleChoiceField):
|
|||||||
super().__init__(label='Tags', choices=get_choices, required=False, *args, **kwargs)
|
super().__init__(label='Tags', choices=get_choices, required=False, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class FilterChoiceIterator(forms.models.ModelChoiceIterator):
|
class DynamicModelChoiceMixin:
|
||||||
|
field_modifier = ''
|
||||||
|
|
||||||
def __iter__(self):
|
def get_bound_field(self, form, field_name):
|
||||||
# Filter on "empty" choice using FILTERS_NULL_CHOICE_VALUE (instead of an empty string)
|
bound_field = BoundField(form, self, field_name)
|
||||||
if self.field.null_label is not None:
|
|
||||||
yield (settings.FILTERS_NULL_CHOICE_VALUE, self.field.null_label)
|
# Modify the QuerySet of the field before we return it. Limit choices to any data already bound: Options
|
||||||
queryset = self.queryset.all()
|
# will be populated on-demand via the APISelect widget.
|
||||||
# Can't use iterator() when queryset uses prefetch_related()
|
field_name = '{}{}'.format(self.to_field_name or 'pk', self.field_modifier)
|
||||||
if not queryset._prefetch_related_lookups:
|
if bound_field.data:
|
||||||
queryset = queryset.iterator()
|
self.queryset = self.queryset.filter(**{field_name: self.prepare_value(bound_field.data)})
|
||||||
for obj in queryset:
|
elif bound_field.initial:
|
||||||
yield self.choice(obj)
|
self.queryset = self.queryset.filter(**{field_name: self.prepare_value(bound_field.initial)})
|
||||||
|
else:
|
||||||
|
self.queryset = self.queryset.none()
|
||||||
|
|
||||||
|
return bound_field
|
||||||
|
|
||||||
|
|
||||||
class FilterChoiceFieldMixin(object):
|
class DynamicModelChoiceField(DynamicModelChoiceMixin, forms.ModelChoiceField):
|
||||||
iterator = FilterChoiceIterator
|
"""
|
||||||
|
Override get_bound_field() to avoid pre-populating field choices with a SQL query. The field will be
|
||||||
def __init__(self, null_label=None, count_attr='filter_count', *args, **kwargs):
|
rendered only with choices set via bound data. Choices are populated on-demand via the APISelect widget.
|
||||||
self.null_label = null_label
|
"""
|
||||||
self.count_attr = count_attr
|
|
||||||
if 'required' not in kwargs:
|
|
||||||
kwargs['required'] = False
|
|
||||||
if 'widget' not in kwargs:
|
|
||||||
kwargs['widget'] = forms.SelectMultiple(attrs={'size': 6})
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def label_from_instance(self, obj):
|
|
||||||
label = super().label_from_instance(obj)
|
|
||||||
obj_count = getattr(obj, self.count_attr, None)
|
|
||||||
if obj_count is not None:
|
|
||||||
return '{} ({})'.format(label, obj_count)
|
|
||||||
return label
|
|
||||||
|
|
||||||
|
|
||||||
class FilterChoiceField(FilterChoiceFieldMixin, forms.ModelMultipleChoiceField):
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class FilterTreeNodeMultipleChoiceField(FilterChoiceFieldMixin, TreeNodeMultipleChoiceField):
|
class DynamicModelMultipleChoiceField(DynamicModelChoiceMixin, forms.ModelMultipleChoiceField):
|
||||||
pass
|
"""
|
||||||
|
A multiple-choice version of DynamicModelChoiceField.
|
||||||
|
"""
|
||||||
|
field_modifier = '__in'
|
||||||
|
|
||||||
|
|
||||||
class LaxURLField(forms.URLField):
|
class LaxURLField(forms.URLField):
|
||||||
@ -675,46 +636,6 @@ class BootstrapMixin(forms.BaseForm):
|
|||||||
field.widget.attrs['placeholder'] = field.label
|
field.widget.attrs['placeholder'] = field.label
|
||||||
|
|
||||||
|
|
||||||
class ChainedFieldsMixin(forms.BaseForm):
|
|
||||||
"""
|
|
||||||
Iterate through all ChainedModelChoiceFields in the form and modify their querysets based on chained fields.
|
|
||||||
"""
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
for field_name, field in self.fields.items():
|
|
||||||
|
|
||||||
if isinstance(field, ChainedModelChoiceField):
|
|
||||||
|
|
||||||
filters_dict = {}
|
|
||||||
for (db_field, parent_field) in field.chains:
|
|
||||||
if self.is_bound and parent_field in self.data and self.data[parent_field]:
|
|
||||||
filters_dict[db_field] = self.data[parent_field] or None
|
|
||||||
elif self.initial.get(parent_field):
|
|
||||||
filters_dict[db_field] = self.initial[parent_field]
|
|
||||||
elif self.fields[parent_field].widget.attrs.get('nullable'):
|
|
||||||
filters_dict[db_field] = None
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
# Limit field queryset by chained field values
|
|
||||||
if filters_dict:
|
|
||||||
field.queryset = field.queryset.filter(**filters_dict)
|
|
||||||
# Editing an existing instance; limit field to its current value
|
|
||||||
elif not self.is_bound and getattr(self, 'instance', None) and hasattr(self.instance, field_name):
|
|
||||||
obj = getattr(self.instance, field_name)
|
|
||||||
if obj is not None:
|
|
||||||
field.queryset = field.queryset.filter(pk=obj.pk)
|
|
||||||
else:
|
|
||||||
field.queryset = field.queryset.none()
|
|
||||||
# Creating a new instance with no bound data; nullify queryset
|
|
||||||
elif not self.data.get(field_name):
|
|
||||||
field.queryset = field.queryset.none()
|
|
||||||
# Creating a new instance with bound data; limit queryset to the specified value
|
|
||||||
else:
|
|
||||||
field.queryset = field.queryset.filter(pk=self.data.get(field_name))
|
|
||||||
|
|
||||||
|
|
||||||
class ReturnURLForm(forms.Form):
|
class ReturnURLForm(forms.Form):
|
||||||
"""
|
"""
|
||||||
Provides a hidden return URL field to control where the user is directed after the form is submitted.
|
Provides a hidden return URL field to control where the user is directed after the form is submitted.
|
||||||
|
@ -1,45 +0,0 @@
|
|||||||
from django.db.models import Manager
|
|
||||||
from django.db.models.expressions import RawSQL
|
|
||||||
|
|
||||||
NAT1 = r"CAST(SUBSTRING({}.{} FROM '^(\d{{1,9}})') AS integer)"
|
|
||||||
NAT2 = r"SUBSTRING({}.{} FROM '^\d*(.*?)\d*$')"
|
|
||||||
NAT3 = r"CAST(SUBSTRING({}.{} FROM '(\d{{1,9}})$') AS integer)"
|
|
||||||
|
|
||||||
|
|
||||||
class NaturalOrderingManager(Manager):
|
|
||||||
"""
|
|
||||||
Order objects naturally by a designated field (defaults to 'name'). Leading and/or trailing digits of values within
|
|
||||||
this field will be cast as independent integers and sorted accordingly. For example, "Foo2" will be ordered before
|
|
||||||
"Foo10", even though the digit 1 is normally ordered before the digit 2.
|
|
||||||
"""
|
|
||||||
natural_order_field = 'name'
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
|
|
||||||
queryset = super().get_queryset()
|
|
||||||
|
|
||||||
db_table = self.model._meta.db_table
|
|
||||||
db_field = self.natural_order_field
|
|
||||||
|
|
||||||
# Append the three subfields derived from the designated natural ordering field
|
|
||||||
queryset = (
|
|
||||||
queryset.annotate(_nat1=RawSQL(NAT1.format(db_table, db_field), ()))
|
|
||||||
.annotate(_nat2=RawSQL(NAT2.format(db_table, db_field), ()))
|
|
||||||
.annotate(_nat3=RawSQL(NAT3.format(db_table, db_field), ()))
|
|
||||||
)
|
|
||||||
|
|
||||||
# Replace any instance of the designated natural ordering field with its three subfields
|
|
||||||
ordering = []
|
|
||||||
for field in self.model._meta.ordering:
|
|
||||||
if field == self.natural_order_field:
|
|
||||||
ordering.append('_nat1')
|
|
||||||
ordering.append('_nat2')
|
|
||||||
ordering.append('_nat3')
|
|
||||||
else:
|
|
||||||
ordering.append(field)
|
|
||||||
|
|
||||||
# Default to using the _nat indexes if Meta.ordering is empty
|
|
||||||
if not ordering:
|
|
||||||
ordering = ('_nat1', '_nat2', '_nat3')
|
|
||||||
|
|
||||||
return queryset.order_by(*ordering)
|
|
80
netbox/utilities/ordering.py
Normal file
80
netbox/utilities/ordering.py
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
INTERFACE_NAME_REGEX = r'(^(?P<type>[^\d\.:]+)?)' \
|
||||||
|
r'((?P<slot>\d+)/)?' \
|
||||||
|
r'((?P<subslot>\d+)/)?' \
|
||||||
|
r'((?P<position>\d+)/)?' \
|
||||||
|
r'((?P<subposition>\d+)/)?' \
|
||||||
|
r'((?P<id>\d+))?' \
|
||||||
|
r'(:(?P<channel>\d+))?' \
|
||||||
|
r'(.(?P<vc>\d+)$)?'
|
||||||
|
|
||||||
|
|
||||||
|
def naturalize(value, max_length=None, integer_places=8):
|
||||||
|
"""
|
||||||
|
Take an alphanumeric string and prepend all integers to `integer_places` places to ensure the strings
|
||||||
|
are ordered naturally. For example:
|
||||||
|
|
||||||
|
site9router21
|
||||||
|
site10router4
|
||||||
|
site10router19
|
||||||
|
|
||||||
|
becomes:
|
||||||
|
|
||||||
|
site00000009router00000021
|
||||||
|
site00000010router00000004
|
||||||
|
site00000010router00000019
|
||||||
|
|
||||||
|
:param value: The value to be naturalized
|
||||||
|
:param max_length: The maximum length of the returned string. Characters beyond this length will be stripped.
|
||||||
|
:param integer_places: The number of places to which each integer will be expanded. (Default: 8)
|
||||||
|
"""
|
||||||
|
if not value:
|
||||||
|
return value
|
||||||
|
output = []
|
||||||
|
for segment in re.split(r'(\d+)', value):
|
||||||
|
if segment.isdigit():
|
||||||
|
output.append(segment.rjust(integer_places, '0'))
|
||||||
|
elif segment:
|
||||||
|
output.append(segment)
|
||||||
|
ret = ''.join(output)
|
||||||
|
|
||||||
|
return ret[:max_length] if max_length else ret
|
||||||
|
|
||||||
|
|
||||||
|
def naturalize_interface(value, max_length=None):
|
||||||
|
"""
|
||||||
|
Similar in nature to naturalize(), but takes into account a particular naming format adapted from the old
|
||||||
|
InterfaceManager.
|
||||||
|
|
||||||
|
:param value: The value to be naturalized
|
||||||
|
:param max_length: The maximum length of the returned string. Characters beyond this length will be stripped.
|
||||||
|
"""
|
||||||
|
output = []
|
||||||
|
match = re.search(INTERFACE_NAME_REGEX, value)
|
||||||
|
if match is None:
|
||||||
|
return value
|
||||||
|
|
||||||
|
# First, we order by slot/position, padding each to four digits. If a field is not present,
|
||||||
|
# set it to 9999 to ensure it is ordered last.
|
||||||
|
for part_name in ('slot', 'subslot', 'position', 'subposition'):
|
||||||
|
part = match.group(part_name)
|
||||||
|
if part is not None:
|
||||||
|
output.append(part.rjust(4, '0'))
|
||||||
|
else:
|
||||||
|
output.append('9999')
|
||||||
|
|
||||||
|
# Append the type, if any.
|
||||||
|
if match.group('type') is not None:
|
||||||
|
output.append(match.group('type'))
|
||||||
|
|
||||||
|
# Finally, append any remaining fields, left-padding to eight digits each.
|
||||||
|
for part_name in ('id', 'channel', 'vc'):
|
||||||
|
part = match.group(part_name)
|
||||||
|
if part is not None:
|
||||||
|
output.append(part.rjust(6, '0'))
|
||||||
|
else:
|
||||||
|
output.append('000000')
|
||||||
|
|
||||||
|
ret = ''.join(output)
|
||||||
|
return ret[:max_length] if max_length else ret
|
@ -1,9 +0,0 @@
|
|||||||
<select name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>
|
|
||||||
{% for group_name, group_choices, group_index in widget.optgroups %}
|
|
||||||
{% if group_name %}<optgroup label="{{ group_name }}">{% endif %}
|
|
||||||
{% for option in group_choices %}
|
|
||||||
{% if option.attrs.selected or option.value == "null" %}{% include option.template_name with widget=option %}{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% if group_name %}</optgroup>{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
@ -100,7 +100,7 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer):
|
|||||||
class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
|
class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||||
virtual_machine = NestedVirtualMachineSerializer()
|
virtual_machine = NestedVirtualMachineSerializer()
|
||||||
type = ChoiceField(choices=VMInterfaceTypeChoices, default=VMInterfaceTypeChoices.TYPE_VIRTUAL, required=False)
|
type = ChoiceField(choices=VMInterfaceTypeChoices, default=VMInterfaceTypeChoices.TYPE_VIRTUAL, required=False)
|
||||||
mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_null=True)
|
mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
|
||||||
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
|
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
|
||||||
tagged_vlans = SerializedPKRelatedField(
|
tagged_vlans = SerializedPKRelatedField(
|
||||||
queryset=VLAN.objects.all(),
|
queryset=VLAN.objects.all(),
|
||||||
|
@ -14,9 +14,8 @@ from tenancy.forms import TenancyFilterForm, TenancyForm
|
|||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
|
add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
|
||||||
ChainedFieldsMixin, ChainedModelChoiceField, ChainedModelMultipleChoiceField, CommentField, ConfirmationForm,
|
CommentField, ConfirmationForm, CSVChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
|
||||||
CSVChoiceField, ExpandableNameField, FilterChoiceField, JSONField, SlugField, SmallTextarea, StaticSelect2,
|
ExpandableNameField, JSONField, SlugField, SmallTextarea, StaticSelect2, StaticSelect2Multiple, TagFilterField,
|
||||||
StaticSelect2Multiple, TagFilterField,
|
|
||||||
)
|
)
|
||||||
from .choices import *
|
from .choices import *
|
||||||
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
||||||
@ -77,6 +76,26 @@ class ClusterGroupCSVForm(forms.ModelForm):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||||
|
type = DynamicModelChoiceField(
|
||||||
|
queryset=ClusterType.objects.all(),
|
||||||
|
widget=APISelect(
|
||||||
|
api_url="/api/virtualization/cluster-types/"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
group = DynamicModelChoiceField(
|
||||||
|
queryset=ClusterGroup.objects.all(),
|
||||||
|
required=False,
|
||||||
|
widget=APISelect(
|
||||||
|
api_url="/api/virtualization/cluster-groups/"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
site = DynamicModelChoiceField(
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
required=False,
|
||||||
|
widget=APISelect(
|
||||||
|
api_url="/api/dcim/sites/"
|
||||||
|
)
|
||||||
|
)
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
tags = TagField(
|
tags = TagField(
|
||||||
required=False
|
required=False
|
||||||
@ -84,20 +103,9 @@ class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Cluster
|
model = Cluster
|
||||||
fields = [
|
fields = (
|
||||||
'name', 'type', 'group', 'tenant', 'site', 'comments', 'tags',
|
'name', 'type', 'group', 'tenant', 'site', 'comments', 'tags',
|
||||||
]
|
)
|
||||||
widgets = {
|
|
||||||
'type': APISelect(
|
|
||||||
api_url="/api/virtualization/cluster-types/"
|
|
||||||
),
|
|
||||||
'group': APISelect(
|
|
||||||
api_url="/api/virtualization/cluster-groups/"
|
|
||||||
),
|
|
||||||
'site': APISelect(
|
|
||||||
api_url="/api/dcim/sites/"
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ClusterCSVForm(CustomFieldModelCSVForm):
|
class ClusterCSVForm(CustomFieldModelCSVForm):
|
||||||
@ -147,25 +155,28 @@ class ClusterBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
|
|||||||
queryset=Cluster.objects.all(),
|
queryset=Cluster.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput()
|
widget=forms.MultipleHiddenInput()
|
||||||
)
|
)
|
||||||
type = forms.ModelChoiceField(
|
type = DynamicModelChoiceField(
|
||||||
queryset=ClusterType.objects.all(),
|
queryset=ClusterType.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url="/api/virtualization/cluster-types/"
|
api_url="/api/virtualization/cluster-types/"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
group = forms.ModelChoiceField(
|
group = DynamicModelChoiceField(
|
||||||
queryset=ClusterGroup.objects.all(),
|
queryset=ClusterGroup.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url="/api/virtualization/cluster-groups/"
|
api_url="/api/virtualization/cluster-groups/"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
tenant = forms.ModelChoiceField(
|
tenant = DynamicModelChoiceField(
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
required=False
|
required=False,
|
||||||
|
widget=APISelect(
|
||||||
|
api_url="/api/tenancy/tenants/"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
site = forms.ModelChoiceField(
|
site = DynamicModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -189,7 +200,7 @@ class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
|
|||||||
'q', 'type', 'region', 'site', 'group', 'tenant_group', 'tenant'
|
'q', 'type', 'region', 'site', 'group', 'tenant_group', 'tenant'
|
||||||
]
|
]
|
||||||
q = forms.CharField(required=False, label='Search')
|
q = forms.CharField(required=False, label='Search')
|
||||||
type = FilterChoiceField(
|
type = DynamicModelMultipleChoiceField(
|
||||||
queryset=ClusterType.objects.all(),
|
queryset=ClusterType.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
required=False,
|
required=False,
|
||||||
@ -198,7 +209,7 @@ class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
|
|||||||
value_field='slug',
|
value_field='slug',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
region = FilterChoiceField(
|
region = DynamicModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
required=False,
|
required=False,
|
||||||
@ -210,10 +221,9 @@ class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
site = FilterChoiceField(
|
site = DynamicModelMultipleChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
null_label='-- None --',
|
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url="/api/dcim/sites/",
|
api_url="/api/dcim/sites/",
|
||||||
@ -221,10 +231,9 @@ class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
|
|||||||
null_option=True,
|
null_option=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
group = FilterChoiceField(
|
group = DynamicModelMultipleChoiceField(
|
||||||
queryset=ClusterGroup.objects.all(),
|
queryset=ClusterGroup.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
null_label='-- None --',
|
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url="/api/virtualization/cluster-groups/",
|
api_url="/api/virtualization/cluster-groups/",
|
||||||
@ -235,8 +244,8 @@ class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
|
|||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
class ClusterAddDevicesForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
|
class ClusterAddDevicesForm(BootstrapMixin, forms.Form):
|
||||||
region = forms.ModelChoiceField(
|
region = DynamicModelChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -249,11 +258,8 @@ class ClusterAddDevicesForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
site = ChainedModelChoiceField(
|
site = DynamicModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
chains=(
|
|
||||||
('region', 'region'),
|
|
||||||
),
|
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url='/api/dcim/sites/',
|
api_url='/api/dcim/sites/',
|
||||||
@ -263,11 +269,8 @@ class ClusterAddDevicesForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
rack = ChainedModelChoiceField(
|
rack = DynamicModelChoiceField(
|
||||||
queryset=Rack.objects.all(),
|
queryset=Rack.objects.all(),
|
||||||
chains=(
|
|
||||||
('site', 'site'),
|
|
||||||
),
|
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url='/api/dcim/racks/',
|
api_url='/api/dcim/racks/',
|
||||||
@ -279,12 +282,8 @@ class ClusterAddDevicesForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
devices = ChainedModelMultipleChoiceField(
|
devices = DynamicModelMultipleChoiceField(
|
||||||
queryset=Device.objects.filter(cluster__isnull=True),
|
queryset=Device.objects.filter(cluster__isnull=True),
|
||||||
chains=(
|
|
||||||
('site', 'site'),
|
|
||||||
('rack', 'rack'),
|
|
||||||
),
|
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url='/api/dcim/devices/',
|
api_url='/api/dcim/devices/',
|
||||||
display_field='display_name',
|
display_field='display_name',
|
||||||
@ -331,7 +330,7 @@ class ClusterRemoveDevicesForm(ConfirmationForm):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||||
cluster_group = forms.ModelChoiceField(
|
cluster_group = DynamicModelChoiceField(
|
||||||
queryset=ClusterGroup.objects.all(),
|
queryset=ClusterGroup.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -344,15 +343,28 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
cluster = ChainedModelChoiceField(
|
cluster = DynamicModelChoiceField(
|
||||||
queryset=Cluster.objects.all(),
|
queryset=Cluster.objects.all(),
|
||||||
chains=(
|
|
||||||
('group', 'cluster_group'),
|
|
||||||
),
|
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url='/api/virtualization/clusters/'
|
api_url='/api/virtualization/clusters/'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
role = DynamicModelChoiceField(
|
||||||
|
queryset=DeviceRole.objects.all(),
|
||||||
|
widget=APISelect(
|
||||||
|
api_url="/api/dcim/device-roles/",
|
||||||
|
additional_query_params={
|
||||||
|
"vm_role": "True"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
platform = DynamicModelChoiceField(
|
||||||
|
queryset=Platform.objects.all(),
|
||||||
|
required=False,
|
||||||
|
widget=APISelect(
|
||||||
|
api_url='/api/dcim/platforms/'
|
||||||
|
)
|
||||||
|
)
|
||||||
tags = TagField(
|
tags = TagField(
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
@ -373,17 +385,8 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
}
|
}
|
||||||
widgets = {
|
widgets = {
|
||||||
"status": StaticSelect2(),
|
"status": StaticSelect2(),
|
||||||
"role": APISelect(
|
|
||||||
api_url="/api/dcim/device-roles/",
|
|
||||||
additional_query_params={
|
|
||||||
"vm_role": "True"
|
|
||||||
}
|
|
||||||
),
|
|
||||||
'primary_ip4': StaticSelect2(),
|
'primary_ip4': StaticSelect2(),
|
||||||
'primary_ip6': StaticSelect2(),
|
'primary_ip6': StaticSelect2(),
|
||||||
'platform': APISelect(
|
|
||||||
api_url='/api/dcim/platforms/'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -493,14 +496,14 @@ class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldB
|
|||||||
initial='',
|
initial='',
|
||||||
widget=StaticSelect2(),
|
widget=StaticSelect2(),
|
||||||
)
|
)
|
||||||
cluster = forms.ModelChoiceField(
|
cluster = DynamicModelChoiceField(
|
||||||
queryset=Cluster.objects.all(),
|
queryset=Cluster.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url='/api/virtualization/clusters/'
|
api_url='/api/virtualization/clusters/'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
role = forms.ModelChoiceField(
|
role = DynamicModelChoiceField(
|
||||||
queryset=DeviceRole.objects.filter(
|
queryset=DeviceRole.objects.filter(
|
||||||
vm_role=True
|
vm_role=True
|
||||||
),
|
),
|
||||||
@ -512,14 +515,14 @@ class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldB
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
tenant = forms.ModelChoiceField(
|
tenant = DynamicModelChoiceField(
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url='/api/tenancy/tenants/'
|
api_url='/api/tenancy/tenants/'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
platform = forms.ModelChoiceField(
|
platform = DynamicModelChoiceField(
|
||||||
queryset=Platform.objects.all(),
|
queryset=Platform.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -559,34 +562,35 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil
|
|||||||
required=False,
|
required=False,
|
||||||
label='Search'
|
label='Search'
|
||||||
)
|
)
|
||||||
cluster_group = FilterChoiceField(
|
cluster_group = DynamicModelMultipleChoiceField(
|
||||||
queryset=ClusterGroup.objects.all(),
|
queryset=ClusterGroup.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
null_label='-- None --',
|
required=False,
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url='/api/virtualization/cluster-groups/',
|
api_url='/api/virtualization/cluster-groups/',
|
||||||
value_field="slug",
|
value_field="slug",
|
||||||
null_option=True,
|
null_option=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
cluster_type = FilterChoiceField(
|
cluster_type = DynamicModelMultipleChoiceField(
|
||||||
queryset=ClusterType.objects.all(),
|
queryset=ClusterType.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
null_label='-- None --',
|
required=False,
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url='/api/virtualization/cluster-types/',
|
api_url='/api/virtualization/cluster-types/',
|
||||||
value_field="slug",
|
value_field="slug",
|
||||||
null_option=True,
|
null_option=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
cluster_id = FilterChoiceField(
|
cluster_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Cluster.objects.all(),
|
queryset=Cluster.objects.all(),
|
||||||
|
required=False,
|
||||||
label='Cluster',
|
label='Cluster',
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url='/api/virtualization/clusters/',
|
api_url='/api/virtualization/clusters/',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
region = FilterChoiceField(
|
region = DynamicModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
required=False,
|
required=False,
|
||||||
@ -598,20 +602,20 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
site = FilterChoiceField(
|
site = DynamicModelMultipleChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
null_label='-- None --',
|
required=False,
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url='/api/dcim/sites/',
|
api_url='/api/dcim/sites/',
|
||||||
value_field="slug",
|
value_field="slug",
|
||||||
null_option=True,
|
null_option=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
role = FilterChoiceField(
|
role = DynamicModelMultipleChoiceField(
|
||||||
queryset=DeviceRole.objects.filter(vm_role=True),
|
queryset=DeviceRole.objects.filter(vm_role=True),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
null_label='-- None --',
|
required=False,
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url='/api/dcim/device-roles/',
|
api_url='/api/dcim/device-roles/',
|
||||||
value_field="slug",
|
value_field="slug",
|
||||||
@ -626,10 +630,10 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil
|
|||||||
required=False,
|
required=False,
|
||||||
widget=StaticSelect2Multiple()
|
widget=StaticSelect2Multiple()
|
||||||
)
|
)
|
||||||
platform = FilterChoiceField(
|
platform = DynamicModelMultipleChoiceField(
|
||||||
queryset=Platform.objects.all(),
|
queryset=Platform.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
null_label='-- None --',
|
required=False,
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url='/api/dcim/platforms/',
|
api_url='/api/dcim/platforms/',
|
||||||
value_field="slug",
|
value_field="slug",
|
||||||
@ -648,7 +652,7 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil
|
|||||||
#
|
#
|
||||||
|
|
||||||
class InterfaceForm(BootstrapMixin, forms.ModelForm):
|
class InterfaceForm(BootstrapMixin, forms.ModelForm):
|
||||||
untagged_vlan = forms.ModelChoiceField(
|
untagged_vlan = DynamicModelChoiceField(
|
||||||
queryset=VLAN.objects.all(),
|
queryset=VLAN.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -657,7 +661,7 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
|
|||||||
full=True
|
full=True
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
tagged_vlans = forms.ModelMultipleChoiceField(
|
tagged_vlans = DynamicModelMultipleChoiceField(
|
||||||
queryset=VLAN.objects.all(),
|
queryset=VLAN.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
@ -774,7 +778,7 @@ class InterfaceCreateForm(BootstrapMixin, forms.Form):
|
|||||||
required=False,
|
required=False,
|
||||||
widget=StaticSelect2(),
|
widget=StaticSelect2(),
|
||||||
)
|
)
|
||||||
untagged_vlan = forms.ModelChoiceField(
|
untagged_vlan = DynamicModelChoiceField(
|
||||||
queryset=VLAN.objects.all(),
|
queryset=VLAN.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -783,7 +787,7 @@ class InterfaceCreateForm(BootstrapMixin, forms.Form):
|
|||||||
full=True
|
full=True
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
tagged_vlans = forms.ModelMultipleChoiceField(
|
tagged_vlans = DynamicModelMultipleChoiceField(
|
||||||
queryset=VLAN.objects.all(),
|
queryset=VLAN.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
@ -862,7 +866,7 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
|
|||||||
required=False,
|
required=False,
|
||||||
widget=StaticSelect2()
|
widget=StaticSelect2()
|
||||||
)
|
)
|
||||||
untagged_vlan = forms.ModelChoiceField(
|
untagged_vlan = DynamicModelChoiceField(
|
||||||
queryset=VLAN.objects.all(),
|
queryset=VLAN.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -871,7 +875,7 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
|
|||||||
full=True
|
full=True
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
tagged_vlans = forms.ModelMultipleChoiceField(
|
tagged_vlans = DynamicModelMultipleChoiceField(
|
||||||
queryset=VLAN.objects.all(),
|
queryset=VLAN.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
|
Loading…
Reference in New Issue
Block a user