mirror of
https://github.com/netbox-community/netbox.git
synced 2026-04-04 08:27:17 +02:00
Compare commits
10 Commits
21455-sql-
...
feature
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02f9ca8f01 | ||
|
|
5ad4e95207 | ||
|
|
a06a300913 | ||
|
|
6c08941542 | ||
|
|
be1a29d7ee | ||
|
|
f06f8f3f1d | ||
|
|
a45ec6620a | ||
|
|
bd35afe320 | ||
|
|
364868a207 | ||
|
|
d4569df305 |
@@ -32,6 +32,26 @@ For example, `{vc_position:1}` will render as `1` when no Virtual Chassis positi
|
||||
|
||||
Automatic renaming is supported for all modular component types (those listed above).
|
||||
|
||||
### Position Inheritance for Nested Modules
|
||||
|
||||
When using nested module bays (modules installed inside other modules), the `{module}` placeholder
|
||||
can also be used in the **position** field of module bay templates to inherit the parent bay's
|
||||
position. This allows a single module type to produce correctly named components at any nesting
|
||||
depth, with a user-controlled separator.
|
||||
|
||||
For example, a line card module type might define sub-bay positions as `{module}/1`, `{module}/2`,
|
||||
etc. When the line card is installed in a device bay with position `3`, these sub-bay positions
|
||||
resolve to `3/1`, `3/2`, etc. An SFP module type with interface template `SFP {module}` installed
|
||||
in sub-bay `3/2` then produces interface `SFP 3/2`.
|
||||
|
||||
The separator between levels is defined by the user in the position field template itself. Using
|
||||
`{module}-1` produces positions like `3-1`, while `{module}.1` produces `3.1`. This provides
|
||||
full flexibility without requiring a global separator configuration.
|
||||
|
||||
!!! note
|
||||
If the position field does not contain `{module}`, no inheritance occurs and behavior is
|
||||
unchanged from previous versions.
|
||||
|
||||
## Fields
|
||||
|
||||
### Manufacturer
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('circuits', '0056_gfk_indexes'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('dcim', '0230_interface_rf_channel_frequency_precision'),
|
||||
('extras', '0136_customfield_validation_schema'),
|
||||
('tenancy', '0023_add_mptt_tree_indexes'),
|
||||
('users', '0015_owner'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='circuit',
|
||||
index=models.Index(fields=['provider', 'provider_account', 'cid'], name='circuits_ci_provide_a0c42c_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='circuitgroupassignment',
|
||||
index=models.Index(
|
||||
fields=['group', 'member_type', 'member_id', 'priority', 'id'], name='circuits_ci_group_i_2f8327_idx'
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='virtualcircuit',
|
||||
index=models.Index(
|
||||
fields=['provider_network', 'provider_account', 'cid'], name='circuits_vi_provide_989efa_idx'
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='virtualcircuittermination',
|
||||
index=models.Index(fields=['virtual_circuit', 'role', 'id'], name='circuits_vi_virtual_4b5c0c_idx'),
|
||||
),
|
||||
]
|
||||
@@ -144,9 +144,6 @@ class Circuit(ContactsMixin, ImageAttachmentsMixin, DistanceMixin, PrimaryModel)
|
||||
name='%(app_label)s_%(class)s_unique_provideraccount_cid'
|
||||
),
|
||||
)
|
||||
indexes = (
|
||||
models.Index(fields=('provider', 'provider_account', 'cid')), # Default ordering
|
||||
)
|
||||
verbose_name = _('circuit')
|
||||
verbose_name_plural = _('circuits')
|
||||
|
||||
@@ -224,9 +221,6 @@ class CircuitGroupAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin,
|
||||
name='%(app_label)s_%(class)s_unique_member_group'
|
||||
),
|
||||
)
|
||||
indexes = (
|
||||
models.Index(fields=('group', 'member_type', 'member_id', 'priority', 'id')), # Default ordering
|
||||
)
|
||||
verbose_name = _('Circuit group assignment')
|
||||
verbose_name_plural = _('Circuit group assignments')
|
||||
|
||||
|
||||
@@ -97,9 +97,6 @@ class VirtualCircuit(ContactsMixin, PrimaryModel):
|
||||
name='%(app_label)s_%(class)s_unique_provideraccount_cid'
|
||||
),
|
||||
)
|
||||
indexes = (
|
||||
models.Index(fields=('provider_network', 'provider_account', 'cid')), # Default ordering
|
||||
)
|
||||
verbose_name = _('virtual circuit')
|
||||
verbose_name_plural = _('virtual circuits')
|
||||
|
||||
@@ -153,9 +150,6 @@ class VirtualCircuitTermination(
|
||||
|
||||
class Meta:
|
||||
ordering = ['virtual_circuit', 'role', 'pk']
|
||||
indexes = (
|
||||
models.Index(fields=('virtual_circuit', 'role', 'id')), # Default ordering
|
||||
)
|
||||
verbose_name = _('virtual circuit termination')
|
||||
verbose_name_plural = _('virtual circuit terminations')
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ class ProviderView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
ObjectsTablePanel(
|
||||
model='circuits.ProviderAccount',
|
||||
filters={'provider_id': lambda ctx: ctx['object'].pk},
|
||||
exclude_columns=['provider'],
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'circuits.ProviderAccount', url_params={'provider': lambda ctx: ctx['object'].pk}
|
||||
@@ -62,6 +63,7 @@ class ProviderView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
ObjectsTablePanel(
|
||||
model='circuits.Circuit',
|
||||
filters={'provider_id': lambda ctx: ctx['object'].pk},
|
||||
exclude_columns=['provider'],
|
||||
actions=[
|
||||
actions.AddObject('circuits.Circuit', url_params={'provider': lambda ctx: ctx['object'].pk}),
|
||||
],
|
||||
@@ -161,6 +163,7 @@ class ProviderAccountView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
ObjectsTablePanel(
|
||||
model='circuits.Circuit',
|
||||
filters={'provider_account_id': lambda ctx: ctx['object'].pk},
|
||||
exclude_columns=['provider_account'],
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'circuits.Circuit',
|
||||
@@ -257,6 +260,7 @@ class ProviderNetworkView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
ObjectsTablePanel(
|
||||
model='circuits.VirtualCircuit',
|
||||
filters={'provider_network_id': lambda ctx: ctx['object'].pk},
|
||||
exclude_columns=['provider_network'],
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'circuits.VirtualCircuit', url_params={'provider_network': lambda ctx: ctx['object'].pk}
|
||||
@@ -801,6 +805,7 @@ class VirtualCircuitView(generic.ObjectView):
|
||||
model='circuits.VirtualCircuitTermination',
|
||||
title=_('Terminations'),
|
||||
filters={'virtual_circuit_id': lambda ctx: ctx['object'].pk},
|
||||
exclude_columns=['virtual_circuit'],
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'circuits.VirtualCircuitTermination',
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('core', '0021_job_queue_name'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='configrevision',
|
||||
index=models.Index(fields=['-created'], name='core_config_created_ef9552_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='job',
|
||||
index=models.Index(fields=['-created'], name='core_job_created_efa7cb_idx'),
|
||||
),
|
||||
]
|
||||
@@ -37,9 +37,6 @@ class ConfigRevision(models.Model):
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created']
|
||||
indexes = (
|
||||
models.Index(fields=('-created',)), # Default ordering
|
||||
)
|
||||
verbose_name = _('config revision')
|
||||
verbose_name_plural = _('config revisions')
|
||||
constraints = [
|
||||
|
||||
@@ -133,7 +133,6 @@ class Job(models.Model):
|
||||
class Meta:
|
||||
ordering = ['-created']
|
||||
indexes = (
|
||||
models.Index(fields=('-created',)), # Default ordering
|
||||
models.Index(fields=('object_type', 'object_id')),
|
||||
)
|
||||
verbose_name = _('job')
|
||||
|
||||
@@ -94,6 +94,7 @@ class DataSourceView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
ObjectsTablePanel(
|
||||
model='core.DataFile',
|
||||
filters={'source_id': lambda ctx: ctx['object'].pk},
|
||||
exclude_columns=['source'],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -8,6 +8,7 @@ from rest_framework import serializers
|
||||
from dcim.choices import *
|
||||
from dcim.constants import MACADDRESS_ASSIGNMENT_MODELS, MODULE_TOKEN
|
||||
from dcim.models import Device, DeviceBay, MACAddress, Module, VirtualDeviceContext
|
||||
from dcim.utils import get_module_bay_positions, resolve_module_placeholder
|
||||
from extras.api.serializers_.configtemplates import ConfigTemplateSerializer
|
||||
from ipam.api.serializers_.ip import IPAddressSerializer
|
||||
from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
|
||||
@@ -207,13 +208,7 @@ class ModuleSerializer(PrimaryModelSerializer):
|
||||
if not all([device, module_type, module_bay]):
|
||||
return data
|
||||
|
||||
# Build module bay tree for MODULE_TOKEN placeholder resolution (outermost to innermost)
|
||||
module_bays = []
|
||||
current_bay = module_bay
|
||||
while current_bay:
|
||||
module_bays.append(current_bay)
|
||||
current_bay = current_bay.module.module_bay if current_bay.module else None
|
||||
module_bays.reverse()
|
||||
positions = get_module_bay_positions(module_bay)
|
||||
|
||||
for templates_attr, component_attr in [
|
||||
('consoleporttemplates', 'consoleports'),
|
||||
@@ -236,17 +231,10 @@ class ModuleSerializer(PrimaryModelSerializer):
|
||||
raise serializers.ValidationError(
|
||||
_("Cannot install module with placeholder values in a module bay with no position defined.")
|
||||
)
|
||||
if template.name.count(MODULE_TOKEN) != len(module_bays):
|
||||
raise serializers.ValidationError(
|
||||
_(
|
||||
"Cannot install module with placeholder values in a module bay tree {level} in tree "
|
||||
"but {tokens} placeholders given."
|
||||
).format(
|
||||
level=len(module_bays), tokens=template.name.count(MODULE_TOKEN)
|
||||
)
|
||||
)
|
||||
for bay in module_bays:
|
||||
resolved_name = resolved_name.replace(MODULE_TOKEN, bay.position, 1)
|
||||
try:
|
||||
resolved_name = resolve_module_placeholder(template.name, positions)
|
||||
except ValueError as e:
|
||||
raise serializers.ValidationError(str(e))
|
||||
|
||||
existing_item = installed_components.get(resolved_name)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.utils import get_module_bay_positions, resolve_module_placeholder
|
||||
from utilities.forms import get_field_value
|
||||
|
||||
__all__ = (
|
||||
@@ -70,18 +71,6 @@ class InterfaceCommonForm(forms.Form):
|
||||
|
||||
class ModuleCommonForm(forms.Form):
|
||||
|
||||
def _get_module_bay_tree(self, module_bay):
|
||||
module_bays = []
|
||||
while module_bay:
|
||||
module_bays.append(module_bay)
|
||||
if module_bay.module:
|
||||
module_bay = module_bay.module.module_bay
|
||||
else:
|
||||
module_bay = None
|
||||
|
||||
module_bays.reverse()
|
||||
return module_bays
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
@@ -100,7 +89,7 @@ class ModuleCommonForm(forms.Form):
|
||||
self.instance._disable_replication = True
|
||||
return
|
||||
|
||||
module_bays = self._get_module_bay_tree(module_bay)
|
||||
positions = get_module_bay_positions(module_bay)
|
||||
|
||||
for templates, component_attribute in [
|
||||
("consoleporttemplates", "consoleports"),
|
||||
@@ -119,25 +108,15 @@ class ModuleCommonForm(forms.Form):
|
||||
# Get the templates for the module type.
|
||||
for template in getattr(module_type, templates).all():
|
||||
resolved_name = template.name
|
||||
# Installing modules with placeholders require that the bay has a position value
|
||||
if MODULE_TOKEN in template.name:
|
||||
if not module_bay.position:
|
||||
raise forms.ValidationError(
|
||||
_("Cannot install module with placeholder values in a module bay with no position defined.")
|
||||
)
|
||||
|
||||
if len(module_bays) != template.name.count(MODULE_TOKEN):
|
||||
raise forms.ValidationError(
|
||||
_(
|
||||
"Cannot install module with placeholder values in a module bay tree {level} in tree "
|
||||
"but {tokens} placeholders given."
|
||||
).format(
|
||||
level=len(module_bays), tokens=template.name.count(MODULE_TOKEN)
|
||||
)
|
||||
)
|
||||
|
||||
for module_bay in module_bays:
|
||||
resolved_name = resolved_name.replace(MODULE_TOKEN, module_bay.position, 1)
|
||||
try:
|
||||
resolved_name = resolve_module_placeholder(template.name, positions)
|
||||
except ValueError as e:
|
||||
raise forms.ValidationError(str(e))
|
||||
|
||||
existing_item = installed_components.get(resolved_name)
|
||||
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('dcim', '0230_interface_rf_channel_frequency_precision'),
|
||||
('extras', '0136_customfield_validation_schema'),
|
||||
('ipam', '0088_rename_vlangroup_total_vlan_ids'),
|
||||
('tenancy', '0023_add_mptt_tree_indexes'),
|
||||
('users', '0015_owner'),
|
||||
('virtualization', '0054_virtualmachinetype'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='consoleporttemplate',
|
||||
index=models.Index(fields=['device_type', 'module_type', 'name'], name='dcim_consol_device__101ed5_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='consoleserverporttemplate',
|
||||
index=models.Index(fields=['device_type', 'module_type', 'name'], name='dcim_consol_device__a901e6_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='device',
|
||||
index=models.Index(fields=['name', 'id'], name='dcim_device_name_c27913_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='frontporttemplate',
|
||||
index=models.Index(fields=['device_type', 'module_type', 'name'], name='dcim_frontp_device__ec2ffb_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='interfacetemplate',
|
||||
index=models.Index(fields=['device_type', 'module_type', 'name'], name='dcim_interf_device__601012_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='macaddress',
|
||||
index=models.Index(fields=['mac_address', 'id'], name='dcim_macadd_mac_add_f2662a_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='modulebaytemplate',
|
||||
index=models.Index(fields=['device_type', 'module_type', 'name'], name='dcim_module_device__9eabad_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='moduletype',
|
||||
index=models.Index(fields=['profile', 'manufacturer', 'model'], name='dcim_module_profile_868277_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='poweroutlettemplate',
|
||||
index=models.Index(fields=['device_type', 'module_type', 'name'], name='dcim_powero_device__b83a8f_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='powerporttemplate',
|
||||
index=models.Index(fields=['device_type', 'module_type', 'name'], name='dcim_powerp_device__6c25da_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='rack',
|
||||
index=models.Index(fields=['site', 'location', 'name', 'id'], name='dcim_rack_site_id_715040_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='rackreservation',
|
||||
index=models.Index(fields=['created', 'id'], name='dcim_rackre_created_84f02e_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='rearporttemplate',
|
||||
index=models.Index(fields=['device_type', 'module_type', 'name'], name='dcim_rearpo_device__27f194_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='virtualchassis',
|
||||
index=models.Index(fields=['name'], name='dcim_virtua_name_2dc5cd_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='virtualdevicecontext',
|
||||
index=models.Index(fields=['name'], name='dcim_virtua_name_079d4d_idx'),
|
||||
),
|
||||
]
|
||||
@@ -9,6 +9,7 @@ from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models.base import PortMappingBase
|
||||
from dcim.models.mixins import InterfaceValidationMixin
|
||||
from dcim.utils import get_module_bay_positions, resolve_module_placeholder
|
||||
from netbox.models import ChangeLoggedModel
|
||||
from utilities.fields import ColorField, NaturalOrderingField
|
||||
from utilities.mptt import TreeManager
|
||||
@@ -143,9 +144,6 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
|
||||
name='%(app_label)s_%(class)s_unique_module_type_name'
|
||||
),
|
||||
)
|
||||
indexes = (
|
||||
models.Index(fields=('device_type', 'module_type', 'name')), # Default ordering
|
||||
)
|
||||
|
||||
def to_objectchange(self, action):
|
||||
objectchange = super().to_objectchange(action)
|
||||
@@ -188,33 +186,27 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
|
||||
|
||||
return VC_POSITION_RE.sub(replacer, value)
|
||||
|
||||
def _get_module_tree(self, module):
|
||||
modules = []
|
||||
while module:
|
||||
modules.append(module)
|
||||
if module.module_bay:
|
||||
module = module.module_bay.module
|
||||
else:
|
||||
module = None
|
||||
|
||||
modules.reverse()
|
||||
return modules
|
||||
|
||||
def _resolve_module_placeholder(self, value, module=None, device=None):
|
||||
if MODULE_TOKEN in value and module:
|
||||
modules = self._get_module_tree(module)
|
||||
for m in modules:
|
||||
value = value.replace(MODULE_TOKEN, m.module_bay.position, 1)
|
||||
if VC_POSITION_RE.search(value) is not None:
|
||||
def _resolve_all_placeholders(self, value, module=None, device=None):
|
||||
has_module = MODULE_TOKEN in value
|
||||
has_vc = VC_POSITION_RE.search(value) is not None
|
||||
if not has_module and not has_vc:
|
||||
return value
|
||||
if has_module and module:
|
||||
positions = get_module_bay_positions(module.module_bay)
|
||||
value = resolve_module_placeholder(value, positions)
|
||||
if has_vc:
|
||||
resolved_device = (module.device if module else None) or device
|
||||
value = self._resolve_vc_position(value, resolved_device)
|
||||
return value
|
||||
|
||||
def resolve_name(self, module=None, device=None):
|
||||
return self._resolve_module_placeholder(self.name, module, device)
|
||||
return self._resolve_all_placeholders(self.name, module, device)
|
||||
|
||||
def resolve_label(self, module=None, device=None):
|
||||
return self._resolve_module_placeholder(self.label, module, device)
|
||||
return self._resolve_all_placeholders(self.label, module, device)
|
||||
|
||||
def resolve_position(self, module=None, device=None):
|
||||
return self._resolve_all_placeholders(self.position, module, device)
|
||||
|
||||
|
||||
class ConsolePortTemplate(ModularComponentTemplateModel):
|
||||
@@ -748,14 +740,11 @@ class ModuleBayTemplate(ModularComponentTemplateModel):
|
||||
verbose_name = _('module bay template')
|
||||
verbose_name_plural = _('module bay templates')
|
||||
|
||||
def resolve_position(self, module):
|
||||
return self._resolve_module_placeholder(self.position, module)
|
||||
|
||||
def instantiate(self, **kwargs):
|
||||
return self.component_model(
|
||||
name=self.resolve_name(kwargs.get('module'), kwargs.get('device')),
|
||||
label=self.resolve_label(kwargs.get('module'), kwargs.get('device')),
|
||||
position=self.resolve_position(kwargs.get('module')),
|
||||
position=self.resolve_position(kwargs.get('module'), kwargs.get('device')),
|
||||
enabled=self.enabled,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
@@ -737,9 +737,6 @@ class Device(
|
||||
|
||||
class Meta:
|
||||
ordering = ('name', 'pk') # Name may be null
|
||||
indexes = (
|
||||
models.Index(fields=('name', 'id')), # Default ordering
|
||||
)
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
Lower('name'), 'site', 'tenant',
|
||||
@@ -1187,9 +1184,6 @@ class VirtualChassis(PrimaryModel):
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
indexes = (
|
||||
models.Index(fields=('name',)), # Default ordering
|
||||
)
|
||||
verbose_name = _('virtual chassis')
|
||||
verbose_name_plural = _('virtual chassis')
|
||||
|
||||
@@ -1296,9 +1290,6 @@ class VirtualDeviceContext(PrimaryModel):
|
||||
name='%(app_label)s_%(class)s_device_name'
|
||||
),
|
||||
)
|
||||
indexes = (
|
||||
models.Index(fields=('name',)), # Default ordering
|
||||
)
|
||||
verbose_name = _('virtual device context')
|
||||
verbose_name_plural = _('virtual device contexts')
|
||||
|
||||
@@ -1365,7 +1356,6 @@ class MACAddress(PrimaryModel):
|
||||
class Meta:
|
||||
ordering = ('mac_address', 'pk')
|
||||
indexes = (
|
||||
models.Index(fields=('mac_address', 'id')), # Default ordering
|
||||
models.Index(fields=('assigned_object_type', 'assigned_object_id')),
|
||||
)
|
||||
verbose_name = _('MAC address')
|
||||
|
||||
@@ -113,9 +113,6 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
|
||||
name='%(app_label)s_%(class)s_unique_manufacturer_model'
|
||||
),
|
||||
)
|
||||
indexes = (
|
||||
models.Index(fields=('profile', 'manufacturer', 'model')), # Default ordering
|
||||
)
|
||||
verbose_name = _('module type')
|
||||
verbose_name_plural = _('module types')
|
||||
|
||||
|
||||
@@ -390,9 +390,6 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, TrackingModelMixin, RackBase):
|
||||
name='%(app_label)s_%(class)s_unique_location_facility_id'
|
||||
),
|
||||
)
|
||||
indexes = (
|
||||
models.Index(fields=('site', 'location', 'name', 'id')), # Default ordering
|
||||
)
|
||||
verbose_name = _('rack')
|
||||
verbose_name_plural = _('racks')
|
||||
|
||||
@@ -741,9 +738,6 @@ class RackReservation(PrimaryModel):
|
||||
|
||||
class Meta:
|
||||
ordering = ['created', 'pk']
|
||||
indexes = (
|
||||
models.Index(fields=('created', 'id')), # Default ordering
|
||||
)
|
||||
verbose_name = _('rack reservation')
|
||||
verbose_name_plural = _('rack reservations')
|
||||
|
||||
|
||||
@@ -999,6 +999,273 @@ class ModuleBayTestCase(TestCase):
|
||||
nested_bay = module.modulebays.get(name='Sub-bay 1-1')
|
||||
self.assertEqual(nested_bay.position, '1-1')
|
||||
|
||||
#
|
||||
# Position inheritance tests (#19796)
|
||||
#
|
||||
|
||||
def test_position_inheritance_depth_2(self):
|
||||
"""
|
||||
A module bay with position '{module}/2' under a parent bay with position '1'
|
||||
should resolve to position '1/2'. A single {module} in the interface template
|
||||
should then resolve to '1/2'.
|
||||
"""
|
||||
manufacturer = Manufacturer.objects.first()
|
||||
site = Site.objects.first()
|
||||
device_role = DeviceRole.objects.first()
|
||||
|
||||
device_type = DeviceType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='Chassis for Inheritance',
|
||||
slug='chassis-for-inheritance'
|
||||
)
|
||||
ModuleBayTemplate.objects.create(
|
||||
device_type=device_type,
|
||||
name='Line card slot 1',
|
||||
position='1'
|
||||
)
|
||||
|
||||
line_card_type = ModuleType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='Line Card with Inherited Bays'
|
||||
)
|
||||
ModuleBayTemplate.objects.create(
|
||||
module_type=line_card_type,
|
||||
name='SFP bay {module}/1',
|
||||
position='{module}/1'
|
||||
)
|
||||
ModuleBayTemplate.objects.create(
|
||||
module_type=line_card_type,
|
||||
name='SFP bay {module}/2',
|
||||
position='{module}/2'
|
||||
)
|
||||
|
||||
sfp_type = ModuleType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='SFP with Inherited Path'
|
||||
)
|
||||
InterfaceTemplate.objects.create(
|
||||
module_type=sfp_type,
|
||||
name='SFP {module}',
|
||||
type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS
|
||||
)
|
||||
|
||||
device = Device.objects.create(
|
||||
name='Inheritance Chassis',
|
||||
device_type=device_type,
|
||||
role=device_role,
|
||||
site=site
|
||||
)
|
||||
|
||||
lc_bay = device.modulebays.get(name='Line card slot 1')
|
||||
line_card = Module.objects.create(
|
||||
device=device,
|
||||
module_bay=lc_bay,
|
||||
module_type=line_card_type
|
||||
)
|
||||
|
||||
sfp_bay = line_card.modulebays.get(name='SFP bay 1/2')
|
||||
sfp_module = Module.objects.create(
|
||||
device=device,
|
||||
module_bay=sfp_bay,
|
||||
module_type=sfp_type
|
||||
)
|
||||
|
||||
interface = sfp_module.interfaces.first()
|
||||
self.assertEqual(interface.name, 'SFP 1/2')
|
||||
|
||||
def test_position_inheritance_depth_3(self):
|
||||
"""
|
||||
Position inheritance at depth 3: positions should chain through the tree.
|
||||
"""
|
||||
manufacturer = Manufacturer.objects.first()
|
||||
site = Site.objects.first()
|
||||
device_role = DeviceRole.objects.first()
|
||||
|
||||
device_type = DeviceType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='Deep Chassis',
|
||||
slug='deep-chassis'
|
||||
)
|
||||
ModuleBayTemplate.objects.create(
|
||||
device_type=device_type,
|
||||
name='Slot A',
|
||||
position='A'
|
||||
)
|
||||
|
||||
mid_type = ModuleType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='Mid Module'
|
||||
)
|
||||
ModuleBayTemplate.objects.create(
|
||||
module_type=mid_type,
|
||||
name='Sub {module}-1',
|
||||
position='{module}-1'
|
||||
)
|
||||
|
||||
leaf_type = ModuleType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='Leaf Module'
|
||||
)
|
||||
InterfaceTemplate.objects.create(
|
||||
module_type=leaf_type,
|
||||
name='Port {module}',
|
||||
type=InterfaceTypeChoices.TYPE_1GE_FIXED
|
||||
)
|
||||
|
||||
device = Device.objects.create(
|
||||
name='Deep Device',
|
||||
device_type=device_type,
|
||||
role=device_role,
|
||||
site=site
|
||||
)
|
||||
|
||||
slot_a = device.modulebays.get(name='Slot A')
|
||||
mid_module = Module.objects.create(
|
||||
device=device,
|
||||
module_bay=slot_a,
|
||||
module_type=mid_type
|
||||
)
|
||||
|
||||
sub_bay = mid_module.modulebays.get(name='Sub A-1')
|
||||
self.assertEqual(sub_bay.position, 'A-1')
|
||||
|
||||
leaf_module = Module.objects.create(
|
||||
device=device,
|
||||
module_bay=sub_bay,
|
||||
module_type=leaf_type
|
||||
)
|
||||
|
||||
interface = leaf_module.interfaces.first()
|
||||
self.assertEqual(interface.name, 'Port A-1')
|
||||
|
||||
def test_position_inheritance_custom_separator(self):
|
||||
"""
|
||||
Users control the separator through the position field template.
|
||||
Using '.' instead of '/' should work correctly.
|
||||
"""
|
||||
manufacturer = Manufacturer.objects.first()
|
||||
site = Site.objects.first()
|
||||
device_role = DeviceRole.objects.first()
|
||||
|
||||
device_type = DeviceType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='Dot Separator Chassis',
|
||||
slug='dot-separator-chassis'
|
||||
)
|
||||
ModuleBayTemplate.objects.create(
|
||||
device_type=device_type,
|
||||
name='Bay 1',
|
||||
position='1'
|
||||
)
|
||||
|
||||
card_type = ModuleType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='Card with Dot Separator'
|
||||
)
|
||||
ModuleBayTemplate.objects.create(
|
||||
module_type=card_type,
|
||||
name='Port {module}.1',
|
||||
position='{module}.1'
|
||||
)
|
||||
|
||||
sfp_type = ModuleType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='SFP Dot'
|
||||
)
|
||||
InterfaceTemplate.objects.create(
|
||||
module_type=sfp_type,
|
||||
name='eth{module}',
|
||||
type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS
|
||||
)
|
||||
|
||||
device = Device.objects.create(
|
||||
name='Dot Device',
|
||||
device_type=device_type,
|
||||
role=device_role,
|
||||
site=site
|
||||
)
|
||||
|
||||
bay = device.modulebays.get(name='Bay 1')
|
||||
card = Module.objects.create(
|
||||
device=device,
|
||||
module_bay=bay,
|
||||
module_type=card_type
|
||||
)
|
||||
|
||||
port_bay = card.modulebays.get(name='Port 1.1')
|
||||
sfp = Module.objects.create(
|
||||
device=device,
|
||||
module_bay=port_bay,
|
||||
module_type=sfp_type
|
||||
)
|
||||
|
||||
interface = sfp.interfaces.first()
|
||||
self.assertEqual(interface.name, 'eth1.1')
|
||||
|
||||
def test_multi_token_backwards_compat(self):
|
||||
"""
|
||||
Multi-token {module}/{module} at matching depth should still resolve
|
||||
level-by-level (backwards compatibility).
|
||||
"""
|
||||
manufacturer = Manufacturer.objects.first()
|
||||
site = Site.objects.first()
|
||||
device_role = DeviceRole.objects.first()
|
||||
|
||||
device_type = DeviceType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='Multi Token Chassis',
|
||||
slug='multi-token-chassis'
|
||||
)
|
||||
ModuleBayTemplate.objects.create(
|
||||
device_type=device_type,
|
||||
name='Slot 1',
|
||||
position='1'
|
||||
)
|
||||
|
||||
card_type = ModuleType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='Card for Multi Token'
|
||||
)
|
||||
ModuleBayTemplate.objects.create(
|
||||
module_type=card_type,
|
||||
name='Port 1',
|
||||
position='2'
|
||||
)
|
||||
|
||||
iface_type = ModuleType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='Interface Module Multi Token'
|
||||
)
|
||||
InterfaceTemplate.objects.create(
|
||||
module_type=iface_type,
|
||||
name='Gi{module}/{module}',
|
||||
type=InterfaceTypeChoices.TYPE_1GE_FIXED
|
||||
)
|
||||
|
||||
device = Device.objects.create(
|
||||
name='Multi Token Device',
|
||||
device_type=device_type,
|
||||
role=device_role,
|
||||
site=site
|
||||
)
|
||||
|
||||
slot = device.modulebays.get(name='Slot 1')
|
||||
card = Module.objects.create(
|
||||
device=device,
|
||||
module_bay=slot,
|
||||
module_type=card_type
|
||||
)
|
||||
|
||||
port = card.modulebays.get(name='Port 1')
|
||||
iface_module = Module.objects.create(
|
||||
device=device,
|
||||
module_bay=port,
|
||||
module_type=iface_type
|
||||
)
|
||||
|
||||
interface = iface_module.interfaces.first()
|
||||
self.assertEqual(interface.name, 'Gi1/2')
|
||||
|
||||
@tag('regression') # #20912
|
||||
def test_module_bay_parent_cleared_when_module_removed(self):
|
||||
"""Test that the parent field is properly cleared when a module bay's module assignment is removed"""
|
||||
|
||||
@@ -3,6 +3,59 @@ from collections import defaultdict
|
||||
from django.apps import apps
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import router, transaction
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from dcim.constants import MODULE_TOKEN
|
||||
|
||||
|
||||
def get_module_bay_positions(module_bay):
|
||||
"""
|
||||
Given a module bay, traverse up the module hierarchy and return
|
||||
a list of bay position strings from root to leaf, resolving any
|
||||
{module} tokens in each position using the parent position
|
||||
(position inheritance).
|
||||
"""
|
||||
positions = []
|
||||
while module_bay:
|
||||
pos = module_bay.position or ''
|
||||
if positions and MODULE_TOKEN in pos:
|
||||
pos = pos.replace(MODULE_TOKEN, positions[-1])
|
||||
positions.append(pos)
|
||||
if module_bay.module:
|
||||
module_bay = module_bay.module.module_bay
|
||||
else:
|
||||
module_bay = None
|
||||
positions.reverse()
|
||||
return positions
|
||||
|
||||
|
||||
def resolve_module_placeholder(value, positions):
|
||||
"""
|
||||
Resolve {module} placeholder tokens in a string using the given
|
||||
list of module bay positions (ordered root to leaf).
|
||||
|
||||
A single {module} token resolves to the leaf (immediate parent) bay's position.
|
||||
Multiple tokens must match the tree depth and resolve level-by-level.
|
||||
|
||||
Returns the resolved string.
|
||||
Raises ValueError if token count is greater than 1 and doesn't match tree depth.
|
||||
"""
|
||||
if MODULE_TOKEN not in value:
|
||||
return value
|
||||
|
||||
token_count = value.count(MODULE_TOKEN)
|
||||
if token_count == 1:
|
||||
return value.replace(MODULE_TOKEN, positions[-1])
|
||||
if token_count == len(positions):
|
||||
for pos in positions:
|
||||
value = value.replace(MODULE_TOKEN, pos, 1)
|
||||
return value
|
||||
raise ValueError(
|
||||
_("Cannot install module with placeholder values in a module bay tree "
|
||||
"{level} levels deep but {tokens} placeholders given.").format(
|
||||
level=len(positions), tokens=token_count
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def compile_path_node(ct_id, object_id):
|
||||
|
||||
@@ -258,6 +258,7 @@ class RegionView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
model='dcim.Region',
|
||||
title=_('Child Regions'),
|
||||
filters={'parent_id': lambda ctx: ctx['object'].pk},
|
||||
exclude_columns=['parent'],
|
||||
actions=[
|
||||
actions.AddObject('dcim.Region', url_params={'parent': lambda ctx: ctx['object'].pk}),
|
||||
],
|
||||
@@ -390,6 +391,7 @@ class SiteGroupView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
model='dcim.SiteGroup',
|
||||
title=_('Child Groups'),
|
||||
filters={'parent_id': lambda ctx: ctx['object'].pk},
|
||||
exclude_columns=['parent'],
|
||||
actions=[
|
||||
actions.AddObject('dcim.SiteGroup', url_params={'parent': lambda ctx: ctx['object'].pk}),
|
||||
],
|
||||
@@ -540,6 +542,7 @@ class SiteView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
ObjectsTablePanel(
|
||||
model='dcim.Location',
|
||||
filters={'site_id': lambda ctx: ctx['object'].pk},
|
||||
exclude_columns=['site'],
|
||||
actions=[
|
||||
actions.AddObject('dcim.Location', url_params={'site': lambda ctx: ctx['object'].pk}),
|
||||
],
|
||||
@@ -552,6 +555,7 @@ class SiteView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
'rack_id': settings.FILTERS_NULL_CHOICE_VALUE,
|
||||
'parent_bay_id': settings.FILTERS_NULL_CHOICE_VALUE,
|
||||
},
|
||||
exclude_columns=['site'],
|
||||
actions=[
|
||||
actions.AddObject('dcim.Device', url_params={'site': lambda ctx: ctx['object'].pk}),
|
||||
],
|
||||
@@ -674,6 +678,7 @@ class LocationView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
model='dcim.Location',
|
||||
title=_('Child Locations'),
|
||||
filters={'parent_id': lambda ctx: ctx['object'].pk},
|
||||
exclude_columns=['parent'],
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'dcim.Location',
|
||||
@@ -692,6 +697,7 @@ class LocationView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
'rack_id': settings.FILTERS_NULL_CHOICE_VALUE,
|
||||
'parent_bay_id': settings.FILTERS_NULL_CHOICE_VALUE,
|
||||
},
|
||||
exclude_columns=['location'],
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'dcim.Device',
|
||||
@@ -1686,6 +1692,7 @@ class ModuleTypeProfileView(generic.ObjectView):
|
||||
filters={
|
||||
'profile_id': lambda ctx: ctx['object'].pk,
|
||||
},
|
||||
exclude_columns=['profile'],
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'dcim.ModuleType',
|
||||
@@ -2427,6 +2434,7 @@ class DeviceRoleView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
model='dcim.DeviceRole',
|
||||
title=_('Child Device Roles'),
|
||||
filters={'parent_id': lambda ctx: ctx['object'].pk},
|
||||
exclude_columns=['parent'],
|
||||
actions=[
|
||||
actions.AddObject('dcim.DeviceRole', url_params={'parent': lambda ctx: ctx['object'].pk}),
|
||||
],
|
||||
@@ -2527,6 +2535,7 @@ class PlatformView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
model='dcim.Platform',
|
||||
title=_('Child Platforms'),
|
||||
filters={'parent_id': lambda ctx: ctx['object'].pk},
|
||||
exclude_columns=['parent'],
|
||||
actions=[
|
||||
actions.AddObject('dcim.Platform', url_params={'parent': lambda ctx: ctx['object'].pk}),
|
||||
],
|
||||
@@ -2605,6 +2614,7 @@ class DeviceView(generic.ObjectView):
|
||||
ObjectsTablePanel(
|
||||
model='dcim.VirtualDeviceContext',
|
||||
filters={'device_id': lambda ctx: ctx['object'].pk},
|
||||
exclude_columns=['device'],
|
||||
actions=[
|
||||
actions.AddObject('dcim.VirtualDeviceContext', url_params={'device': lambda ctx: ctx['object'].pk}),
|
||||
],
|
||||
@@ -2617,6 +2627,7 @@ class DeviceView(generic.ObjectView):
|
||||
model='ipam.Service',
|
||||
title=_('Application Services'),
|
||||
filters={'device_id': lambda ctx: ctx['object'].pk},
|
||||
exclude_columns=['parent'],
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'ipam.Service',
|
||||
@@ -3376,11 +3387,13 @@ class InterfaceView(generic.ObjectView):
|
||||
model='ipam.IPAddress',
|
||||
filters={'interface_id': lambda ctx: ctx['object'].pk},
|
||||
title=_('IP Addresses'),
|
||||
exclude_columns=['assigned', 'assigned_object', 'assigned_object_parent'],
|
||||
),
|
||||
ObjectsTablePanel(
|
||||
model='dcim.MACAddress',
|
||||
filters={'interface_id': lambda ctx: ctx['object'].pk},
|
||||
title=_('MAC Addresses'),
|
||||
exclude_columns=['assigned_object', 'assigned_object_parent'],
|
||||
),
|
||||
ObjectsTablePanel(
|
||||
model='ipam.VLAN',
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('core', '0022_default_ordering_indexes'),
|
||||
('dcim', '0231_default_ordering_indexes'),
|
||||
('extras', '0136_customfield_validation_schema'),
|
||||
('tenancy', '0023_add_mptt_tree_indexes'),
|
||||
('users', '0015_owner'),
|
||||
('virtualization', '0054_virtualmachinetype'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='bookmark',
|
||||
index=models.Index(fields=['created', 'id'], name='extras_book_created_1cb4a5_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='configcontext',
|
||||
index=models.Index(fields=['weight', 'name'], name='extras_conf_weight_ef9a81_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='configtemplate',
|
||||
index=models.Index(fields=['name'], name='extras_conf_name_e276bf_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='customfield',
|
||||
index=models.Index(fields=['group_name', 'weight', 'name'], name='extras_cust_group_n_40cb93_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='customlink',
|
||||
index=models.Index(fields=['group_name', 'weight', 'name'], name='extras_cust_group_n_5a8be0_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='exporttemplate',
|
||||
index=models.Index(fields=['name'], name='extras_expo_name_55a9af_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='imageattachment',
|
||||
index=models.Index(fields=['name', 'id'], name='extras_imag_name_23cd9f_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='journalentry',
|
||||
index=models.Index(fields=['-created'], name='extras_jour_created_ec0fac_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='notification',
|
||||
index=models.Index(fields=['-created', 'id'], name='extras_noti_created_1d5146_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='savedfilter',
|
||||
index=models.Index(fields=['weight', 'name'], name='extras_save_weight_c070c4_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='script',
|
||||
index=models.Index(fields=['module', 'name'], name='extras_scri_module__8bd99c_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='subscription',
|
||||
index=models.Index(fields=['-created', 'user'], name='extras_subs_created_034618_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='tableconfig',
|
||||
index=models.Index(fields=['weight', 'name'], name='extras_tabl_weight_7c4bb6_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='tag',
|
||||
index=models.Index(fields=['weight', 'name'], name='extras_tag_weight_d99f50_idx'),
|
||||
),
|
||||
]
|
||||
@@ -176,9 +176,6 @@ class ConfigContext(SyncedDataMixin, CloningMixin, CustomLinksMixin, OwnerMixin,
|
||||
|
||||
class Meta:
|
||||
ordering = ['weight', 'name']
|
||||
indexes = (
|
||||
models.Index(fields=('weight', 'name')), # Default ordering
|
||||
)
|
||||
verbose_name = _('config context')
|
||||
verbose_name_plural = _('config contexts')
|
||||
|
||||
@@ -297,9 +294,6 @@ class ConfigTemplate(
|
||||
|
||||
class Meta:
|
||||
ordering = ('name',)
|
||||
indexes = (
|
||||
models.Index(fields=('name',)), # Default ordering
|
||||
)
|
||||
verbose_name = _('config template')
|
||||
verbose_name_plural = _('config templates')
|
||||
|
||||
|
||||
@@ -274,9 +274,6 @@ class CustomField(CloningMixin, ExportTemplatesMixin, OwnerMixin, ChangeLoggedMo
|
||||
|
||||
class Meta:
|
||||
ordering = ['group_name', 'weight', 'name']
|
||||
indexes = (
|
||||
models.Index(fields=('group_name', 'weight', 'name')), # Default ordering
|
||||
)
|
||||
verbose_name = _('custom field')
|
||||
verbose_name_plural = _('custom fields')
|
||||
|
||||
|
||||
@@ -356,9 +356,6 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, OwnerMixin, ChangeLoggedMod
|
||||
|
||||
class Meta:
|
||||
ordering = ['group_name', 'weight', 'name']
|
||||
indexes = (
|
||||
models.Index(fields=('group_name', 'weight', 'name')), # Default ordering
|
||||
)
|
||||
verbose_name = _('custom link')
|
||||
verbose_name_plural = _('custom links')
|
||||
|
||||
@@ -432,9 +429,6 @@ class ExportTemplate(
|
||||
|
||||
class Meta:
|
||||
ordering = ('name',)
|
||||
indexes = (
|
||||
models.Index(fields=('name',)), # Default ordering
|
||||
)
|
||||
verbose_name = _('export template')
|
||||
verbose_name_plural = _('export templates')
|
||||
|
||||
@@ -521,9 +515,6 @@ class SavedFilter(CloningMixin, ExportTemplatesMixin, OwnerMixin, ChangeLoggedMo
|
||||
|
||||
class Meta:
|
||||
ordering = ('weight', 'name')
|
||||
indexes = (
|
||||
models.Index(fields=('weight', 'name')), # Default ordering
|
||||
)
|
||||
verbose_name = _('saved filter')
|
||||
verbose_name_plural = _('saved filters')
|
||||
|
||||
@@ -606,9 +597,6 @@ class TableConfig(CloningMixin, ChangeLoggedModel):
|
||||
|
||||
class Meta:
|
||||
ordering = ('weight', 'name')
|
||||
indexes = (
|
||||
models.Index(fields=('weight', 'name')), # Default ordering
|
||||
)
|
||||
verbose_name = _('table config')
|
||||
verbose_name_plural = _('table configs')
|
||||
|
||||
@@ -712,7 +700,6 @@ class ImageAttachment(ChangeLoggedModel):
|
||||
class Meta:
|
||||
ordering = ('name', 'pk') # name may be non-unique
|
||||
indexes = (
|
||||
models.Index(fields=('name', 'id')), # Default ordering
|
||||
models.Index(fields=('object_type', 'object_id')),
|
||||
)
|
||||
verbose_name = _('image attachment')
|
||||
@@ -823,7 +810,6 @@ class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ExportTemplat
|
||||
class Meta:
|
||||
ordering = ('-created',)
|
||||
indexes = (
|
||||
models.Index(fields=('-created',)), # Default ordering
|
||||
models.Index(fields=('assigned_object_type', 'assigned_object_id')),
|
||||
)
|
||||
verbose_name = _('journal entry')
|
||||
@@ -879,7 +865,6 @@ class Bookmark(models.Model):
|
||||
class Meta:
|
||||
ordering = ('created', 'pk')
|
||||
indexes = (
|
||||
models.Index(fields=('created', 'id')), # Default ordering
|
||||
models.Index(fields=('object_type', 'object_id')),
|
||||
)
|
||||
constraints = (
|
||||
|
||||
@@ -73,7 +73,6 @@ class Notification(models.Model):
|
||||
class Meta:
|
||||
ordering = ('-created', 'pk')
|
||||
indexes = (
|
||||
models.Index(fields=('-created', 'id')), # Default ordering
|
||||
models.Index(fields=('object_type', 'object_id')),
|
||||
)
|
||||
constraints = (
|
||||
@@ -216,7 +215,6 @@ class Subscription(models.Model):
|
||||
class Meta:
|
||||
ordering = ('-created', 'user')
|
||||
indexes = (
|
||||
models.Index(fields=('-created', 'user')), # Default ordering
|
||||
models.Index(fields=('object_type', 'object_id')),
|
||||
)
|
||||
constraints = (
|
||||
|
||||
@@ -62,9 +62,6 @@ class Script(EventRulesMixin, JobsMixin):
|
||||
name='extras_script_unique_name_module'
|
||||
),
|
||||
)
|
||||
indexes = (
|
||||
models.Index(fields=('module', 'name')), # Default ordering
|
||||
)
|
||||
verbose_name = _('script')
|
||||
verbose_name_plural = _('scripts')
|
||||
|
||||
|
||||
@@ -52,9 +52,6 @@ class Tag(CloningMixin, ExportTemplatesMixin, OwnerMixin, ChangeLoggedModel, Tag
|
||||
|
||||
class Meta:
|
||||
ordering = ('weight', 'name')
|
||||
indexes = (
|
||||
models.Index(fields=('weight', 'name')), # Default ordering
|
||||
)
|
||||
verbose_name = _('tag')
|
||||
verbose_name_plural = _('tags')
|
||||
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('dcim', '0231_default_ordering_indexes'),
|
||||
('extras', '0137_default_ordering_indexes'),
|
||||
('ipam', '0088_rename_vlangroup_total_vlan_ids'),
|
||||
('tenancy', '0023_add_mptt_tree_indexes'),
|
||||
('users', '0015_owner'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='aggregate',
|
||||
index=models.Index(fields=['prefix', 'id'], name='ipam_aggreg_prefix_89dd71_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='fhrpgroup',
|
||||
index=models.Index(fields=['protocol', 'group_id', 'id'], name='ipam_fhrpgr_protoco_0445ae_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='fhrpgroupassignment',
|
||||
index=models.Index(fields=['-priority', 'id'], name='ipam_fhrpgr_priorit_b76335_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='ipaddress',
|
||||
index=models.Index(fields=['address', 'id'], name='ipam_ipaddr_address_3ddeea_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='role',
|
||||
index=models.Index(fields=['weight', 'name'], name='ipam_role_weight_01396b_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='service',
|
||||
index=models.Index(fields=['protocol', 'ports', 'id'], name='ipam_servic_protoco_687d13_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='vlan',
|
||||
index=models.Index(fields=['site', 'group', 'vid', 'id'], name='ipam_vlan_site_id_985573_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='vrf',
|
||||
index=models.Index(fields=['name', 'rd', 'id'], name='ipam_vrf_name_ec911d_idx'),
|
||||
),
|
||||
]
|
||||
@@ -59,9 +59,6 @@ class FHRPGroup(PrimaryModel):
|
||||
|
||||
class Meta:
|
||||
ordering = ['protocol', 'group_id', 'pk']
|
||||
indexes = (
|
||||
models.Index(fields=('protocol', 'group_id', 'id')), # Default ordering
|
||||
)
|
||||
verbose_name = _('FHRP group')
|
||||
verbose_name_plural = _('FHRP groups')
|
||||
|
||||
@@ -108,7 +105,6 @@ class FHRPGroupAssignment(ChangeLoggedModel):
|
||||
class Meta:
|
||||
ordering = ('-priority', 'pk')
|
||||
indexes = (
|
||||
models.Index(fields=('-priority', 'id')), # Default ordering
|
||||
models.Index(fields=('interface_type', 'interface_id')),
|
||||
)
|
||||
constraints = (
|
||||
|
||||
@@ -110,9 +110,6 @@ class Aggregate(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel):
|
||||
|
||||
class Meta:
|
||||
ordering = ('prefix', 'pk') # prefix may be non-unique
|
||||
indexes = (
|
||||
models.Index(fields=('prefix', 'id')), # Default ordering
|
||||
)
|
||||
verbose_name = _('aggregate')
|
||||
verbose_name_plural = _('aggregates')
|
||||
|
||||
@@ -203,9 +200,6 @@ class Role(OrganizationalModel):
|
||||
|
||||
class Meta:
|
||||
ordering = ('weight', 'name')
|
||||
indexes = (
|
||||
models.Index(fields=('weight', 'name')), # Default ordering
|
||||
)
|
||||
verbose_name = _('role')
|
||||
verbose_name_plural = _('roles')
|
||||
|
||||
@@ -839,7 +833,6 @@ class IPAddress(ContactsMixin, PrimaryModel):
|
||||
class Meta:
|
||||
ordering = ('address', 'pk') # address may be non-unique
|
||||
indexes = (
|
||||
models.Index(fields=('address', 'id')), # Default ordering
|
||||
models.Index(Cast(Host('address'), output_field=IPAddressField()), name='ipam_ipaddress_host'),
|
||||
models.Index(fields=('assigned_object_type', 'assigned_object_id')),
|
||||
)
|
||||
|
||||
@@ -93,7 +93,6 @@ class Service(ContactsMixin, ServiceBase, PrimaryModel):
|
||||
|
||||
class Meta:
|
||||
indexes = (
|
||||
models.Index(fields=('protocol', 'ports', 'id')), # Default ordering
|
||||
models.Index(fields=('parent_object_type', 'parent_object_id')),
|
||||
)
|
||||
ordering = ('protocol', 'ports', 'pk') # (protocol, port) may be non-unique
|
||||
|
||||
@@ -267,9 +267,6 @@ class VLAN(PrimaryModel):
|
||||
|
||||
class Meta:
|
||||
ordering = ('site', 'group', 'vid', 'pk') # (site, group, vid) may be non-unique
|
||||
indexes = (
|
||||
models.Index(fields=('site', 'group', 'vid', 'id')), # Default ordering
|
||||
)
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('group', 'vid'),
|
||||
|
||||
@@ -58,9 +58,6 @@ class VRF(PrimaryModel):
|
||||
|
||||
class Meta:
|
||||
ordering = ('name', 'rd', 'pk') # (name, rd) may be non-unique
|
||||
indexes = (
|
||||
models.Index(fields=('name', 'rd', 'id')), # Default ordering
|
||||
)
|
||||
verbose_name = _('VRF')
|
||||
verbose_name_plural = _('VRFs')
|
||||
|
||||
|
||||
@@ -1331,6 +1331,7 @@ class VLANTranslationPolicyView(generic.ObjectView):
|
||||
'ipam.vlantranslationrule',
|
||||
filters={'policy_id': lambda ctx: ctx['object'].pk},
|
||||
title=_('VLAN translation rules'),
|
||||
exclude_columns=['policy'],
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'ipam.vlantranslationrule',
|
||||
@@ -1628,6 +1629,7 @@ class VLANView(generic.ObjectView):
|
||||
'ipam.prefix',
|
||||
filters={'vlan_id': lambda ctx: ctx['object'].pk},
|
||||
title=_('Prefixes'),
|
||||
exclude_columns=['vlan'],
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'ipam.prefix',
|
||||
|
||||
@@ -185,6 +185,18 @@ class BaseTable(tables.Table):
|
||||
columns = getattr(self.Meta, 'default_columns', self.Meta.fields)
|
||||
|
||||
self._set_columns(columns)
|
||||
|
||||
# Apply column inclusion/exclusion (overrides user preferences)
|
||||
if columns_param := request.GET.get('include_columns'):
|
||||
for column_name in columns_param.split(','):
|
||||
if column_name in self.columns.names():
|
||||
self.columns.show(column_name)
|
||||
if exclude_columns := request.GET.get('exclude_columns'):
|
||||
exclude_columns = exclude_columns.split(',')
|
||||
for column_name in exclude_columns:
|
||||
if column_name in self.columns.names() and column_name not in self.exempt_columns:
|
||||
self.columns.hide(column_name)
|
||||
|
||||
self._apply_prefetching()
|
||||
if ordering is not None:
|
||||
self.order_by = ordering
|
||||
|
||||
@@ -282,11 +282,13 @@ class ObjectsTablePanel(Panel):
|
||||
model (str): The dotted label of the model to be added (e.g. "dcim.site")
|
||||
filters (dict): A dictionary of arbitrary URL parameters to append to the table's URL. If the value of a key is
|
||||
a callable, it will be passed the current template context.
|
||||
include_columns (list): A list of column names to always display (overrides user preferences)
|
||||
exclude_columns (list): A list of column names to hide from the table (overrides user preferences)
|
||||
"""
|
||||
template_name = 'ui/panels/objects_table.html'
|
||||
title = None
|
||||
|
||||
def __init__(self, model, filters=None, **kwargs):
|
||||
def __init__(self, model, filters=None, include_columns=None, exclude_columns=None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# Resolve the model class from its app.name label
|
||||
@@ -297,6 +299,8 @@ class ObjectsTablePanel(Panel):
|
||||
raise ValueError(f"Invalid model label: {model}")
|
||||
|
||||
self.filters = filters or {}
|
||||
self.include_columns = include_columns or []
|
||||
self.exclude_columns = exclude_columns or []
|
||||
|
||||
# If no title is specified, derive one from the model name
|
||||
if self.title is None:
|
||||
@@ -308,6 +312,10 @@ class ObjectsTablePanel(Panel):
|
||||
}
|
||||
if 'return_url' not in url_params and 'object' in context:
|
||||
url_params['return_url'] = context['object'].get_absolute_url()
|
||||
if self.include_columns:
|
||||
url_params['include_columns'] = ','.join(self.include_columns)
|
||||
if self.exclude_columns:
|
||||
url_params['exclude_columns'] = ','.join(self.exclude_columns)
|
||||
return {
|
||||
**super().get_context(context),
|
||||
'viewname': get_viewname(self.model, 'list'),
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('extras', '0137_default_ordering_indexes'),
|
||||
('tenancy', '0023_add_mptt_tree_indexes'),
|
||||
('users', '0015_owner'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='contact',
|
||||
index=models.Index(fields=['name'], name='tenancy_con_name_c26153_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='contactassignment',
|
||||
index=models.Index(fields=['contact', 'priority', 'role', 'id'], name='tenancy_con_contact_23011f_idx'),
|
||||
),
|
||||
]
|
||||
@@ -90,9 +90,6 @@ class Contact(PrimaryModel):
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
indexes = (
|
||||
models.Index(fields=('name',)), # Default ordering
|
||||
)
|
||||
verbose_name = _('contact')
|
||||
verbose_name_plural = _('contacts')
|
||||
|
||||
@@ -133,7 +130,6 @@ class ContactAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, Chan
|
||||
class Meta:
|
||||
ordering = ('contact', 'priority', 'role', 'pk')
|
||||
indexes = (
|
||||
models.Index(fields=('contact', 'priority', 'role', 'id')), # Default ordering
|
||||
models.Index(fields=('object_type', 'object_id')),
|
||||
)
|
||||
constraints = (
|
||||
|
||||
@@ -57,6 +57,7 @@ class TenantGroupView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
'tenancy.tenantgroup',
|
||||
filters={'parent_id': lambda ctx: ctx['object'].pk},
|
||||
title=_('Child Groups'),
|
||||
exclude_columns=['parent'],
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'tenancy.tenantgroup',
|
||||
@@ -235,6 +236,7 @@ class ContactGroupView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
'tenancy.contactgroup',
|
||||
filters={'parent_id': lambda ctx: ctx['object'].pk},
|
||||
title=_('Child Groups'),
|
||||
exclude_columns=['parent'],
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'tenancy.contactgroup',
|
||||
@@ -414,6 +416,7 @@ class ContactView(generic.ObjectView):
|
||||
'tenancy.contactassignment',
|
||||
filters={'contact_id': lambda ctx: ctx['object'].pk},
|
||||
title=_('Assignments'),
|
||||
exclude_columns=['contact'],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('users', '0015_owner'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='objectpermission',
|
||||
index=models.Index(fields=['name'], name='users_objec_name_ca707b_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='token',
|
||||
index=models.Index(fields=['-created'], name='users_token_created_1467b4_idx'),
|
||||
),
|
||||
]
|
||||
@@ -52,9 +52,6 @@ class ObjectPermission(CloningMixin, models.Model):
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
indexes = (
|
||||
models.Index(fields=('name',)), # Default ordering
|
||||
)
|
||||
verbose_name = _('permission')
|
||||
verbose_name_plural = _('permissions')
|
||||
|
||||
|
||||
@@ -117,9 +117,6 @@ class Token(models.Model):
|
||||
|
||||
class Meta:
|
||||
ordering = ('-created',)
|
||||
indexes = (
|
||||
models.Index(fields=('-created',)), # Default ordering
|
||||
)
|
||||
verbose_name = _('token')
|
||||
verbose_name_plural = _('tokens')
|
||||
constraints = [
|
||||
|
||||
@@ -200,7 +200,10 @@ class GroupView(generic.ObjectView):
|
||||
OrganizationalObjectPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
ObjectsTablePanel('users.User', filters={'group_id': lambda ctx: ctx['object'].pk}),
|
||||
ObjectsTablePanel(
|
||||
'users.User',
|
||||
filters={'group_id': lambda ctx: ctx['object'].pk},
|
||||
),
|
||||
ObjectsTablePanel(
|
||||
'users.ObjectPermission',
|
||||
title=_('Assigned Permissions'),
|
||||
@@ -345,6 +348,7 @@ class OwnerGroupView(generic.ObjectView):
|
||||
'users.Owner',
|
||||
filters={'group_id': lambda ctx: ctx['object'].pk},
|
||||
title=_('Members'),
|
||||
exclude_columns=['group'],
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'users.Owner',
|
||||
@@ -412,8 +416,14 @@ class OwnerView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.OwnerPanel(),
|
||||
ObjectsTablePanel('users.Group', filters={'owner_id': lambda ctx: ctx['object'].pk}),
|
||||
ObjectsTablePanel('users.User', filters={'owner_id': lambda ctx: ctx['object'].pk}),
|
||||
ObjectsTablePanel(
|
||||
'users.Group',
|
||||
filters={'owner_id': lambda ctx: ctx['object'].pk},
|
||||
),
|
||||
ObjectsTablePanel(
|
||||
'users.User',
|
||||
filters={'owner_id': lambda ctx: ctx['object'].pk},
|
||||
),
|
||||
],
|
||||
right_panels=[
|
||||
RelatedObjectsPanel(),
|
||||
|
||||
@@ -31,11 +31,11 @@ class EnhancedURLValidator(URLValidator):
|
||||
fqdn_re = URLValidator.hostname_re + URLValidator.domain_re + URLValidator.tld_re
|
||||
host_res = [URLValidator.ipv4_re, URLValidator.ipv6_re, fqdn_re, URLValidator.hostname_re]
|
||||
regex = _lazy_re_compile(
|
||||
r'^(?:[a-z0-9\.\-\+]*)://' # Scheme (enforced separately)
|
||||
r'(?:\S+(?::\S*)?@)?' # HTTP basic authentication
|
||||
r'(?:' + '|'.join(host_res) + ')' # IPv4, IPv6, FQDN, or hostname
|
||||
r'(?::\d{1,5})?' # Port number
|
||||
r'(?:[/?#][^\s]*)?' # Path
|
||||
r'^(?:[a-z0-9\.\-\+]*)://' # Scheme (enforced separately)
|
||||
r'(?:[^\s:@/]+(?::[^\s:@/]*)?@)?' # HTTP basic authentication
|
||||
r'(?:' + '|'.join(host_res) + ')' # IPv4, IPv6, FQDN, or hostname
|
||||
r'(?::\d{1,5})?' # Port number
|
||||
r'(?:[/?#][^\s]*)?' # Path
|
||||
r'\Z', re.IGNORECASE)
|
||||
schemes = None
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('dcim', '0231_default_ordering_indexes'),
|
||||
('extras', '0137_default_ordering_indexes'),
|
||||
('ipam', '0089_default_ordering_indexes'),
|
||||
('tenancy', '0024_default_ordering_indexes'),
|
||||
('users', '0016_default_ordering_indexes'),
|
||||
('virtualization', '0054_virtualmachinetype'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='virtualmachine',
|
||||
index=models.Index(fields=['name', 'id'], name='virtualizat_name_16033e_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='virtualmachinetype',
|
||||
index=models.Index(fields=['name'], name='virtualizat_name_6cff11_idx'),
|
||||
),
|
||||
]
|
||||
@@ -97,9 +97,6 @@ class Cluster(ContactsMixin, CachedScopeMixin, PrimaryModel):
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
indexes = (
|
||||
models.Index(fields=('name',)), # Default ordering
|
||||
)
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('group', 'name'),
|
||||
|
||||
@@ -92,9 +92,6 @@ class VirtualMachineType(ImageAttachmentsMixin, PrimaryModel):
|
||||
violation_error_message=_('Virtual machine type slug must be unique.'),
|
||||
),
|
||||
)
|
||||
indexes = (
|
||||
models.Index(fields=('name',)), # Default ordering
|
||||
)
|
||||
verbose_name = _('virtual machine type')
|
||||
verbose_name_plural = _('virtual machine types')
|
||||
|
||||
@@ -252,9 +249,6 @@ class VirtualMachine(
|
||||
|
||||
class Meta:
|
||||
ordering = ('name', 'pk') # Name may be non-unique
|
||||
indexes = (
|
||||
models.Index(fields=('name', 'id')), # Default ordering
|
||||
)
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
Lower('name'), 'cluster', 'tenant',
|
||||
|
||||
@@ -492,6 +492,7 @@ class VirtualMachineView(generic.ObjectView):
|
||||
model='ipam.Service',
|
||||
title=_('Application Services'),
|
||||
filters={'virtual_machine_id': lambda ctx: ctx['object'].pk},
|
||||
exclude_columns=['parent'],
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'ipam.Service',
|
||||
@@ -508,6 +509,7 @@ class VirtualMachineView(generic.ObjectView):
|
||||
ObjectsTablePanel(
|
||||
model='virtualization.VirtualDisk',
|
||||
filters={'virtual_machine_id': lambda ctx: ctx['object'].pk},
|
||||
exclude_columns=['virtual_machine'],
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'virtualization.VirtualDisk', url_params={'virtual_machine': lambda ctx: ctx['object'].pk}
|
||||
@@ -649,6 +651,7 @@ class VMInterfaceView(generic.ObjectView):
|
||||
ObjectsTablePanel(
|
||||
model='ipam.IPaddress',
|
||||
filters={'vminterface_id': lambda ctx: ctx['object'].pk},
|
||||
exclude_columns=['assigned', 'assigned_object', 'assigned_object_parent'],
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'ipam.IPaddress',
|
||||
@@ -662,6 +665,7 @@ class VMInterfaceView(generic.ObjectView):
|
||||
ObjectsTablePanel(
|
||||
model='dcim.MACAddress',
|
||||
filters={'vminterface_id': lambda ctx: ctx['object'].pk},
|
||||
exclude_columns=['assigned_object', 'assigned_object_parent'],
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'dcim.MACAddress', url_params={'vminterface': lambda ctx: ctx['object'].pk}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('extras', '0137_default_ordering_indexes'),
|
||||
('ipam', '0089_default_ordering_indexes'),
|
||||
('vpn', '0011_add_comments_to_organizationalmodel'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='tunneltermination',
|
||||
index=models.Index(fields=['tunnel', 'role', 'id'], name='vpn_tunnelt_tunnel__f542d3_idx'),
|
||||
),
|
||||
]
|
||||
@@ -145,9 +145,6 @@ class TunnelTermination(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ChangeLo
|
||||
violation_error_message=_("An object may be terminated to only one tunnel at a time.")
|
||||
),
|
||||
)
|
||||
indexes = (
|
||||
models.Index(fields=('tunnel', 'role', 'id')), # Default ordering
|
||||
)
|
||||
verbose_name = _('tunnel termination')
|
||||
verbose_name_plural = _('tunnel terminations')
|
||||
|
||||
|
||||
@@ -129,6 +129,7 @@ class TunnelView(generic.ObjectView):
|
||||
ObjectsTablePanel(
|
||||
'vpn.tunneltermination',
|
||||
filters={'tunnel_id': lambda ctx: ctx['object'].pk},
|
||||
exclude_columns=['tunnel'],
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'vpn.tunneltermination',
|
||||
@@ -223,6 +224,7 @@ class TunnelTerminationView(generic.ObjectView):
|
||||
'tunnel_id': lambda ctx: ctx['object'].tunnel.pk,
|
||||
'id__n': lambda ctx: ctx['object'].pk,
|
||||
},
|
||||
exclude_columns=['tunnel'],
|
||||
title=_('Peer Terminations'),
|
||||
),
|
||||
],
|
||||
@@ -675,6 +677,7 @@ class L2VPNView(generic.ObjectView):
|
||||
ObjectsTablePanel(
|
||||
'vpn.l2vpntermination',
|
||||
filters={'l2vpn_id': lambda ctx: ctx['object'].pk},
|
||||
exclude_columns=['l2vpn'],
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'vpn.l2vpntermination',
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('dcim', '0231_default_ordering_indexes'),
|
||||
('extras', '0137_default_ordering_indexes'),
|
||||
('ipam', '0089_default_ordering_indexes'),
|
||||
('tenancy', '0024_default_ordering_indexes'),
|
||||
('users', '0016_default_ordering_indexes'),
|
||||
('wireless', '0018_add_mptt_tree_indexes'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='wirelesslan',
|
||||
index=models.Index(fields=['ssid', 'id'], name='wireless_wi_ssid_64a9ce_idx'),
|
||||
),
|
||||
]
|
||||
@@ -118,7 +118,6 @@ class WirelessLAN(WirelessAuthenticationBase, CachedScopeMixin, PrimaryModel):
|
||||
class Meta:
|
||||
ordering = ('ssid', 'pk')
|
||||
indexes = (
|
||||
models.Index(fields=('ssid', 'id')), # Default ordering
|
||||
models.Index(fields=('scope_type', 'scope_id')),
|
||||
)
|
||||
verbose_name = _('wireless LAN')
|
||||
|
||||
@@ -53,6 +53,7 @@ class WirelessLANGroupView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
model='wireless.WirelessLANGroup',
|
||||
title=_('Child Groups'),
|
||||
filters={'parent_id': lambda ctx: ctx['object'].pk},
|
||||
exclude_columns=['parent'],
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'wireless.WirelessLANGroup',
|
||||
|
||||
Reference in New Issue
Block a user