00ff00
)'),
diff --git a/netbox/dcim/forms/connections.py b/netbox/dcim/forms/connections.py
index a2ceea6cf..770dc211b 100644
--- a/netbox/dcim/forms/connections.py
+++ b/netbox/dcim/forms/connections.py
@@ -2,6 +2,7 @@ from circuits.models import Circuit, CircuitTermination, Provider
from dcim.models import *
from extras.forms import CustomFieldModelForm
from extras.models import Tag
+from tenancy.forms import TenancyForm
from utilities.forms import BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, StaticSelect
__all__ = (
@@ -17,7 +18,7 @@ __all__ = (
)
-class ConnectCableToDeviceForm(BootstrapMixin, CustomFieldModelForm):
+class ConnectCableToDeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
"""
Base form for connecting a Cable to a Device component
"""
@@ -78,7 +79,8 @@ class ConnectCableToDeviceForm(BootstrapMixin, CustomFieldModelForm):
model = Cable
fields = [
'termination_b_region', 'termination_b_site', 'termination_b_rack', 'termination_b_device',
- 'termination_b_id', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags',
+ 'termination_b_id', 'type', 'status', 'tenant_group', 'tenant', 'label', 'color', 'length', 'length_unit',
+ 'tags',
]
widgets = {
'status': StaticSelect,
@@ -169,7 +171,7 @@ class ConnectCableToRearPortForm(ConnectCableToDeviceForm):
)
-class ConnectCableToCircuitTerminationForm(BootstrapMixin, CustomFieldModelForm):
+class ConnectCableToCircuitTerminationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
termination_b_provider = DynamicModelChoiceField(
queryset=Provider.objects.all(),
label='Provider',
@@ -219,7 +221,8 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, CustomFieldModelForm)
model = Cable
fields = [
'termination_b_provider', 'termination_b_region', 'termination_b_site', 'termination_b_circuit',
- 'termination_b_id', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags',
+ 'termination_b_id', 'type', 'status', 'tenant_group', 'tenant', 'label', 'color', 'length', 'length_unit',
+ 'tags',
]
def clean_termination_b_id(self):
@@ -227,7 +230,7 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, CustomFieldModelForm)
return getattr(self.cleaned_data['termination_b_id'], 'pk', None)
-class ConnectCableToPowerFeedForm(BootstrapMixin, CustomFieldModelForm):
+class ConnectCableToPowerFeedForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
termination_b_region = DynamicModelChoiceField(
queryset=Region.objects.all(),
label='Region',
@@ -280,8 +283,8 @@ class ConnectCableToPowerFeedForm(BootstrapMixin, CustomFieldModelForm):
class Meta:
model = Cable
fields = [
- 'termination_b_location', 'termination_b_powerpanel', 'termination_b_id', 'type', 'status', 'label',
- 'color', 'length', 'length_unit', 'tags',
+ 'termination_b_location', 'termination_b_powerpanel', 'termination_b_id', 'type', 'status', 'tenant_group',
+ 'tenant', 'label', 'color', 'length', 'length_unit', 'tags',
]
def clean_termination_b_id(self):
diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py
index e28714914..5c776386a 100644
--- a/netbox/dcim/forms/filtersets.py
+++ b/netbox/dcim/forms/filtersets.py
@@ -7,7 +7,6 @@ from dcim.constants import *
from dcim.models import *
from extras.forms import CustomFieldModelFilterForm, LocalConfigContextFilterForm
from tenancy.forms import TenancyFilterForm
-from tenancy.models import Tenant
from utilities.forms import (
APISelectMultiple, add_blank_choice, BootstrapMixin, ColorField, DynamicModelMultipleChoiceField, StaticSelect,
StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
@@ -692,13 +691,13 @@ class VirtualChassisFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldMod
tag = TagFilterField(model)
-class CableFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
+class CableFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
model = Cable
field_groups = [
['q', 'tag'],
['site_id', 'rack_id', 'device_id'],
['type', 'status', 'color'],
- ['tenant_id'],
+ ['tenant_group_id', 'tenant_id'],
]
q = forms.CharField(
required=False,
@@ -720,12 +719,6 @@ class CableFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
label=_('Site'),
fetch_trigger='open'
)
- tenant_id = DynamicModelMultipleChoiceField(
- queryset=Tenant.objects.all(),
- required=False,
- label=_('Tenant'),
- fetch_trigger='open'
- )
rack_id = DynamicModelMultipleChoiceField(
queryset=Rack.objects.all(),
required=False,
@@ -973,10 +966,15 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
model = Interface
field_groups = [
['q', 'tag'],
- ['name', 'label', 'type', 'enabled', 'mgmt_only', 'mac_address', 'wwn'],
+ ['name', 'label', 'kind', 'type', 'enabled', 'mgmt_only', 'mac_address', 'wwn'],
['rf_role', 'rf_channel', 'rf_channel_width'],
['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
]
+ kind = forms.MultipleChoiceField(
+ choices=InterfaceKindChoices,
+ required=False,
+ widget=StaticSelectMultiple()
+ )
type = forms.MultipleChoiceField(
choices=InterfaceTypeChoices,
required=False,
diff --git a/netbox/dcim/forms/models.py b/netbox/dcim/forms/models.py
index 9ce0b54aa..e395c67d2 100644
--- a/netbox/dcim/forms/models.py
+++ b/netbox/dcim/forms/models.py
@@ -71,11 +71,15 @@ class RegionForm(BootstrapMixin, CustomFieldModelForm):
required=False
)
slug = SlugField()
+ tags = DynamicModelMultipleChoiceField(
+ queryset=Tag.objects.all(),
+ required=False
+ )
class Meta:
model = Region
fields = (
- 'parent', 'name', 'slug', 'description',
+ 'parent', 'name', 'slug', 'description', 'tags',
)
@@ -85,11 +89,15 @@ class SiteGroupForm(BootstrapMixin, CustomFieldModelForm):
required=False
)
slug = SlugField()
+ tags = DynamicModelMultipleChoiceField(
+ queryset=Tag.objects.all(),
+ required=False
+ )
class Meta:
model = SiteGroup
fields = (
- 'parent', 'name', 'slug', 'description',
+ 'parent', 'name', 'slug', 'description', 'tags',
)
@@ -188,15 +196,19 @@ class LocationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
}
)
slug = SlugField()
+ tags = DynamicModelMultipleChoiceField(
+ queryset=Tag.objects.all(),
+ required=False
+ )
class Meta:
model = Location
fields = (
- 'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description', 'tenant_group', 'tenant',
+ 'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description', 'tenant_group', 'tenant', 'tags',
)
fieldsets = (
('Location', (
- 'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description',
+ 'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description', 'tags',
)),
('Tenancy', ('tenant_group', 'tenant')),
)
@@ -204,11 +216,15 @@ class LocationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
class RackRoleForm(BootstrapMixin, CustomFieldModelForm):
slug = SlugField()
+ tags = DynamicModelMultipleChoiceField(
+ queryset=Tag.objects.all(),
+ required=False
+ )
class Meta:
model = RackRole
fields = [
- 'name', 'slug', 'color', 'description',
+ 'name', 'slug', 'color', 'description', 'tags',
]
@@ -344,11 +360,15 @@ class RackReservationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
class ManufacturerForm(BootstrapMixin, CustomFieldModelForm):
slug = SlugField()
+ tags = DynamicModelMultipleChoiceField(
+ queryset=Tag.objects.all(),
+ required=False
+ )
class Meta:
model = Manufacturer
fields = [
- 'name', 'slug', 'description',
+ 'name', 'slug', 'description', 'tags',
]
@@ -393,11 +413,15 @@ class DeviceTypeForm(BootstrapMixin, CustomFieldModelForm):
class DeviceRoleForm(BootstrapMixin, CustomFieldModelForm):
slug = SlugField()
+ tags = DynamicModelMultipleChoiceField(
+ queryset=Tag.objects.all(),
+ required=False
+ )
class Meta:
model = DeviceRole
fields = [
- 'name', 'slug', 'color', 'vm_role', 'description',
+ 'name', 'slug', 'color', 'vm_role', 'description', 'tags',
]
@@ -409,11 +433,15 @@ class PlatformForm(BootstrapMixin, CustomFieldModelForm):
slug = SlugField(
max_length=64
)
+ tags = DynamicModelMultipleChoiceField(
+ queryset=Tag.objects.all(),
+ required=False
+ )
class Meta:
model = Platform
fields = [
- 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description',
+ 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'tags',
]
widgets = {
'napalm_args': SmallTextarea(),
@@ -602,7 +630,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
self.fields['position'].widget.choices = [(position, f'U{position}')]
-class CableForm(BootstrapMixin, CustomFieldModelForm):
+class CableForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
@@ -611,7 +639,7 @@ class CableForm(BootstrapMixin, CustomFieldModelForm):
class Meta:
model = Cable
fields = [
- 'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags',
+ 'type', 'status', 'tenant_group', 'tenant', 'label', 'color', 'length', 'length_unit', 'tags',
]
widgets = {
'status': StaticSelect,
diff --git a/netbox/dcim/migrations/0135_location_tenant.py b/netbox/dcim/migrations/0135_tenancy_extensions.py
similarity index 67%
rename from netbox/dcim/migrations/0135_location_tenant.py
rename to netbox/dcim/migrations/0135_tenancy_extensions.py
index 0b1f429f9..673b5027f 100644
--- a/netbox/dcim/migrations/0135_location_tenant.py
+++ b/netbox/dcim/migrations/0135_tenancy_extensions.py
@@ -15,4 +15,9 @@ class Migration(migrations.Migration):
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='locations', to='tenancy.tenant'),
),
+ migrations.AddField(
+ model_name='cable',
+ name='tenant',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='cables', to='tenancy.tenant'),
+ ),
]
diff --git a/netbox/dcim/migrations/0136_device_airflow.py b/netbox/dcim/migrations/0136_device_airflow.py
index a0887a0b4..94cc89f3f 100644
--- a/netbox/dcim/migrations/0136_device_airflow.py
+++ b/netbox/dcim/migrations/0136_device_airflow.py
@@ -4,7 +4,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('dcim', '0135_location_tenant'),
+ ('dcim', '0135_tenancy_extensions'),
]
operations = [
diff --git a/netbox/dcim/migrations/0137_relax_uniqueness_constraints.py b/netbox/dcim/migrations/0137_relax_uniqueness_constraints.py
new file mode 100644
index 000000000..8f7d40026
--- /dev/null
+++ b/netbox/dcim/migrations/0137_relax_uniqueness_constraints.py
@@ -0,0 +1,45 @@
+# Generated by Django 3.2.8 on 2021-10-19 17:41
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dcim', '0136_device_airflow'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='region',
+ name='name',
+ field=models.CharField(max_length=100),
+ ),
+ migrations.AlterField(
+ model_name='region',
+ name='slug',
+ field=models.SlugField(max_length=100),
+ ),
+ migrations.AlterField(
+ model_name='sitegroup',
+ name='name',
+ field=models.CharField(max_length=100),
+ ),
+ migrations.AlterField(
+ model_name='sitegroup',
+ name='slug',
+ field=models.SlugField(max_length=100),
+ ),
+ migrations.AlterUniqueTogether(
+ name='location',
+ unique_together={('site', 'parent', 'name'), ('site', 'parent', 'slug')},
+ ),
+ migrations.AlterUniqueTogether(
+ name='region',
+ unique_together={('parent', 'slug'), ('parent', 'name')},
+ ),
+ migrations.AlterUniqueTogether(
+ name='sitegroup',
+ unique_together={('parent', 'slug'), ('parent', 'name')},
+ ),
+ ]
diff --git a/netbox/dcim/migrations/0138_extend_tag_support.py b/netbox/dcim/migrations/0138_extend_tag_support.py
new file mode 100644
index 000000000..763b53c50
--- /dev/null
+++ b/netbox/dcim/migrations/0138_extend_tag_support.py
@@ -0,0 +1,50 @@
+# Generated by Django 3.2.8 on 2021-10-21 14:50
+
+from django.db import migrations
+import taggit.managers
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('extras', '0062_clear_secrets_changelog'),
+ ('dcim', '0137_relax_uniqueness_constraints'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='devicerole',
+ name='tags',
+ field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
+ ),
+ migrations.AddField(
+ model_name='location',
+ name='tags',
+ field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
+ ),
+ migrations.AddField(
+ model_name='manufacturer',
+ name='tags',
+ field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
+ ),
+ migrations.AddField(
+ model_name='platform',
+ name='tags',
+ field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
+ ),
+ migrations.AddField(
+ model_name='rackrole',
+ name='tags',
+ field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
+ ),
+ migrations.AddField(
+ model_name='region',
+ name='tags',
+ field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
+ ),
+ migrations.AddField(
+ model_name='sitegroup',
+ name='tags',
+ field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
+ ),
+ ]
diff --git a/netbox/dcim/migrations/0137_rename_cable_peer.py b/netbox/dcim/migrations/0139_rename_cable_peer.py
similarity index 98%
rename from netbox/dcim/migrations/0137_rename_cable_peer.py
rename to netbox/dcim/migrations/0139_rename_cable_peer.py
index bc13b3e9a..59dc04e2a 100644
--- a/netbox/dcim/migrations/0137_rename_cable_peer.py
+++ b/netbox/dcim/migrations/0139_rename_cable_peer.py
@@ -4,7 +4,7 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
- ('dcim', '0136_device_airflow'),
+ ('dcim', '0138_extend_tag_support'),
]
operations = [
diff --git a/netbox/dcim/migrations/0138_wireless.py b/netbox/dcim/migrations/0140_wireless.py
similarity index 97%
rename from netbox/dcim/migrations/0138_wireless.py
rename to netbox/dcim/migrations/0140_wireless.py
index bbdb28283..012b78dd4 100644
--- a/netbox/dcim/migrations/0138_wireless.py
+++ b/netbox/dcim/migrations/0140_wireless.py
@@ -5,7 +5,7 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
- ('dcim', '0137_rename_cable_peer'),
+ ('dcim', '0139_rename_cable_peer'),
('wireless', '0001_wireless'),
]
diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py
index 129617746..54012f0e9 100644
--- a/netbox/dcim/models/cables.py
+++ b/netbox/dcim/models/cables.py
@@ -67,6 +67,13 @@ class Cable(PrimaryModel):
choices=LinkStatusChoices,
default=LinkStatusChoices.STATUS_CONNECTED
)
+ tenant = models.ForeignKey(
+ to='tenancy.Tenant',
+ on_delete=models.PROTECT,
+ related_name='cables',
+ blank=True,
+ null=True
+ )
label = models.CharField(
max_length=100,
blank=True
diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py
index 669f5cfbd..2b3b80d24 100644
--- a/netbox/dcim/models/devices.py
+++ b/netbox/dcim/models/devices.py
@@ -36,7 +36,7 @@ __all__ = (
# Device Types
#
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Manufacturer(OrganizationalModel):
"""
A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
@@ -54,6 +54,11 @@ class Manufacturer(OrganizationalModel):
blank=True
)
+ # Generic relations
+ contacts = GenericRelation(
+ to='tenancy.ContactAssignment'
+ )
+
objects = RestrictedQuerySet.as_manager()
class Meta:
@@ -346,7 +351,7 @@ class DeviceType(PrimaryModel):
# Devices
#
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class DeviceRole(OrganizationalModel):
"""
Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a
@@ -386,7 +391,7 @@ class DeviceRole(OrganizationalModel):
return reverse('dcim:devicerole', args=[self.pk])
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Platform(OrganizationalModel):
"""
Platform refers to the software or firmware running on a Device. For example, "Cisco IOS-XR" or "Juniper Junos".
@@ -584,6 +589,11 @@ class Device(PrimaryModel, ConfigContextModel):
comments = models.TextField(
blank=True
)
+
+ # Generic relations
+ contacts = GenericRelation(
+ to='tenancy.ContactAssignment'
+ )
images = GenericRelation(
to='extras.ImageAttachment'
)
diff --git a/netbox/dcim/models/power.py b/netbox/dcim/models/power.py
index 6d6a04cea..30e11b342 100644
--- a/netbox/dcim/models/power.py
+++ b/netbox/dcim/models/power.py
@@ -40,6 +40,11 @@ class PowerPanel(PrimaryModel):
name = models.CharField(
max_length=100
)
+
+ # Generic relations
+ contacts = GenericRelation(
+ to='tenancy.ContactAssignment'
+ )
images = GenericRelation(
to='extras.ImageAttachment'
)
diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py
index 94e7bf53a..a6d7f33af 100644
--- a/netbox/dcim/models/racks.py
+++ b/netbox/dcim/models/racks.py
@@ -35,7 +35,7 @@ __all__ = (
# Racks
#
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class RackRole(OrganizationalModel):
"""
Racks can be organized by functional role, similar to Devices.
@@ -175,12 +175,17 @@ class Rack(PrimaryModel):
comments = models.TextField(
blank=True
)
+
+ # Generic relations
vlan_groups = GenericRelation(
to='ipam.VLANGroup',
content_type_field='scope_type',
object_id_field='scope_id',
related_query_name='rack'
)
+ contacts = GenericRelation(
+ to='tenancy.ContactAssignment'
+ )
images = GenericRelation(
to='extras.ImageAttachment'
)
diff --git a/netbox/dcim/models/sites.py b/netbox/dcim/models/sites.py
index b343f61f2..a978e69e6 100644
--- a/netbox/dcim/models/sites.py
+++ b/netbox/dcim/models/sites.py
@@ -25,7 +25,7 @@ __all__ = (
# Regions
#
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Region(NestedGroupModel):
"""
A region represents a geographic collection of sites. For example, you might create regions representing countries,
@@ -41,23 +41,32 @@ class Region(NestedGroupModel):
db_index=True
)
name = models.CharField(
- max_length=100,
- unique=True
+ max_length=100
)
slug = models.SlugField(
- max_length=100,
- unique=True
+ max_length=100
)
description = models.CharField(
max_length=200,
blank=True
)
+
+ # Generic relations
vlan_groups = GenericRelation(
to='ipam.VLANGroup',
content_type_field='scope_type',
object_id_field='scope_id',
related_query_name='region'
)
+ contacts = GenericRelation(
+ to='tenancy.ContactAssignment'
+ )
+
+ class Meta:
+ unique_together = (
+ ('parent', 'name'),
+ ('parent', 'slug'),
+ )
def get_absolute_url(self):
return reverse('dcim:region', args=[self.pk])
@@ -73,7 +82,7 @@ class Region(NestedGroupModel):
# Site groups
#
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class SiteGroup(NestedGroupModel):
"""
A site group is an arbitrary grouping of sites. For example, you might have corporate sites and customer sites; and
@@ -89,23 +98,32 @@ class SiteGroup(NestedGroupModel):
db_index=True
)
name = models.CharField(
- max_length=100,
- unique=True
+ max_length=100
)
slug = models.SlugField(
- max_length=100,
- unique=True
+ max_length=100
)
description = models.CharField(
max_length=200,
blank=True
)
+
+ # Generic relations
vlan_groups = GenericRelation(
to='ipam.VLANGroup',
content_type_field='scope_type',
object_id_field='scope_id',
related_query_name='site_group'
)
+ contacts = GenericRelation(
+ to='tenancy.ContactAssignment'
+ )
+
+ class Meta:
+ unique_together = (
+ ('parent', 'name'),
+ ('parent', 'slug'),
+ )
def get_absolute_url(self):
return reverse('dcim:sitegroup', args=[self.pk])
@@ -221,12 +239,17 @@ class Site(PrimaryModel):
comments = models.TextField(
blank=True
)
+
+ # Generic relations
vlan_groups = GenericRelation(
to='ipam.VLANGroup',
content_type_field='scope_type',
object_id_field='scope_id',
related_query_name='site'
)
+ contacts = GenericRelation(
+ to='tenancy.ContactAssignment'
+ )
images = GenericRelation(
to='extras.ImageAttachment'
)
@@ -255,7 +278,7 @@ class Site(PrimaryModel):
# Locations
#
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Location(NestedGroupModel):
"""
A Location represents a subgroup of Racks and/or Devices within a Site. A Location may represent a building within a
@@ -291,12 +314,17 @@ class Location(NestedGroupModel):
max_length=200,
blank=True
)
+
+ # Generic relations
vlan_groups = GenericRelation(
to='ipam.VLANGroup',
content_type_field='scope_type',
object_id_field='scope_id',
related_query_name='location'
)
+ contacts = GenericRelation(
+ to='tenancy.ContactAssignment'
+ )
images = GenericRelation(
to='extras.ImageAttachment'
)
@@ -305,10 +333,10 @@ class Location(NestedGroupModel):
class Meta:
ordering = ['site', 'name']
- unique_together = [
- ['site', 'name'],
- ['site', 'slug'],
- ]
+ unique_together = ([
+ ('site', 'parent', 'name'),
+ ('site', 'parent', 'slug'),
+ ])
def get_absolute_url(self):
return reverse('dcim:location', args=[self.pk])
diff --git a/netbox/dcim/tables/cables.py b/netbox/dcim/tables/cables.py
index 14cf34505..87913cbfd 100644
--- a/netbox/dcim/tables/cables.py
+++ b/netbox/dcim/tables/cables.py
@@ -2,6 +2,7 @@ import django_tables2 as tables
from django_tables2.utils import Accessor
from dcim.models import Cable
+from tenancy.tables import TenantColumn
from utilities.tables import BaseTable, ChoiceFieldColumn, ColorColumn, TagColumn, TemplateColumn, ToggleColumn
from .template_code import CABLE_LENGTH, CABLE_TERMINATION_PARENT
@@ -45,6 +46,7 @@ class CableTable(BaseTable):
verbose_name='Termination B'
)
status = ChoiceFieldColumn()
+ tenant = TenantColumn()
length = TemplateColumn(
template_code=CABLE_LENGTH,
order_by='_abs_length'
@@ -58,7 +60,7 @@ class CableTable(BaseTable):
model = Cable
fields = (
'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b',
- 'status', 'type', 'color', 'length', 'tags',
+ 'status', 'type', 'tenant', 'color', 'length', 'tags',
)
default_columns = (
'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b',
diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py
index 343667d46..06c594f6b 100644
--- a/netbox/dcim/tables/devices.py
+++ b/netbox/dcim/tables/devices.py
@@ -80,11 +80,16 @@ class DeviceRoleTable(BaseTable):
)
color = ColorColumn()
vm_role = BooleanColumn()
+ tags = TagColumn(
+ url_name='dcim:devicerole_list'
+ )
actions = ButtonsColumn(DeviceRole)
class Meta(BaseTable.Meta):
model = DeviceRole
- fields = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'actions')
+ fields = (
+ 'pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'tags', 'actions',
+ )
default_columns = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'actions')
@@ -107,13 +112,16 @@ class PlatformTable(BaseTable):
url_params={'platform_id': 'pk'},
verbose_name='VMs'
)
+ tags = TagColumn(
+ url_name='dcim:platform_list'
+ )
actions = ButtonsColumn(Platform)
class Meta(BaseTable.Meta):
model = Platform
fields = (
'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'napalm_args',
- 'description', 'actions',
+ 'description', 'tags', 'actions',
)
default_columns = (
'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'napalm_driver', 'description', 'actions',
diff --git a/netbox/dcim/tables/devicetypes.py b/netbox/dcim/tables/devicetypes.py
index b3310d5d2..9631b5709 100644
--- a/netbox/dcim/tables/devicetypes.py
+++ b/netbox/dcim/tables/devicetypes.py
@@ -41,12 +41,16 @@ class ManufacturerTable(BaseTable):
verbose_name='Platforms'
)
slug = tables.Column()
+ tags = TagColumn(
+ url_name='dcim:manufacturer_list'
+ )
actions = ButtonsColumn(Manufacturer)
class Meta(BaseTable.Meta):
model = Manufacturer
fields = (
- 'pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug', 'actions',
+ 'pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug', 'tags',
+ 'actions',
)
diff --git a/netbox/dcim/tables/racks.py b/netbox/dcim/tables/racks.py
index fcc3ed4d2..bdc5ae713 100644
--- a/netbox/dcim/tables/racks.py
+++ b/netbox/dcim/tables/racks.py
@@ -24,11 +24,14 @@ class RackRoleTable(BaseTable):
name = tables.Column(linkify=True)
rack_count = tables.Column(verbose_name='Racks')
color = ColorColumn()
+ tags = TagColumn(
+ url_name='dcim:rackrole_list'
+ )
actions = ButtonsColumn(RackRole)
class Meta(BaseTable.Meta):
model = RackRole
- fields = ('pk', 'name', 'rack_count', 'color', 'description', 'slug', 'actions')
+ fields = ('pk', 'name', 'rack_count', 'color', 'description', 'slug', 'tags', 'actions')
default_columns = ('pk', 'name', 'rack_count', 'color', 'description', 'actions')
diff --git a/netbox/dcim/tables/sites.py b/netbox/dcim/tables/sites.py
index 3ff6ab75b..65419e9c8 100644
--- a/netbox/dcim/tables/sites.py
+++ b/netbox/dcim/tables/sites.py
@@ -29,11 +29,14 @@ class RegionTable(BaseTable):
url_params={'region_id': 'pk'},
verbose_name='Sites'
)
+ tags = TagColumn(
+ url_name='dcim:region_list'
+ )
actions = ButtonsColumn(Region)
class Meta(BaseTable.Meta):
model = Region
- fields = ('pk', 'name', 'slug', 'site_count', 'description', 'actions')
+ fields = ('pk', 'name', 'slug', 'site_count', 'description', 'tags', 'actions')
default_columns = ('pk', 'name', 'site_count', 'description', 'actions')
@@ -51,11 +54,14 @@ class SiteGroupTable(BaseTable):
url_params={'group_id': 'pk'},
verbose_name='Sites'
)
+ tags = TagColumn(
+ url_name='dcim:sitegroup_list'
+ )
actions = ButtonsColumn(SiteGroup)
class Meta(BaseTable.Meta):
model = SiteGroup
- fields = ('pk', 'name', 'slug', 'site_count', 'description', 'actions')
+ fields = ('pk', 'name', 'slug', 'site_count', 'description', 'tags', 'actions')
default_columns = ('pk', 'name', 'site_count', 'description', 'actions')
@@ -114,6 +120,9 @@ class LocationTable(BaseTable):
url_params={'location_id': 'pk'},
verbose_name='Devices'
)
+ tags = TagColumn(
+ url_name='dcim:location_list'
+ )
actions = ButtonsColumn(
model=Location,
prepend_template=LOCATION_ELEVATIONS
@@ -121,5 +130,7 @@ class LocationTable(BaseTable):
class Meta(BaseTable.Meta):
model = Location
- fields = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'actions')
+ fields = (
+ 'pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'tags', 'actions',
+ )
default_columns = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'actions')
diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py
index 62bdaed82..f66ceb855 100644
--- a/netbox/dcim/tests/test_filtersets.py
+++ b/netbox/dcim/tests/test_filtersets.py
@@ -2838,6 +2838,7 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
tenants = (
Tenant(name='Tenant 1', slug='tenant-1'),
Tenant(name='Tenant 2', slug='tenant-2'),
+ Tenant(name='Tenant 3', slug='tenant-3'),
)
Tenant.objects.bulk_create(tenants)
@@ -2853,9 +2854,9 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
devices = (
- Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], rack=racks[0], position=1, tenant=tenants[0]),
- Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[0], rack=racks[0], position=2, tenant=tenants[0]),
- Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[1], rack=racks[1], position=1, tenant=tenants[1]),
+ Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], rack=racks[0], position=1),
+ Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[0], rack=racks[0], position=2),
+ Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[1], rack=racks[1], position=1),
Device(name='Device 4', device_type=device_type, device_role=device_role, site=sites[1], rack=racks[1], position=2),
Device(name='Device 5', device_type=device_type, device_role=device_role, site=sites[2], rack=racks[2], position=1),
Device(name='Device 6', device_type=device_type, device_role=device_role, site=sites[2], rack=racks[2], position=2),
@@ -2882,12 +2883,12 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
console_server_port = ConsoleServerPort.objects.create(device=devices[0], name='Console Server Port 1')
# Cables
- Cable(termination_a=interfaces[1], termination_b=interfaces[2], label='Cable 1', type=CableTypeChoices.TYPE_CAT3, status=LinkStatusChoices.STATUS_CONNECTED, color='aa1409', length=10, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
- Cable(termination_a=interfaces[3], termination_b=interfaces[4], label='Cable 2', type=CableTypeChoices.TYPE_CAT3, status=LinkStatusChoices.STATUS_CONNECTED, color='aa1409', length=20, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
- Cable(termination_a=interfaces[5], termination_b=interfaces[6], label='Cable 3', type=CableTypeChoices.TYPE_CAT5E, status=LinkStatusChoices.STATUS_CONNECTED, color='f44336', length=30, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
- Cable(termination_a=interfaces[7], termination_b=interfaces[8], label='Cable 4', type=CableTypeChoices.TYPE_CAT5E, status=LinkStatusChoices.STATUS_PLANNED, color='f44336', length=40, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
- Cable(termination_a=interfaces[9], termination_b=interfaces[10], label='Cable 5', type=CableTypeChoices.TYPE_CAT6, status=LinkStatusChoices.STATUS_PLANNED, color='e91e63', length=10, length_unit=CableLengthUnitChoices.UNIT_METER).save()
- Cable(termination_a=interfaces[11], termination_b=interfaces[0], label='Cable 6', type=CableTypeChoices.TYPE_CAT6, status=LinkStatusChoices.STATUS_PLANNED, color='e91e63', length=20, length_unit=CableLengthUnitChoices.UNIT_METER).save()
+ Cable(termination_a=interfaces[1], termination_b=interfaces[2], label='Cable 1', type=CableTypeChoices.TYPE_CAT3, tenant=tenants[0], status=LinkStatusChoices.STATUS_CONNECTED, color='aa1409', length=10, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
+ Cable(termination_a=interfaces[3], termination_b=interfaces[4], label='Cable 2', type=CableTypeChoices.TYPE_CAT3, tenant=tenants[0], status=LinkStatusChoices.STATUS_CONNECTED, color='aa1409', length=20, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
+ Cable(termination_a=interfaces[5], termination_b=interfaces[6], label='Cable 3', type=CableTypeChoices.TYPE_CAT5E, tenant=tenants[1], status=LinkStatusChoices.STATUS_CONNECTED, color='f44336', length=30, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
+ Cable(termination_a=interfaces[7], termination_b=interfaces[8], label='Cable 4', type=CableTypeChoices.TYPE_CAT5E, tenant=tenants[1], status=LinkStatusChoices.STATUS_PLANNED, color='f44336', length=40, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
+ Cable(termination_a=interfaces[9], termination_b=interfaces[10], label='Cable 5', type=CableTypeChoices.TYPE_CAT6, tenant=tenants[2], status=LinkStatusChoices.STATUS_PLANNED, color='e91e63', length=10, length_unit=CableLengthUnitChoices.UNIT_METER).save()
+ Cable(termination_a=interfaces[11], termination_b=interfaces[0], label='Cable 6', type=CableTypeChoices.TYPE_CAT6, tenant=tenants[2], status=LinkStatusChoices.STATUS_PLANNED, color='e91e63', length=20, length_unit=CableLengthUnitChoices.UNIT_METER).save()
Cable(termination_a=console_port, termination_b=console_server_port, label='Cable 7').save()
def test_label(self):
@@ -2940,9 +2941,9 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
def test_tenant(self):
tenant = Tenant.objects.all()[:2]
params = {'tenant_id': [tenant[0].pk, tenant[1].pk]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'tenant': [tenant[0].slug, tenant[1].slug]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_termination_types(self):
params = {'termination_a_type': 'dcim.consoleport'}
diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py
index c0af2d438..c08eb6e8a 100644
--- a/netbox/dcim/tests/test_views.py
+++ b/netbox/dcim/tests/test_views.py
@@ -31,11 +31,14 @@ class RegionTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
for region in regions:
region.save()
+ tags = create_tags('Alpha', 'Bravo', 'Charlie')
+
cls.form_data = {
'name': 'Region X',
'slug': 'region-x',
'parent': regions[2].pk,
'description': 'A new region',
+ 'tags': [t.pk for t in tags],
}
cls.csv_data = (
@@ -65,11 +68,14 @@ class SiteGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
for sitegroup in sitegroups:
sitegroup.save()
+ tags = create_tags('Alpha', 'Bravo', 'Charlie')
+
cls.form_data = {
'name': 'Site Group X',
'slug': 'site-group-x',
'parent': sitegroups[2].pk,
'description': 'A new site group',
+ 'tags': [t.pk for t in tags],
}
cls.csv_data = (
@@ -169,12 +175,15 @@ class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
for location in locations:
location.save()
+ tags = create_tags('Alpha', 'Bravo', 'Charlie')
+
cls.form_data = {
'name': 'Location X',
'slug': 'location-x',
'site': site.pk,
'tenant': tenant.pk,
'description': 'A new location',
+ 'tags': [t.pk for t in tags],
}
cls.csv_data = (
@@ -201,11 +210,14 @@ class RackRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
RackRole(name='Rack Role 3', slug='rack-role-3'),
])
+ tags = create_tags('Alpha', 'Bravo', 'Charlie')
+
cls.form_data = {
'name': 'Rack Role X',
'slug': 'rack-role-x',
'color': 'c0c0c0',
'description': 'New role',
+ 'tags': [t.pk for t in tags],
}
cls.csv_data = (
@@ -368,10 +380,13 @@ class ManufacturerTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
Manufacturer(name='Manufacturer 3', slug='manufacturer-3'),
])
+ tags = create_tags('Alpha', 'Bravo', 'Charlie')
+
cls.form_data = {
'name': 'Manufacturer X',
'slug': 'manufacturer-x',
'description': 'A new manufacturer',
+ 'tags': [t.pk for t in tags],
}
cls.csv_data = (
@@ -435,6 +450,116 @@ class DeviceTypeTestCase(
'is_full_depth': False,
}
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
+ def test_devicetype_consoleports(self):
+ devicetype = DeviceType.objects.first()
+ console_ports = (
+ ConsolePortTemplate(device_type=devicetype, name='Console Port 1'),
+ ConsolePortTemplate(device_type=devicetype, name='Console Port 2'),
+ ConsolePortTemplate(device_type=devicetype, name='Console Port 3'),
+ )
+ ConsolePortTemplate.objects.bulk_create(console_ports)
+
+ url = reverse('dcim:devicetype_consoleports', kwargs={'pk': devicetype.pk})
+ self.assertHttpStatus(self.client.get(url), 200)
+
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
+ def test_devicetype_consoleserverports(self):
+ devicetype = DeviceType.objects.first()
+ console_server_ports = (
+ ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port 1'),
+ ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port 2'),
+ ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port 3'),
+ )
+ ConsoleServerPortTemplate.objects.bulk_create(console_server_ports)
+
+ url = reverse('dcim:devicetype_consoleserverports', kwargs={'pk': devicetype.pk})
+ self.assertHttpStatus(self.client.get(url), 200)
+
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
+ def test_devicetype_powerports(self):
+ devicetype = DeviceType.objects.first()
+ power_ports = (
+ PowerPortTemplate(device_type=devicetype, name='Power Port 1'),
+ PowerPortTemplate(device_type=devicetype, name='Power Port 2'),
+ PowerPortTemplate(device_type=devicetype, name='Power Port 3'),
+ )
+ PowerPortTemplate.objects.bulk_create(power_ports)
+
+ url = reverse('dcim:devicetype_powerports', kwargs={'pk': devicetype.pk})
+ self.assertHttpStatus(self.client.get(url), 200)
+
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
+ def test_devicetype_poweroutlets(self):
+ devicetype = DeviceType.objects.first()
+ power_outlets = (
+ PowerOutletTemplate(device_type=devicetype, name='Power Outlet 1'),
+ PowerOutletTemplate(device_type=devicetype, name='Power Outlet 2'),
+ PowerOutletTemplate(device_type=devicetype, name='Power Outlet 3'),
+ )
+ PowerOutletTemplate.objects.bulk_create(power_outlets)
+
+ url = reverse('dcim:devicetype_poweroutlets', kwargs={'pk': devicetype.pk})
+ self.assertHttpStatus(self.client.get(url), 200)
+
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
+ def test_devicetype_interfaces(self):
+ devicetype = DeviceType.objects.first()
+ interfaces = (
+ InterfaceTemplate(device_type=devicetype, name='Interface 1'),
+ InterfaceTemplate(device_type=devicetype, name='Interface 2'),
+ InterfaceTemplate(device_type=devicetype, name='Interface 3'),
+ )
+ InterfaceTemplate.objects.bulk_create(interfaces)
+
+ url = reverse('dcim:devicetype_interfaces', kwargs={'pk': devicetype.pk})
+ self.assertHttpStatus(self.client.get(url), 200)
+
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
+ def test_devicetype_rearports(self):
+ devicetype = DeviceType.objects.first()
+ rear_ports = (
+ RearPortTemplate(device_type=devicetype, name='Rear Port 1'),
+ RearPortTemplate(device_type=devicetype, name='Rear Port 2'),
+ RearPortTemplate(device_type=devicetype, name='Rear Port 3'),
+ )
+ RearPortTemplate.objects.bulk_create(rear_ports)
+
+ url = reverse('dcim:devicetype_rearports', kwargs={'pk': devicetype.pk})
+ self.assertHttpStatus(self.client.get(url), 200)
+
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
+ def test_devicetype_frontports(self):
+ devicetype = DeviceType.objects.first()
+ rear_ports = (
+ RearPortTemplate(device_type=devicetype, name='Rear Port 1'),
+ RearPortTemplate(device_type=devicetype, name='Rear Port 2'),
+ RearPortTemplate(device_type=devicetype, name='Rear Port 3'),
+ )
+ RearPortTemplate.objects.bulk_create(rear_ports)
+ front_ports = (
+ FrontPortTemplate(device_type=devicetype, name='Front Port 1', rear_port=rear_ports[0], rear_port_position=1),
+ FrontPortTemplate(device_type=devicetype, name='Front Port 2', rear_port=rear_ports[1], rear_port_position=1),
+ FrontPortTemplate(device_type=devicetype, name='Front Port 3', rear_port=rear_ports[2], rear_port_position=1),
+ )
+ FrontPortTemplate.objects.bulk_create(front_ports)
+
+ url = reverse('dcim:devicetype_frontports', kwargs={'pk': devicetype.pk})
+ self.assertHttpStatus(self.client.get(url), 200)
+
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
+ def test_devicetype_devicebays(self):
+ devicetype = DeviceType.objects.first()
+ device_bays = (
+ DeviceBayTemplate(device_type=devicetype, name='Device Bay 1'),
+ DeviceBayTemplate(device_type=devicetype, name='Device Bay 2'),
+ DeviceBayTemplate(device_type=devicetype, name='Device Bay 3'),
+ )
+ DeviceBayTemplate.objects.bulk_create(device_bays)
+
+ url = reverse('dcim:devicetype_devicebays', kwargs={'pk': devicetype.pk})
+ self.assertHttpStatus(self.client.get(url), 200)
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_import_objects(self):
"""
@@ -924,12 +1049,15 @@ class DeviceRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
DeviceRole(name='Device Role 3', slug='device-role-3'),
])
+ tags = create_tags('Alpha', 'Bravo', 'Charlie')
+
cls.form_data = {
'name': 'Devie Role X',
'slug': 'device-role-x',
'color': 'c0c0c0',
'vm_role': False,
'description': 'New device role',
+ 'tags': [t.pk for t in tags],
}
cls.csv_data = (
@@ -959,6 +1087,8 @@ class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturer),
])
+ tags = create_tags('Alpha', 'Bravo', 'Charlie')
+
cls.form_data = {
'name': 'Platform X',
'slug': 'platform-x',
@@ -966,6 +1096,7 @@ class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
'napalm_driver': 'junos',
'napalm_args': None,
'description': 'A new platform',
+ 'tags': [t.pk for t in tags],
}
cls.csv_data = (
diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py
index 01e470e5c..dd81ca2ba 100644
--- a/netbox/dcim/urls.py
+++ b/netbox/dcim/urls.py
@@ -109,6 +109,14 @@ urlpatterns = [
path('device-types/edit/', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'),
path('device-types/delete/', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
path('device-types/{{ object.napalm_args }}
Physical Address | @@ -138,36 +129,59 @@ {% endif %} |
---|---|
Contact Name | -{{ object.contact_name|placeholder }} | -
Contact Phone | -- {% if object.contact_phone %} - {{ object.contact_phone }} - {% else %} - — - {% endif %} - | -
Contact E-Mail | -- {% if object.contact_email %} - {{ object.contact_email }} - {% else %} - — - {% endif %} - | -
Contact Name | +
+ {% if object.contact_name %}
+
+
+
+ {% endif %}
+ {{ object.contact_name|placeholder }}
+ |
+
---|---|
Contact Phone | +
+ {% if object.contact_phone %}
+
+
+
+ {{ object.contact_phone }}
+ {% else %}
+ —
+ {% endif %}
+ |
+
Contact E-Mail | +
+ {% if object.contact_email %}
+
+
+
+ {{ object.contact_email }}
+ {% else %}
+ —
+ {% endif %}
+ |
+
{% for k, v in object.postchange_data.items %}{% spaceless %}
- {{ k }}: {{ v|render_json }}
- {% endspaceless %}{% endfor %}
-
+ {% for k, v in object.postchange_data.items %}{% spaceless %}
+ {{ k }}: {{ v|render_json }}
+ {% endspaceless %}{% endfor %}
+
{% else %}
- None
+ None
{% endif %}