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