Merge branch 'develop' into 3995-navbar-overflow

This commit is contained in:
Jeremy Stretch 2020-02-12 09:40:31 -05:00 committed by GitHub
commit 5befa533c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 1709 additions and 887 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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
),
]

View File

@ -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
),
]

View 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
),
]

View 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
),
]

View File

@ -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'),

View File

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

View File

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

View File

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

View File

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

View File

@ -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'),

View File

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

View File

@ -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'),

View File

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

View File

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

View File

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

View File

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

View File

@ -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'
}); });
} }

View File

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

View File

@ -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' %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

@ -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(),

View File

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