Compare commits

...

11 Commits

Author SHA1 Message Date
Jeremy Stretch
c5471a1f6e Define UI layout for Module view 2025-12-31 15:48:37 -05:00
Jeremy Stretch
976b76dcb2 Define UI layout for Platform view 2025-12-31 15:35:24 -05:00
Jeremy Stretch
06d53ef10b Define UI layout for DeviceRole view 2025-12-31 15:33:17 -05:00
Jeremy Stretch
eb01f6fde8 Define UI layout for ModuleType view 2025-12-31 15:17:12 -05:00
Jeremy Stretch
fba40ddf72 Permit passing template_name to Panel instance 2025-12-31 15:16:15 -05:00
Jeremy Stretch
ebada4bf72 Closes #21001: Annotate plugin filterset registration in v4.5 release notes (#21058)
Some checks failed
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2025-12-31 09:42:47 -06:00
Jeremy Stretch
c78b8401dc Fixes #21020: Fix object filtering for image attachments panel (#21030)
Some checks failed
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2025-12-29 15:19:24 -06:00
bctiemann
edf35e35be Merge pull request #21028 from netbox-community/fix/device-api-missing-owner-field
Some checks failed
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Fix missing owner field in DeviceWithConfigContextSerializer
2025-12-22 14:28:58 -05:00
Jeremy Stretch
062a871521 Add missing owner field to device & VM component serializers
Some checks failed
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
2025-12-22 13:52:39 -05:00
Mark Coleman
07d8157ccd Fix missing owner field in DeviceWithConfigContextSerializer
Fixes: https://github.com/netbox-community/netbox/issues/21022
2025-12-20 11:02:36 +01:00
Jeremy Stretch
712c743bcb Closes #20954: Add indexes for GFKs (#21015)
Some checks failed
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2025-12-18 14:49:00 -08:00
24 changed files with 345 additions and 258 deletions

View File

@@ -22,7 +22,7 @@
#### Lookup Modifiers in Filter Forms ([#7604](https://github.com/netbox-community/netbox/issues/7604)) #### Lookup Modifiers in Filter Forms ([#7604](https://github.com/netbox-community/netbox/issues/7604))
Most object list filters within the UI have been extended to include optional lookup modifiers to support more complex queries. For instance, filters for numeric values now include a dropdown where a user can select "less than," "greater than," or "not" in addition to the default equivalency match. The specific modifiers available depend on the type of each filter. Most object list filters within the UI have been extended to include optional lookup modifiers to support more complex queries. For instance, filters for numeric values now include a dropdown where a user can select "less than," "greater than," or "not" in addition to the default equivalency match. The specific modifiers available depend on the type of each filter. Plugins can register their own filtersets using the `register_filterset()` decorator to enable this new functionality.
(Note that this feature does not introduce any new filters. Rather, it makes available in the UI filters which already exist.) (Note that this feature does not introduce any new filters. Rather, it makes available in the UI filters which already exist.)

View File

@@ -0,0 +1,17 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0055_add_comments_to_organizationalmodel'),
('contenttypes', '0002_remove_content_type_name'),
('dcim', '0224_add_comments_to_organizationalmodel'),
('extras', '0134_owner'),
]
operations = [
migrations.AddIndex(
model_name='circuittermination',
index=models.Index(fields=['termination_type', 'termination_id'], name='circuits_ci_termina_505dda_idx'),
),
]

View File

@@ -335,6 +335,9 @@ class CircuitTermination(
name='%(app_label)s_%(class)s_unique_circuit_term_side' name='%(app_label)s_%(class)s_unique_circuit_term_side'
), ),
) )
indexes = (
models.Index(fields=('termination_type', 'termination_id')),
)
verbose_name = _('circuit termination') verbose_name = _('circuit termination')
verbose_name_plural = _('circuit terminations') verbose_name_plural = _('circuit terminations')

View File

@@ -14,6 +14,7 @@ from ipam.models import VLAN
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
from netbox.api.gfk_fields import GFKSerializerField from netbox.api.gfk_fields import GFKSerializerField
from netbox.api.serializers import NetBoxModelSerializer from netbox.api.serializers import NetBoxModelSerializer
from users.api.serializers_.mixins import OwnerMixin
from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer
from wireless.api.serializers_.nested import NestedWirelessLinkSerializer from wireless.api.serializers_.nested import NestedWirelessLinkSerializer
from wireless.api.serializers_.wirelesslans import WirelessLANSerializer from wireless.api.serializers_.wirelesslans import WirelessLANSerializer
@@ -40,7 +41,12 @@ __all__ = (
) )
class ConsoleServerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): class ConsoleServerPortSerializer(
OwnerMixin,
NetBoxModelSerializer,
CabledObjectSerializer,
ConnectedEndpointsSerializer
):
device = DeviceSerializer(nested=True) device = DeviceSerializer(nested=True)
module = ModuleSerializer( module = ModuleSerializer(
nested=True, nested=True,
@@ -64,13 +70,18 @@ class ConsoleServerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer,
fields = [ fields = [
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description', 'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description',
'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints',
'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created', 'connected_endpoints_type', 'connected_endpoints_reachable', 'owner', 'tags', 'custom_fields', 'created',
'last_updated', '_occupied', 'last_updated', '_occupied',
] ]
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
class ConsolePortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): class ConsolePortSerializer(
OwnerMixin,
NetBoxModelSerializer,
CabledObjectSerializer,
ConnectedEndpointsSerializer
):
device = DeviceSerializer(nested=True) device = DeviceSerializer(nested=True)
module = ModuleSerializer( module = ModuleSerializer(
nested=True, nested=True,
@@ -94,13 +105,18 @@ class ConsolePortSerializer(NetBoxModelSerializer, CabledObjectSerializer, Conne
fields = [ fields = [
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description', 'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description',
'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints',
'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created', 'connected_endpoints_type', 'connected_endpoints_reachable', 'owner', 'tags', 'custom_fields', 'created',
'last_updated', '_occupied', 'last_updated', '_occupied',
] ]
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
class PowerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): class PowerPortSerializer(
OwnerMixin,
NetBoxModelSerializer,
CabledObjectSerializer,
ConnectedEndpointsSerializer
):
device = DeviceSerializer(nested=True) device = DeviceSerializer(nested=True)
module = ModuleSerializer( module = ModuleSerializer(
nested=True, nested=True,
@@ -120,13 +136,18 @@ class PowerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
fields = [ fields = [
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'maximum_draw',
'allocated_draw', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'allocated_draw', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'owner', 'tags',
'created', 'last_updated', '_occupied', 'custom_fields', 'created', 'last_updated', '_occupied',
] ]
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): class PowerOutletSerializer(
OwnerMixin,
NetBoxModelSerializer,
CabledObjectSerializer,
ConnectedEndpointsSerializer
):
device = DeviceSerializer(nested=True) device = DeviceSerializer(nested=True)
module = ModuleSerializer( module = ModuleSerializer(
nested=True, nested=True,
@@ -159,12 +180,17 @@ class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, Conne
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'status', 'color', 'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'status', 'color',
'power_port', 'feed_leg', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'power_port', 'feed_leg', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers',
'link_peers_type', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'link_peers_type', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable',
'tags', 'custom_fields', 'created', 'last_updated', '_occupied', 'owner', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
] ]
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): class InterfaceSerializer(
OwnerMixin,
NetBoxModelSerializer,
CabledObjectSerializer,
ConnectedEndpointsSerializer
):
device = DeviceSerializer(nested=True) device = DeviceSerializer(nested=True)
vdcs = SerializedPKRelatedField( vdcs = SerializedPKRelatedField(
queryset=VirtualDeviceContext.objects.all(), queryset=VirtualDeviceContext.objects.all(),
@@ -226,7 +252,7 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan',
'vlan_translation_policy', 'mark_connected', 'cable', 'cable_end', 'wireless_link', 'link_peers', 'vlan_translation_policy', 'mark_connected', 'cable', 'cable_end', 'wireless_link', 'link_peers',
'link_peers_type', 'wireless_lans', 'vrf', 'l2vpn_termination', 'connected_endpoints', 'link_peers_type', 'wireless_lans', 'vrf', 'l2vpn_termination', 'connected_endpoints',
'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created', 'connected_endpoints_type', 'connected_endpoints_reachable', 'owner', 'tags', 'custom_fields', 'created',
'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied', 'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
] ]
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
@@ -307,7 +333,7 @@ class RearPortMappingSerializer(serializers.ModelSerializer):
fields = ('position', 'front_port', 'front_port_position') fields = ('position', 'front_port', 'front_port_position')
class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, PortSerializer): class RearPortSerializer(OwnerMixin, NetBoxModelSerializer, CabledObjectSerializer, PortSerializer):
device = DeviceSerializer(nested=True) device = DeviceSerializer(nested=True)
module = ModuleSerializer( module = ModuleSerializer(
nested=True, nested=True,
@@ -327,7 +353,7 @@ class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, PortSeri
fields = [ fields = [
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions',
'front_ports', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'front_ports', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
'tags', 'custom_fields', 'created', 'last_updated', '_occupied', 'owner', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
] ]
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
@@ -345,7 +371,7 @@ class FrontPortMappingSerializer(serializers.ModelSerializer):
fields = ('position', 'rear_port', 'rear_port_position') fields = ('position', 'rear_port', 'rear_port_position')
class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, PortSerializer): class FrontPortSerializer(OwnerMixin, NetBoxModelSerializer, CabledObjectSerializer, PortSerializer):
device = DeviceSerializer(nested=True) device = DeviceSerializer(nested=True)
module = ModuleSerializer( module = ModuleSerializer(
nested=True, nested=True,
@@ -365,12 +391,12 @@ class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, PortSer
fields = [ fields = [
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions',
'rear_ports', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'rear_ports', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
'tags', 'custom_fields', 'created', 'last_updated', '_occupied', 'owner', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
] ]
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
class ModuleBaySerializer(NetBoxModelSerializer): class ModuleBaySerializer(OwnerMixin, NetBoxModelSerializer):
device = DeviceSerializer(nested=True) device = DeviceSerializer(nested=True)
module = ModuleSerializer( module = ModuleSerializer(
nested=True, nested=True,
@@ -390,12 +416,12 @@ class ModuleBaySerializer(NetBoxModelSerializer):
model = ModuleBay model = ModuleBay
fields = [ fields = [
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'installed_module', 'label', 'position', 'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'installed_module', 'label', 'position',
'description', 'tags', 'custom_fields', 'created', 'last_updated', 'description', 'owner', 'tags', 'custom_fields', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'installed_module', 'name', 'description') brief_fields = ('id', 'url', 'display', 'installed_module', 'name', 'description')
class DeviceBaySerializer(NetBoxModelSerializer): class DeviceBaySerializer(OwnerMixin, NetBoxModelSerializer):
device = DeviceSerializer(nested=True) device = DeviceSerializer(nested=True)
installed_device = DeviceSerializer(nested=True, required=False, allow_null=True) installed_device = DeviceSerializer(nested=True, required=False, allow_null=True)
@@ -403,12 +429,12 @@ class DeviceBaySerializer(NetBoxModelSerializer):
model = DeviceBay model = DeviceBay
fields = [ fields = [
'id', 'url', 'display_url', 'display', 'device', 'name', 'label', 'description', 'installed_device', 'id', 'url', 'display_url', 'display', 'device', 'name', 'label', 'description', 'installed_device',
'tags', 'custom_fields', 'created', 'last_updated', 'owner', 'tags', 'custom_fields', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description') brief_fields = ('id', 'url', 'display', 'device', 'name', 'description')
class InventoryItemSerializer(NetBoxModelSerializer): class InventoryItemSerializer(OwnerMixin, NetBoxModelSerializer):
device = DeviceSerializer(nested=True) device = DeviceSerializer(nested=True)
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None) parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
role = InventoryItemRoleSerializer(nested=True, required=False, allow_null=True) role = InventoryItemRoleSerializer(nested=True, required=False, allow_null=True)
@@ -427,6 +453,6 @@ class InventoryItemSerializer(NetBoxModelSerializer):
fields = [ fields = [
'id', 'url', 'display_url', 'display', 'device', 'parent', 'name', 'label', 'status', 'role', 'id', 'url', 'display_url', 'display', 'device', 'parent', 'name', 'label', 'status', 'role',
'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description', 'component_type', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description', 'component_type',
'component_id', 'component', 'tags', 'custom_fields', 'created', 'last_updated', '_depth', 'component_id', 'component', 'owner', 'tags', 'custom_fields', 'created', 'last_updated', '_depth',
] ]
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', '_depth') brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', '_depth')

View File

@@ -111,7 +111,7 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
'id', 'url', 'display_url', 'display', 'name', 'device_type', 'role', 'tenant', 'platform', 'serial', 'id', 'url', 'display_url', 'display', 'name', 'device_type', 'role', 'tenant', 'platform', 'serial',
'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device',
'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', 'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis',
'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'config_context', 'vc_position', 'vc_priority', 'description', 'owner', 'comments', 'config_template', 'config_context',
'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', 'console_port_count', 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', 'console_port_count',
'console_server_port_count', 'power_port_count', 'power_outlet_count', 'interface_count', 'console_server_port_count', 'power_port_count', 'power_outlet_count', 'interface_count',
'front_port_count', 'rear_port_count', 'device_bay_count', 'module_bay_count', 'inventory_item_count', 'front_port_count', 'rear_port_count', 'device_bay_count', 'module_bay_count', 'inventory_item_count',

View File

@@ -0,0 +1,19 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('dcim', '0224_add_comments_to_organizationalmodel'),
('extras', '0134_owner'),
('users', '0015_owner'),
]
operations = [
migrations.AddIndex(
model_name='macaddress',
index=models.Index(
fields=['assigned_object_type', 'assigned_object_id'], name='dcim_macadd_assigne_54115d_idx'
),
),
]

View File

@@ -1318,7 +1318,10 @@ class MACAddress(PrimaryModel):
) )
class Meta: class Meta:
ordering = ('mac_address', 'pk',) ordering = ('mac_address', 'pk')
indexes = (
models.Index(fields=('assigned_object_type', 'assigned_object_id')),
)
verbose_name = _('MAC address') verbose_name = _('MAC address')
verbose_name_plural = _('MAC addresses') verbose_name_plural = _('MAC addresses')

View File

@@ -129,6 +129,12 @@ class DeviceDimensionsPanel(panels.ObjectAttributesPanel):
total_weight = attrs.TemplatedAttr('total_weight', template_name='dcim/device/attrs/total_weight.html') total_weight = attrs.TemplatedAttr('total_weight', template_name='dcim/device/attrs/total_weight.html')
class DeviceRolePanel(panels.NestedGroupObjectPanel):
color = attrs.ColorAttr('color')
vm_role = attrs.BooleanAttr('vm_role', label=_('VM role'))
config_template = attrs.RelatedObjectAttr('config_template', linkify=True)
class DeviceTypePanel(panels.ObjectAttributesPanel): class DeviceTypePanel(panels.ObjectAttributesPanel):
manufacturer = attrs.RelatedObjectAttr('manufacturer', linkify=True) manufacturer = attrs.RelatedObjectAttr('manufacturer', linkify=True)
model = attrs.TextAttr('model') model = attrs.TextAttr('model')
@@ -145,11 +151,36 @@ class DeviceTypePanel(panels.ObjectAttributesPanel):
rear_image = attrs.ImageAttr('rear_image') rear_image = attrs.ImageAttr('rear_image')
class ModulePanel(panels.ObjectAttributesPanel):
device = attrs.RelatedObjectAttr('device', linkify=True)
device_type = attrs.RelatedObjectAttr('device.device_type', linkify=True, grouped_by='manufacturer')
module_bay = attrs.NestedObjectAttr('module_bay')
status = attrs.ChoiceAttr('status')
description = attrs.TextAttr('description')
serial = attrs.TextAttr('serial', label=_('Serial number'), style='font-monospace', copy_button=True)
asset_tag = attrs.TextAttr('asset_tag', style='font-monospace', copy_button=True)
class ModuleTypeProfilePanel(panels.ObjectAttributesPanel): class ModuleTypeProfilePanel(panels.ObjectAttributesPanel):
name = attrs.TextAttr('name') name = attrs.TextAttr('name')
description = attrs.TextAttr('description') description = attrs.TextAttr('description')
class ModuleTypePanel(panels.ObjectAttributesPanel):
profile = attrs.RelatedObjectAttr('profile', linkify=True)
manufacturer = attrs.RelatedObjectAttr('manufacturer', linkify=True)
model = attrs.TextAttr('name')
part_number = attrs.TextAttr('part_number')
description = attrs.TextAttr('description')
airflow = attrs.ChoiceAttr('airflow')
weight = attrs.NumericAttr('weight', unit_accessor='get_weight_unit_display')
class PlatformPanel(panels.NestedGroupObjectPanel):
manufacturer = attrs.RelatedObjectAttr('manufacturer', linkify=True)
config_template = attrs.RelatedObjectAttr('config_template', linkify=True)
class VirtualChassisMembersPanel(panels.ObjectPanel): class VirtualChassisMembersPanel(panels.ObjectPanel):
""" """
A panel which lists all members of a virtual chassis. A panel which lists all members of a virtual chassis.

View File

@@ -21,8 +21,8 @@ from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable
from netbox.object_actions import * from netbox.object_actions import *
from netbox.ui import actions, layout from netbox.ui import actions, layout
from netbox.ui.panels import ( from netbox.ui.panels import (
CommentsPanel, JSONPanel, NestedGroupObjectPanel, ObjectsTablePanel, OrganizationalObjectPanel, RelatedObjectsPanel, CommentsPanel, JSONPanel, NestedGroupObjectPanel, ObjectsTablePanel, OrganizationalObjectPanel, Panel,
TemplatePanel, RelatedObjectsPanel, TemplatePanel,
) )
from netbox.views import generic from netbox.views import generic
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
@@ -1656,6 +1656,22 @@ class ModuleTypeListView(generic.ObjectListView):
@register_model_view(ModuleType) @register_model_view(ModuleType)
class ModuleTypeView(GetRelatedModelsMixin, generic.ObjectView): class ModuleTypeView(GetRelatedModelsMixin, generic.ObjectView):
queryset = ModuleType.objects.all() queryset = ModuleType.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.ModuleTypePanel(),
TagsPanel(),
CommentsPanel(),
],
right_panels=[
Panel(
title=_('Attributes'),
template_name='dcim/panels/module_type_attributes.html',
),
RelatedObjectsPanel(),
CustomFieldsPanel(),
ImageAttachmentsPanel(),
],
)
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
return { return {
@@ -2294,6 +2310,27 @@ class DeviceRoleListView(generic.ObjectListView):
@register_model_view(DeviceRole) @register_model_view(DeviceRole)
class DeviceRoleView(GetRelatedModelsMixin, generic.ObjectView): class DeviceRoleView(GetRelatedModelsMixin, generic.ObjectView):
queryset = DeviceRole.objects.all() queryset = DeviceRole.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.DeviceRolePanel(),
TagsPanel(),
],
right_panels=[
RelatedObjectsPanel(),
CustomFieldsPanel(),
CommentsPanel(),
],
bottom_panels=[
ObjectsTablePanel(
model='dcim.DeviceRole',
title=_('Child Device Roles'),
filters={'parent_id': lambda ctx: ctx['object'].pk},
actions=[
actions.AddObject('dcim.DeviceRole', url_params={'parent': lambda ctx: ctx['object'].pk}),
],
),
]
)
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
return { return {
@@ -2373,6 +2410,27 @@ class PlatformListView(generic.ObjectListView):
@register_model_view(Platform) @register_model_view(Platform)
class PlatformView(GetRelatedModelsMixin, generic.ObjectView): class PlatformView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Platform.objects.all() queryset = Platform.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.PlatformPanel(),
TagsPanel(),
],
right_panels=[
RelatedObjectsPanel(),
CustomFieldsPanel(),
CommentsPanel(),
],
bottom_panels=[
ObjectsTablePanel(
model='dcim.Platform',
title=_('Child Platforms'),
filters={'parent_id': lambda ctx: ctx['object'].pk},
actions=[
actions.AddObject('dcim.Platform', url_params={'parent': lambda ctx: ctx['object'].pk}),
],
),
]
)
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
return { return {
@@ -2763,6 +2821,21 @@ class ModuleListView(generic.ObjectListView):
@register_model_view(Module) @register_model_view(Module)
class ModuleView(GetRelatedModelsMixin, generic.ObjectView): class ModuleView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Module.objects.all() queryset = Module.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.ModulePanel(),
TagsPanel(),
CommentsPanel(),
],
right_panels=[
Panel(
title=_('Module Type'),
template_name='dcim/panels/module_type.html',
),
RelatedObjectsPanel(),
CustomFieldsPanel(),
],
)
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
return { return {

View File

@@ -51,7 +51,14 @@ class ImageAttachmentsPanel(panels.ObjectsTablePanel):
] ]
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__('extras.imageattachment', **kwargs) super().__init__(
'extras.imageattachment',
filters={
'object_type_id': lambda ctx: ContentType.objects.get_for_model(ctx['object']).pk,
'object_id': lambda ctx: ctx['object'].pk,
},
**kwargs,
)
class TagsPanel(panels.ObjectPanel): class TagsPanel(panels.ObjectPanel):

View File

@@ -0,0 +1,19 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('dcim', '0225_gfk_indexes'),
('extras', '0134_owner'),
('ipam', '0085_add_comments_to_organizationalmodel'),
('tenancy', '0022_add_comments_to_organizationalmodel'),
('users', '0015_owner'),
]
operations = [
migrations.AddIndex(
model_name='prefix',
index=models.Index(fields=['scope_type', 'scope_id'], name='ipam_prefix_scope_t_fe84a6_idx'),
),
]

View File

@@ -282,13 +282,10 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary
ordering = (F('vrf').asc(nulls_first=True), 'prefix', 'pk') # (vrf, prefix) may be non-unique ordering = (F('vrf').asc(nulls_first=True), 'prefix', 'pk') # (vrf, prefix) may be non-unique
verbose_name = _('prefix') verbose_name = _('prefix')
verbose_name_plural = _('prefixes') verbose_name_plural = _('prefixes')
indexes = [ indexes = (
GistIndex( models.Index(fields=('scope_type', 'scope_id')),
fields=['prefix'], GistIndex(fields=['prefix'], name='ipam_prefix_gist_idx', opclasses=['inet_ops']),
name='ipam_prefix_gist_idx', )
opclasses=['inet_ops'],
),
]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)

View File

@@ -43,15 +43,18 @@ class Panel:
Parameters: Parameters:
title (str): The human-friendly title of the panel title (str): The human-friendly title of the panel
actions (list): An iterable of PanelActions to include in the panel header actions (list): An iterable of PanelActions to include in the panel header
template_name (str): Overrides the default template name, if defined
""" """
template_name = None template_name = None
title = None title = None
actions = None actions = None
def __init__(self, title=None, actions=None): def __init__(self, title=None, actions=None, template_name=None):
if title is not None: if title is not None:
self.title = title self.title = title
self.actions = actions or self.actions or [] self.actions = actions or self.actions or []
if template_name is not None:
self.template_name = template_name
def get_context(self, context): def get_context(self, context):
""" """
@@ -316,9 +319,8 @@ class TemplatePanel(Panel):
Parameters: Parameters:
template_name (str): The name of the template to render template_name (str): The name of the template to render
""" """
def __init__(self, template_name, **kwargs): def __init__(self, template_name):
super().__init__(**kwargs) super().__init__(template_name=template_name)
self.template_name = template_name
def render(self, context): def render(self, context):
# Pass the entire context to the template # Pass the entire context to the template

View File

@@ -15,67 +15,3 @@
</a> </a>
{% endif %} {% endif %}
{% endblock extra_controls %} {% endblock extra_controls %}
{% block content %}
<div class="row mb-3">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Device Role" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Parent" %}</th>
<td>{{ object.parent|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Color" %}</th>
<td>
<span class="badge color-label" style="background-color: #{{ object.color }}">&nbsp;</span>
</td>
</tr>
<tr>
<th scope="row">{% trans "VM Role" %}</th>
<td>{% checkmark object.vm_role %}</td>
</tr>
<tr>
<th scope="row">{% trans "Config Template" %}</th>
<td>{{ object.config_template|linkify|placeholder }}</td>
</tr>
</table>
</div>
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
{% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row mb-3">
<div class="col col-md-12">
<div class="card">
<h2 class="card-header">
{% trans "Child Device Roles" %}
{% if perms.dcim.add_devicerole %}
<div class="card-actions">
<a href="{% url 'dcim:devicerole_add' %}?parent={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-ghost-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add a Device Role" %}
</a>
</div>
{% endif %}
</h2>
{% htmx_table 'dcim:devicerole_list' parent_id=object.pk %}
</div>
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -47,7 +47,7 @@
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block content %} {% block contentx %}
<div class="row"> <div class="row">
<div class="col col-12 col-md-6"> <div class="col col-12 col-md-6">
<div class="card"> <div class="card">

View File

@@ -1,7 +1,4 @@
{% extends 'generic/object.html' %} {% extends 'generic/object.html' %}
{% load buttons %}
{% load helpers %}
{% load plugins %}
{% load i18n %} {% load i18n %}
{% block title %}{{ object.manufacturer }} {{ object.model }}{% endblock %} {% block title %}{{ object.manufacturer }} {{ object.model }}{% endblock %}
@@ -14,92 +11,5 @@
{% endblock %} {% endblock %}
{% block extra_controls %} {% block extra_controls %}
{% include 'dcim/inc/moduletype_buttons.html' %} {% include 'dcim/inc/moduletype_buttons.html' %}
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Module Type" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Profile" %}</th>
<td>{{ object.profile|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Manufacturer" %}</th>
<td>{{ object.manufacturer|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Model Name" %}</th>
<td>{{ object.model }}</td>
</tr>
<tr>
<th scope="row">{% trans "Part Number" %}</th>
<td>{{ object.part_number|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Airflow" %}</th>
<td>{{ object.get_airflow_display|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Weight" %}</th>
<td>
{% if object.weight %}
{{ object.weight|floatformat }} {{ object.get_weight_unit_display }}
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
</table>
</div>
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Attributes" %}</h2>
{% if not object.profile %}
<div class="card-body text-muted">
{% trans "No profile assigned" %}
</div>
{% elif object.attributes %}
<table class="table table-hover attr-table">
{% for k, v in object.attributes.items %}
<tr>
<th scope="row">{{ k }}</th>
<td>
{% if v is True or v is False %}
{% checkmark v %}
{% else %}
{{ v|placeholder }}
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% else %}
<div class="card-body text-muted">
{% trans "None" %}
</div>
{% endif %}
</div>
{% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/image_attachments.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,27 @@
{% extends "ui/panels/_base.html" %}
{% load helpers i18n %}
{% block panel_content %}
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Manufacturer" %}</th>
<td>{{ object.module_type.manufacturer|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Model" %}</th>
<td>{{ object.module_type|linkify }}</td>
</tr>
{% for k, v in object.module_type.attributes.items %}
<tr>
<th scope="row">{{ k }}</th>
<td>
{% if v is True or v is False %}
{% checkmark v %}
{% else %}
{{ v|placeholder }}
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% endblock panel_content %}

View File

@@ -0,0 +1,29 @@
{% extends "ui/panels/_base.html" %}
{% load helpers i18n %}
{% block panel_content %}
{% if not object.profile %}
<div class="card-body text-muted">
{% trans "No profile assigned" %}
</div>
{% elif object.attributes %}
<table class="table table-hover attr-table">
{% for k, v in object.attributes.items %}
<tr>
<th scope="row">{{ k }}</th>
<td>
{% if v is True or v is False %}
{% checkmark v %}
{% else %}
{{ v|placeholder }}
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% else %}
<div class="card-body text-muted">
{% trans "None" %}
</div>
{% endif %}
{% endblock panel_content %}

View File

@@ -18,61 +18,3 @@
</a> </a>
{% endif %} {% endif %}
{% endblock extra_controls %} {% endblock extra_controls %}
{% block content %}
<div class="row mb-3">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Platform" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Parent" %}</th>
<td>{{ object.parent|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Manufacturer" %}</th>
<td>{{ object.manufacturer|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Config Template" %}</th>
<td>{{ object.config_template|linkify|placeholder }}</td>
</tr>
</table>
</div>
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
{% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row mb-3">
<div class="col col-md-12">
<div class="card">
<h2 class="card-header">
{% trans "Child Platforms" %}
{% if perms.dcim.add_platform %}
<div class="card-actions">
<a href="{% url 'dcim:platform_add' %}?parent={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-ghost-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add a Platform" %}
</a>
</div>
{% endif %}
</h2>
{% htmx_table 'dcim:platform_list' parent_id=object.pk %}
</div>
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -15,6 +15,7 @@ from ipam.models import VLAN
from netbox.api.fields import ChoiceField, SerializedPKRelatedField from netbox.api.fields import ChoiceField, SerializedPKRelatedField
from netbox.api.serializers import NetBoxModelSerializer, PrimaryModelSerializer from netbox.api.serializers import NetBoxModelSerializer, PrimaryModelSerializer
from tenancy.api.serializers_.tenants import TenantSerializer from tenancy.api.serializers_.tenants import TenantSerializer
from users.api.serializers_.mixins import OwnerMixin
from virtualization.choices import * from virtualization.choices import *
from virtualization.models import VirtualDisk, VirtualMachine, VMInterface from virtualization.models import VirtualDisk, VirtualMachine, VMInterface
from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer
@@ -65,8 +66,8 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer):
fields = [ fields = [
'id', 'url', 'display_url', 'display', 'name', 'status', 'start_on_boot', 'site', 'cluster', 'device', 'id', 'url', 'display_url', 'display', 'name', 'status', 'start_on_boot', 'site', 'cluster', 'device',
'serial', 'role', 'tenant', 'platform', 'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'serial', 'role', 'tenant', 'platform', 'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory',
'disk', 'description', 'comments', 'config_template', 'local_context_data', 'tags', 'custom_fields', 'disk', 'description', 'owner', 'comments', 'config_template', 'local_context_data', 'tags',
'config_context', 'created', 'last_updated', 'interface_count', 'virtual_disk_count', 'custom_fields', 'config_context', 'created', 'last_updated', 'interface_count', 'virtual_disk_count',
] ]
@extend_schema_field(serializers.JSONField(allow_null=True)) @extend_schema_field(serializers.JSONField(allow_null=True))
@@ -78,7 +79,7 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer):
# VM interfaces # VM interfaces
# #
class VMInterfaceSerializer(NetBoxModelSerializer): class VMInterfaceSerializer(OwnerMixin, NetBoxModelSerializer):
virtual_machine = VirtualMachineSerializer(nested=True) virtual_machine = VirtualMachineSerializer(nested=True)
parent = NestedVMInterfaceSerializer(required=False, allow_null=True) parent = NestedVMInterfaceSerializer(required=False, allow_null=True)
bridge = NestedVMInterfaceSerializer(required=False, allow_null=True) bridge = NestedVMInterfaceSerializer(required=False, allow_null=True)
@@ -107,7 +108,7 @@ class VMInterfaceSerializer(NetBoxModelSerializer):
fields = [ fields = [
'id', 'url', 'display_url', 'display', 'virtual_machine', 'name', 'enabled', 'parent', 'bridge', 'mtu', 'id', 'url', 'display_url', 'display', 'virtual_machine', 'name', 'enabled', 'parent', 'bridge', 'mtu',
'mac_address', 'primary_mac_address', 'mac_addresses', 'description', 'mode', 'untagged_vlan', 'mac_address', 'primary_mac_address', 'mac_addresses', 'description', 'mode', 'untagged_vlan',
'tagged_vlans', 'qinq_svlan', 'vlan_translation_policy', 'vrf', 'l2vpn_termination', 'tags', 'tagged_vlans', 'qinq_svlan', 'vlan_translation_policy', 'vrf', 'l2vpn_termination', 'owner', 'tags',
'custom_fields', 'created', 'last_updated', 'count_ipaddresses', 'count_fhrp_groups', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses', 'count_fhrp_groups',
] ]
brief_fields = ('id', 'url', 'display', 'virtual_machine', 'name', 'description') brief_fields = ('id', 'url', 'display', 'virtual_machine', 'name', 'description')
@@ -147,13 +148,13 @@ class VMInterfaceSerializer(NetBoxModelSerializer):
# Virtual Disk # Virtual Disk
# #
class VirtualDiskSerializer(NetBoxModelSerializer): class VirtualDiskSerializer(OwnerMixin, NetBoxModelSerializer):
virtual_machine = VirtualMachineSerializer(nested=True) virtual_machine = VirtualMachineSerializer(nested=True)
class Meta: class Meta:
model = VirtualDisk model = VirtualDisk
fields = [ fields = [
'id', 'url', 'display_url', 'display', 'virtual_machine', 'name', 'description', 'size', 'tags', 'id', 'url', 'display_url', 'display', 'virtual_machine', 'name', 'description', 'size', 'owner', 'tags',
'custom_fields', 'created', 'last_updated', 'custom_fields', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'virtual_machine', 'name', 'description', 'size') brief_fields = ('id', 'url', 'display', 'virtual_machine', 'name', 'description', 'size')

View File

@@ -0,0 +1,19 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('dcim', '0225_gfk_indexes'),
('extras', '0134_owner'),
('tenancy', '0022_add_comments_to_organizationalmodel'),
('users', '0015_owner'),
('virtualization', '0051_add_comments_to_organizationalmodel'),
]
operations = [
migrations.AddIndex(
model_name='cluster',
index=models.Index(fields=['scope_type', 'scope_id'], name='virtualizat_scope_t_fb3b6e_idx'),
),
]

View File

@@ -107,6 +107,9 @@ class Cluster(ContactsMixin, CachedScopeMixin, PrimaryModel):
name='%(app_label)s_%(class)s_unique__site_name' name='%(app_label)s_%(class)s_unique__site_name'
), ),
) )
indexes = (
models.Index(fields=('scope_type', 'scope_id')),
)
verbose_name = _('cluster') verbose_name = _('cluster')
verbose_name_plural = _('clusters') verbose_name_plural = _('clusters')

View File

@@ -0,0 +1,20 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('dcim', '0225_gfk_indexes'),
('extras', '0134_owner'),
('ipam', '0086_gfk_indexes'),
('tenancy', '0022_add_comments_to_organizationalmodel'),
('users', '0015_owner'),
('wireless', '0016_owner'),
]
operations = [
migrations.AddIndex(
model_name='wirelesslan',
index=models.Index(fields=['scope_type', 'scope_id'], name='wireless_wi_scope_t_6740a3_idx'),
),
]

View File

@@ -113,6 +113,9 @@ class WirelessLAN(WirelessAuthenticationBase, CachedScopeMixin, PrimaryModel):
class Meta: class Meta:
ordering = ('ssid', 'pk') ordering = ('ssid', 'pk')
indexes = (
models.Index(fields=('scope_type', 'scope_id')),
)
verbose_name = _('wireless LAN') verbose_name = _('wireless LAN')
verbose_name_plural = _('wireless LANs') verbose_name_plural = _('wireless LANs')