diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9ba75118b..b12c80eac 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -56,7 +56,7 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- pip install pycodestyle coverage
+ pip install pycodestyle coverage tblib
ln -s configuration.testing.py netbox/netbox/configuration.py
- name: Build documentation
diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md
index a80115ac7..1f6a52891 100644
--- a/docs/release-notes/version-3.1.md
+++ b/docs/release-notes/version-3.1.md
@@ -5,10 +5,14 @@
### Enhancements
* [#8233](https://github.com/netbox-community/netbox/issues/8233) - Restrict API key usage by source IP
+* [#8457](https://github.com/netbox-community/netbox/issues/8457) - Enable adding non-racked devices from site & location views
* [#8553](https://github.com/netbox-community/netbox/issues/8553) - Add missing object types to global search form
+* [#8575](https://github.com/netbox-community/netbox/issues/8575) - Add rack columns to cables list
+* [#8645](https://github.com/netbox-community/netbox/issues/8645) - Enable filtering objects by assigned contacts & contact roles
### Bug Fixes
+* [#8813](https://github.com/netbox-community/netbox/issues/8813) - Retain global search bar query after submitting
* [#8820](https://github.com/netbox-community/netbox/issues/8820) - Fix navbar background color in dark mode
* [#8850](https://github.com/netbox-community/netbox/issues/8850) - Show airflow field on device REST API serializer when config context data is included
diff --git a/netbox/circuits/filtersets.py b/netbox/circuits/filtersets.py
index 5a6a95785..701ff8174 100644
--- a/netbox/circuits/filtersets.py
+++ b/netbox/circuits/filtersets.py
@@ -5,7 +5,7 @@ from dcim.filtersets import CableTerminationFilterSet
from dcim.models import Region, Site, SiteGroup
from extras.filters import TagFilter
from netbox.filtersets import ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet
-from tenancy.filtersets import TenancyFilterSet
+from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
from utilities.filters import TreeNodeMultipleChoiceFilter
from .choices import *
from .models import *
@@ -19,7 +19,7 @@ __all__ = (
)
-class ProviderFilterSet(PrimaryModelFilterSet):
+class ProviderFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -118,7 +118,7 @@ class CircuitTypeFilterSet(OrganizationalModelFilterSet):
fields = ['id', 'name', 'slug', 'description']
-class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
+class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
diff --git a/netbox/circuits/forms/filtersets.py b/netbox/circuits/forms/filtersets.py
index a668f9b16..ee7a77572 100644
--- a/netbox/circuits/forms/filtersets.py
+++ b/netbox/circuits/forms/filtersets.py
@@ -5,7 +5,7 @@ from circuits.choices import CircuitStatusChoices
from circuits.models import *
from dcim.models import Region, Site, SiteGroup
from extras.forms import CustomFieldModelFilterForm
-from tenancy.forms import TenancyFilterForm
+from tenancy.forms import TenancyFilterForm, ContactModelFilterForm
from utilities.forms import DynamicModelMultipleChoiceField, StaticSelectMultiple, TagFilterField
__all__ = (
@@ -16,12 +16,13 @@ __all__ = (
)
-class ProviderFilterForm(CustomFieldModelFilterForm):
+class ProviderFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm):
model = Provider
field_groups = [
['q', 'tag'],
['region_id', 'site_group_id', 'site_id'],
['asn'],
+ ['contact', 'contact_role']
]
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
@@ -68,7 +69,7 @@ class CircuitTypeFilterForm(CustomFieldModelFilterForm):
tag = TagFilterField(model)
-class CircuitFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
+class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, CustomFieldModelFilterForm):
model = Circuit
field_groups = [
['q', 'tag'],
@@ -76,6 +77,7 @@ class CircuitFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
['type_id', 'status', 'commit_rate'],
['region_id', 'site_group_id', 'site_id'],
['tenant_group_id', 'tenant_id'],
+ ['contact', 'contact_role']
]
type_id = DynamicModelMultipleChoiceField(
queryset=CircuitType.objects.all(),
diff --git a/netbox/circuits/tables.py b/netbox/circuits/tables.py
index 889792be3..b4e0c7d2d 100644
--- a/netbox/circuits/tables.py
+++ b/netbox/circuits/tables.py
@@ -58,6 +58,9 @@ class ProviderTable(BaseTable):
verbose_name='Circuits'
)
comments = MarkdownColumn()
+ contacts = tables.ManyToManyColumn(
+ linkify_item=True
+ )
tags = TagColumn(
url_name='circuits:provider_list'
)
@@ -66,7 +69,7 @@ class ProviderTable(BaseTable):
model = Provider
fields = (
'pk', 'id', 'name', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'circuit_count',
- 'comments', 'tags', 'created', 'last_updated',
+ 'comments', 'contacts', 'tags', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'asn', 'account', 'circuit_count')
@@ -142,6 +145,9 @@ class CircuitTable(BaseTable):
)
commit_rate = CommitRateColumn()
comments = MarkdownColumn()
+ contacts = tables.ManyToManyColumn(
+ linkify_item=True
+ )
tags = TagColumn(
url_name='circuits:circuit_list'
)
@@ -150,7 +156,7 @@ class CircuitTable(BaseTable):
model = Circuit
fields = (
'pk', 'id', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'install_date',
- 'commit_rate', 'description', 'comments', 'tags', 'created', 'last_updated',
+ 'commit_rate', 'description', 'comments', 'contacts', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'description',
diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py
index 62326b289..504ad69ca 100644
--- a/netbox/dcim/filtersets.py
+++ b/netbox/dcim/filtersets.py
@@ -7,8 +7,8 @@ from ipam.models import ASN
from netbox.filtersets import (
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet,
)
-from tenancy.filtersets import TenancyFilterSet
-from tenancy.models import Tenant
+from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
+from tenancy.models import *
from utilities.choices import ColorChoices
from utilities.filters import (
ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter,
@@ -62,7 +62,7 @@ __all__ = (
)
-class RegionFilterSet(OrganizationalModelFilterSet):
+class RegionFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=Region.objects.all(),
label='Parent region (ID)',
@@ -80,7 +80,7 @@ class RegionFilterSet(OrganizationalModelFilterSet):
fields = ['id', 'name', 'slug', 'description']
-class SiteGroupFilterSet(OrganizationalModelFilterSet):
+class SiteGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
label='Parent site group (ID)',
@@ -98,7 +98,7 @@ class SiteGroupFilterSet(OrganizationalModelFilterSet):
fields = ['id', 'name', 'slug', 'description']
-class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
+class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -167,7 +167,7 @@ class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
return queryset.filter(qs_filter)
-class LocationFilterSet(TenancyFilterSet, OrganizationalModelFilterSet):
+class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalModelFilterSet):
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='site__region',
@@ -240,7 +240,7 @@ class RackRoleFilterSet(OrganizationalModelFilterSet):
fields = ['id', 'name', 'slug', 'color', 'description']
-class RackFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
+class RackFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -398,7 +398,7 @@ class RackReservationFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
)
-class ManufacturerFilterSet(OrganizationalModelFilterSet):
+class ManufacturerFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
tag = TagFilter()
class Meta:
@@ -608,7 +608,7 @@ class PlatformFilterSet(OrganizationalModelFilterSet):
fields = ['id', 'name', 'slug', 'napalm_driver', 'description']
-class DeviceFilterSet(PrimaryModelFilterSet, TenancyFilterSet, LocalConfigContextFilterSet):
+class DeviceFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet, LocalConfigContextFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -1289,7 +1289,7 @@ class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
return queryset
-class PowerPanelFilterSet(PrimaryModelFilterSet):
+class PowerPanelFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py
index f4b4c0a87..91d83ae53 100644
--- a/netbox/dcim/forms/filtersets.py
+++ b/netbox/dcim/forms/filtersets.py
@@ -5,9 +5,10 @@ from django.utils.translation import gettext as _
from dcim.choices import *
from dcim.constants import *
from dcim.models import *
+from tenancy.models import *
from extras.forms import CustomFieldModelFilterForm, LocalConfigContextFilterForm
from ipam.models import ASN
-from tenancy.forms import TenancyFilterForm
+from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
from utilities.forms import (
APISelectMultiple, add_blank_choice, ColorField, DynamicModelMultipleChoiceField, FilterForm, StaticSelect,
StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
@@ -98,8 +99,13 @@ class DeviceComponentFilterForm(CustomFieldModelFilterForm):
)
-class RegionFilterForm(CustomFieldModelFilterForm):
+class RegionFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm):
model = Region
+ field_groups = [
+ ['q', 'tag'],
+ ['parent_id'],
+ ['contact', 'contact_role'],
+ ]
parent_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
@@ -108,8 +114,13 @@ class RegionFilterForm(CustomFieldModelFilterForm):
tag = TagFilterField(model)
-class SiteGroupFilterForm(CustomFieldModelFilterForm):
+class SiteGroupFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm):
model = SiteGroup
+ field_groups = [
+ ['q', 'tag'],
+ ['parent_id'],
+ ['contact', 'contact_role'],
+ ]
parent_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
@@ -118,13 +129,14 @@ class SiteGroupFilterForm(CustomFieldModelFilterForm):
tag = TagFilterField(model)
-class SiteFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
+class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, CustomFieldModelFilterForm):
model = Site
field_groups = [
['q', 'tag'],
['status', 'region_id', 'group_id'],
['tenant_group_id', 'tenant_id'],
- ['asn_id']
+ ['asn_id'],
+ ['contact', 'contact_role'],
]
status = forms.MultipleChoiceField(
choices=SiteStatusChoices,
@@ -149,12 +161,13 @@ class SiteFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
tag = TagFilterField(model)
-class LocationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
+class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, CustomFieldModelFilterForm):
model = Location
field_groups = [
['q', 'tag'],
['region_id', 'site_group_id', 'site_id', 'parent_id'],
['tenant_group_id', 'tenant_id'],
+ ['contact', 'contact_role'],
]
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
@@ -192,7 +205,7 @@ class RackRoleFilterForm(CustomFieldModelFilterForm):
tag = TagFilterField(model)
-class RackFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
+class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, CustomFieldModelFilterForm):
model = Rack
field_groups = [
['q', 'tag'],
@@ -200,6 +213,7 @@ class RackFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
['status', 'role_id'],
['type', 'width', 'serial', 'asset_tag'],
['tenant_group_id', 'tenant_id'],
+ ['contact', 'contact_role']
]
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
@@ -303,8 +317,12 @@ class RackReservationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
tag = TagFilterField(model)
-class ManufacturerFilterForm(CustomFieldModelFilterForm):
+class ManufacturerFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm):
model = Manufacturer
+ field_groups = [
+ ['q', 'tag'],
+ ['contact', 'contact_role'],
+ ]
tag = TagFilterField(model)
@@ -390,7 +408,7 @@ class PlatformFilterForm(CustomFieldModelFilterForm):
tag = TagFilterField(model)
-class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, CustomFieldModelFilterForm):
+class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, ContactModelFilterForm, CustomFieldModelFilterForm):
model = Device
field_groups = [
['q', 'tag'],
@@ -402,6 +420,7 @@ class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, CustomFi
'has_primary_ip', 'virtual_chassis_member', 'console_ports', 'console_server_ports', 'power_ports',
'power_outlets', 'interfaces', 'pass_through_ports', 'local_context_data',
],
+ ['contact', 'contact_role'],
]
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
@@ -636,11 +655,12 @@ class CableFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
tag = TagFilterField(model)
-class PowerPanelFilterForm(CustomFieldModelFilterForm):
+class PowerPanelFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm):
model = PowerPanel
field_groups = (
('q', 'tag'),
- ('region_id', 'site_group_id', 'site_id', 'location_id')
+ ('region_id', 'site_group_id', 'site_id', 'location_id'),
+ ('contact', 'contact_role')
)
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
diff --git a/netbox/dcim/tables/cables.py b/netbox/dcim/tables/cables.py
index 9f2c08342..97b54bf41 100644
--- a/netbox/dcim/tables/cables.py
+++ b/netbox/dcim/tables/cables.py
@@ -23,6 +23,12 @@ class CableTable(BaseTable):
orderable=False,
verbose_name='Side A'
)
+ rack_a = tables.Column(
+ accessor=Accessor('termination_a__device__rack'),
+ orderable=False,
+ linkify=True,
+ verbose_name='Rack A'
+ )
termination_a = tables.Column(
accessor=Accessor('termination_a'),
orderable=False,
@@ -35,6 +41,12 @@ class CableTable(BaseTable):
orderable=False,
verbose_name='Side B'
)
+ rack_b = tables.Column(
+ accessor=Accessor('termination_b__device__rack'),
+ orderable=False,
+ linkify=True,
+ verbose_name='Rack B'
+ )
termination_b = tables.Column(
accessor=Accessor('termination_b'),
orderable=False,
@@ -55,7 +67,7 @@ class CableTable(BaseTable):
class Meta(BaseTable.Meta):
model = Cable
fields = (
- 'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b',
+ 'pk', 'id', 'label', 'termination_a_parent', 'rack_a', 'termination_a', 'termination_b_parent', 'rack_b', 'termination_b',
'status', 'type', 'tenant', 'color', 'length', 'tags', 'created', 'last_updated',
)
default_columns = (
diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py
index 3c2b3dace..debc074d0 100644
--- a/netbox/dcim/tables/devices.py
+++ b/netbox/dcim/tables/devices.py
@@ -194,6 +194,9 @@ class DeviceTable(BaseTable):
vc_priority = tables.Column(
verbose_name='VC Priority'
)
+ contacts = tables.ManyToManyColumn(
+ linkify_item=True
+ )
comments = MarkdownColumn()
tags = TagColumn(
url_name='dcim:device_list'
@@ -204,8 +207,8 @@ class DeviceTable(BaseTable):
fields = (
'pk', 'id', 'name', 'status', 'tenant', 'device_role', 'manufacturer', 'device_type', 'platform', 'serial',
'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'primary_ip', 'airflow', 'primary_ip4',
- 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags', 'created',
- 'last_updated',
+ 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'contacts', 'tags',
+ 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type',
diff --git a/netbox/dcim/tables/devicetypes.py b/netbox/dcim/tables/devicetypes.py
index 5643edc37..fde9ca61c 100644
--- a/netbox/dcim/tables/devicetypes.py
+++ b/netbox/dcim/tables/devicetypes.py
@@ -41,6 +41,9 @@ class ManufacturerTable(BaseTable):
verbose_name='Platforms'
)
slug = tables.Column()
+ contacts = tables.ManyToManyColumn(
+ linkify_item=True
+ )
tags = TagColumn(
url_name='dcim:manufacturer_list'
)
@@ -50,7 +53,7 @@ class ManufacturerTable(BaseTable):
model = Manufacturer
fields = (
'pk', 'id', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug',
- 'actions', 'created', 'last_updated',
+ 'contacts', 'actions', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug', 'actions',
diff --git a/netbox/dcim/tables/power.py b/netbox/dcim/tables/power.py
index c1ea8a34c..517a48aa1 100644
--- a/netbox/dcim/tables/power.py
+++ b/netbox/dcim/tables/power.py
@@ -27,13 +27,16 @@ class PowerPanelTable(BaseTable):
url_params={'power_panel_id': 'pk'},
verbose_name='Feeds'
)
+ contacts = tables.ManyToManyColumn(
+ linkify_item=True
+ )
tags = TagColumn(
url_name='dcim:powerpanel_list'
)
class Meta(BaseTable.Meta):
model = PowerPanel
- fields = ('pk', 'id', 'name', 'site', 'location', 'powerfeed_count', 'tags', 'created', 'last_updated',)
+ fields = ('pk', 'id', 'name', 'site', 'location', 'powerfeed_count', 'contacts', 'tags', 'created', 'last_updated',)
default_columns = ('pk', 'name', 'site', 'location', 'powerfeed_count')
diff --git a/netbox/dcim/tables/racks.py b/netbox/dcim/tables/racks.py
index dba28603c..4d2aac3dd 100644
--- a/netbox/dcim/tables/racks.py
+++ b/netbox/dcim/tables/racks.py
@@ -75,6 +75,9 @@ class RackTable(BaseTable):
orderable=False,
verbose_name='Power'
)
+ contacts = tables.ManyToManyColumn(
+ linkify_item=True
+ )
tags = TagColumn(
url_name='dcim:rack_list'
)
@@ -92,7 +95,7 @@ class RackTable(BaseTable):
fields = (
'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag',
'type', 'width', 'outer_width', 'outer_depth', 'u_height', 'comments', 'device_count', 'get_utilization',
- 'get_power_utilization', 'tags', 'created', 'last_updated',
+ 'get_power_utilization', 'contacts', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count',
diff --git a/netbox/dcim/tables/sites.py b/netbox/dcim/tables/sites.py
index bf4812cfa..b749315eb 100644
--- a/netbox/dcim/tables/sites.py
+++ b/netbox/dcim/tables/sites.py
@@ -29,6 +29,9 @@ class RegionTable(BaseTable):
url_params={'region_id': 'pk'},
verbose_name='Sites'
)
+ contacts = tables.ManyToManyColumn(
+ linkify_item=True
+ )
tags = TagColumn(
url_name='dcim:region_list'
)
@@ -36,7 +39,7 @@ class RegionTable(BaseTable):
class Meta(BaseTable.Meta):
model = Region
- fields = ('pk', 'id', 'name', 'slug', 'site_count', 'description', 'tags', 'actions', 'created', 'last_updated')
+ fields = ('pk', 'id', 'name', 'slug', 'site_count', 'description', 'contacts', 'tags', 'actions', 'created', 'last_updated')
default_columns = ('pk', 'name', 'site_count', 'description', 'actions')
@@ -54,6 +57,9 @@ class SiteGroupTable(BaseTable):
url_params={'group_id': 'pk'},
verbose_name='Sites'
)
+ contacts = tables.ManyToManyColumn(
+ linkify_item=True
+ )
tags = TagColumn(
url_name='dcim:sitegroup_list'
)
@@ -61,7 +67,7 @@ class SiteGroupTable(BaseTable):
class Meta(BaseTable.Meta):
model = SiteGroup
- fields = ('pk', 'id', 'name', 'slug', 'site_count', 'description', 'tags', 'actions', 'created', 'last_updated')
+ fields = ('pk', 'id', 'name', 'slug', 'site_count', 'description', 'contacts', 'tags', 'actions', 'created', 'last_updated')
default_columns = ('pk', 'name', 'site_count', 'description', 'actions')
@@ -92,6 +98,9 @@ class SiteTable(BaseTable):
verbose_name='ASNs'
)
tenant = TenantColumn()
+ contacts = tables.ManyToManyColumn(
+ linkify_item=True
+ )
comments = MarkdownColumn()
tags = TagColumn(
url_name='dcim:site_list'
@@ -102,7 +111,7 @@ class SiteTable(BaseTable):
fields = (
'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asns', 'asn_count',
'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name',
- 'contact_phone', 'contact_email', 'comments', 'tags', 'created', 'last_updated',
+ 'contact_phone', 'contact_email', 'contacts', 'comments', 'tags', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'status', 'facility', 'region', 'group', 'tenant', 'description')
@@ -130,6 +139,9 @@ class LocationTable(BaseTable):
url_params={'location_id': 'pk'},
verbose_name='Devices'
)
+ contacts = tables.ManyToManyColumn(
+ linkify_item=True
+ )
tags = TagColumn(
url_name='dcim:location_list'
)
@@ -141,7 +153,7 @@ class LocationTable(BaseTable):
class Meta(BaseTable.Meta):
model = Location
fields = (
- 'pk', 'id', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'tags',
- 'actions', 'created', 'last_updated',
+ 'pk', 'id', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'contacts',
+ 'tags', 'actions', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'actions')
diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py
index cee516f5c..87c4828d5 100644
--- a/netbox/dcim/views.py
+++ b/netbox/dcim/views.py
@@ -328,6 +328,11 @@ class SiteView(generic.ObjectView):
'device_count',
cumulative=True
).restrict(request.user, 'view').filter(site=instance)
+ nonracked_devices = Device.objects.filter(
+ site=instance,
+ position__isnull=True,
+ parent_bay__isnull=True
+ ).prefetch_related('device_type__manufacturer')
asns = ASN.objects.restrict(request.user, 'view').filter(sites=instance)
asn_count = asns.count()
@@ -338,6 +343,7 @@ class SiteView(generic.ObjectView):
'stats': stats,
'locations': locations,
'asns': asns,
+ 'nonracked_devices': nonracked_devices,
}
@@ -415,11 +421,17 @@ class LocationView(generic.ObjectView):
).filter(pk__in=location_ids).exclude(pk=instance.pk)
child_locations_table = tables.LocationTable(child_locations)
paginate_table(child_locations_table, request)
+ nonracked_devices = Device.objects.filter(
+ location=instance,
+ position__isnull=True,
+ parent_bay__isnull=True
+ ).prefetch_related('device_type__manufacturer')
return {
'rack_count': rack_count,
'device_count': device_count,
'child_locations_table': child_locations_table,
+ 'nonracked_devices': nonracked_devices,
}
diff --git a/netbox/templates/base/layout.html b/netbox/templates/base/layout.html
index da2d10c65..0def1c90e 100644
--- a/netbox/templates/base/layout.html
+++ b/netbox/templates/base/layout.html
@@ -33,7 +33,7 @@
- {% search_options %}
+ {% search_options request %}
@@ -45,7 +45,7 @@
{# Search bar #}
- {% search_options %}
+ {% search_options request %}
{# Proflie/login button #}
diff --git a/netbox/templates/dcim/inc/cable_termination.html b/netbox/templates/dcim/inc/cable_termination.html
index 1ba3d05c9..c9f3f0d4a 100644
--- a/netbox/templates/dcim/inc/cable_termination.html
+++ b/netbox/templates/dcim/inc/cable_termination.html
@@ -8,6 +8,22 @@
{{ termination.device }}
+ {% if termination.device.site %}
+
+ Site
+
+ {{ termination.device.site }}
+
+
+ {% endif %}
+ {% if termination.device.rack %}
+
+ Rack
+
+ {{ termination.device.rack }}
+
+
+ {% endif %}
Type
diff --git a/netbox/templates/dcim/inc/nonracked_devices.html b/netbox/templates/dcim/inc/nonracked_devices.html
new file mode 100644
index 000000000..f1b669eb9
--- /dev/null
+++ b/netbox/templates/dcim/inc/nonracked_devices.html
@@ -0,0 +1,62 @@
+{% load helpers %}
+
+
+
+
+{% if nonracked_devices %}
+
+
+ Name
+ Role
+ Type
+ Parent Device
+
+ {% for device in nonracked_devices %}
+
+
+ {{ device }}
+
+ {{ device.device_role }}
+ {{ device.device_type }}
+ {% if device.parent_bay %}
+ {{ device.parent_bay.device }}
+ {{ device.parent_bay }}
+ {% else %}
+ —
+ {% endif %}
+
+ {% endfor %}
+
+ {% else %}
+
+ None
+
+ {% endif %}
+
+ {% if perms.dcim.add_device %}
+ {% if object|meta:'verbose_name' == 'rack' %}
+
+ {% elif object|meta:'verbose_name' == 'site' %}
+
+ {% elif object|meta:'verbose_name' == 'location' %}
+
+ {% endif %}
+ {% endif %}
+
\ No newline at end of file
diff --git a/netbox/templates/dcim/location.html b/netbox/templates/dcim/location.html
index b684385a7..43bbfd114 100644
--- a/netbox/templates/dcim/location.html
+++ b/netbox/templates/dcim/location.html
@@ -90,6 +90,7 @@
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/contacts.html' %}
+ {% include 'dcim/inc/nonracked_devices.html' %}
{% include 'inc/panels/image_attachments.html' %}
{% plugin_right_page object %}
diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html
index 93bd21fd9..4eb94a0ce 100644
--- a/netbox/templates/dcim/rack.html
+++ b/netbox/templates/dcim/rack.html
@@ -288,50 +288,7 @@
-
-
-
- {% if nonracked_devices %}
-
-
- Name
- Role
- Type
- Parent Device
-
- {% for device in nonracked_devices %}
-
-
- {{ device }}
-
- {{ device.device_role }}
- {{ device.device_type }}
- {% if device.parent_bay %}
- {{ device.parent_bay.device }}
- {{ device.parent_bay }}
- {% else %}
- —
- {% endif %}
-
- {% endfor %}
-
- {% else %}
-
- None
-
- {% endif %}
-
- {% if perms.dcim.add_device %}
-
- {% endif %}
-
+ {% include 'dcim/inc/nonracked_devices.html' %}
{% include 'inc/panels/contacts.html' %}
{% plugin_right_page object %}
diff --git a/netbox/templates/dcim/site.html b/netbox/templates/dcim/site.html
index f71105d1b..aa17fd57f 100644
--- a/netbox/templates/dcim/site.html
+++ b/netbox/templates/dcim/site.html
@@ -277,6 +277,7 @@
+ {% include 'dcim/inc/nonracked_devices.html' %}
{% include 'inc/panels/contacts.html' %}
diff --git a/netbox/tenancy/filtersets.py b/netbox/tenancy/filtersets.py
index c7e766389..3ff45ab5c 100644
--- a/netbox/tenancy/filtersets.py
+++ b/netbox/tenancy/filtersets.py
@@ -11,6 +11,7 @@ __all__ = (
'ContactAssignmentFilterSet',
'ContactFilterSet',
'ContactGroupFilterSet',
+ 'ContactModelFilterSet',
'ContactRoleFilterSet',
'TenancyFilterSet',
'TenantFilterSet',
@@ -18,92 +19,6 @@ __all__ = (
)
-#
-# Tenancy
-#
-
-class TenantGroupFilterSet(OrganizationalModelFilterSet):
- parent_id = django_filters.ModelMultipleChoiceFilter(
- queryset=TenantGroup.objects.all(),
- label='Tenant group (ID)',
- )
- parent = django_filters.ModelMultipleChoiceFilter(
- field_name='parent__slug',
- queryset=TenantGroup.objects.all(),
- to_field_name='slug',
- label='Tenant group (slug)',
- )
- tag = TagFilter()
-
- class Meta:
- model = TenantGroup
- fields = ['id', 'name', 'slug', 'description']
-
-
-class TenantFilterSet(PrimaryModelFilterSet):
- q = django_filters.CharFilter(
- method='search',
- label='Search',
- )
- group_id = TreeNodeMultipleChoiceFilter(
- queryset=TenantGroup.objects.all(),
- field_name='group',
- lookup_expr='in',
- label='Tenant group (ID)',
- )
- group = TreeNodeMultipleChoiceFilter(
- queryset=TenantGroup.objects.all(),
- field_name='group',
- lookup_expr='in',
- to_field_name='slug',
- label='Tenant group (slug)',
- )
- tag = TagFilter()
-
- class Meta:
- model = Tenant
- fields = ['id', 'name', 'slug', 'description']
-
- def search(self, queryset, name, value):
- if not value.strip():
- return queryset
- return queryset.filter(
- Q(name__icontains=value) |
- Q(slug__icontains=value) |
- Q(description__icontains=value) |
- Q(comments__icontains=value)
- )
-
-
-class TenancyFilterSet(django_filters.FilterSet):
- """
- An inheritable FilterSet for models which support Tenant assignment.
- """
- tenant_group_id = TreeNodeMultipleChoiceFilter(
- queryset=TenantGroup.objects.all(),
- field_name='tenant__group',
- lookup_expr='in',
- label='Tenant Group (ID)',
- )
- tenant_group = TreeNodeMultipleChoiceFilter(
- queryset=TenantGroup.objects.all(),
- field_name='tenant__group',
- to_field_name='slug',
- lookup_expr='in',
- label='Tenant Group (slug)',
- )
- tenant_id = django_filters.ModelMultipleChoiceFilter(
- queryset=Tenant.objects.all(),
- label='Tenant (ID)',
- )
- tenant = django_filters.ModelMultipleChoiceFilter(
- queryset=Tenant.objects.all(),
- field_name='tenant__slug',
- to_field_name='slug',
- label='Tenant (slug)',
- )
-
-
#
# Contacts
#
@@ -191,3 +106,102 @@ class ContactAssignmentFilterSet(ChangeLoggedModelFilterSet):
class Meta:
model = ContactAssignment
fields = ['id', 'content_type_id', 'object_id', 'priority']
+
+
+class ContactModelFilterSet(django_filters.FilterSet):
+ contact = django_filters.ModelMultipleChoiceFilter(
+ field_name='contacts__contact',
+ queryset=Contact.objects.all(),
+ label='Contact',
+ )
+ contact_role = django_filters.ModelMultipleChoiceFilter(
+ field_name='contacts__role',
+ queryset=ContactRole.objects.all(),
+ label='Contact Role'
+ )
+
+
+#
+# Tenancy
+#
+
+class TenantGroupFilterSet(OrganizationalModelFilterSet):
+ parent_id = django_filters.ModelMultipleChoiceFilter(
+ queryset=TenantGroup.objects.all(),
+ label='Tenant group (ID)',
+ )
+ parent = django_filters.ModelMultipleChoiceFilter(
+ field_name='parent__slug',
+ queryset=TenantGroup.objects.all(),
+ to_field_name='slug',
+ label='Tenant group (slug)',
+ )
+ tag = TagFilter()
+
+ class Meta:
+ model = TenantGroup
+ fields = ['id', 'name', 'slug', 'description']
+
+
+class TenantFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
+ q = django_filters.CharFilter(
+ method='search',
+ label='Search',
+ )
+ group_id = TreeNodeMultipleChoiceFilter(
+ queryset=TenantGroup.objects.all(),
+ field_name='group',
+ lookup_expr='in',
+ label='Tenant group (ID)',
+ )
+ group = TreeNodeMultipleChoiceFilter(
+ queryset=TenantGroup.objects.all(),
+ field_name='group',
+ lookup_expr='in',
+ to_field_name='slug',
+ label='Tenant group (slug)',
+ )
+ tag = TagFilter()
+
+ class Meta:
+ model = Tenant
+ fields = ['id', 'name', 'slug', 'description']
+
+ def search(self, queryset, name, value):
+ if not value.strip():
+ return queryset
+ return queryset.filter(
+ Q(name__icontains=value) |
+ Q(slug__icontains=value) |
+ Q(description__icontains=value) |
+ Q(comments__icontains=value)
+ )
+
+
+class TenancyFilterSet(django_filters.FilterSet):
+ """
+ An inheritable FilterSet for models which support Tenant assignment.
+ """
+ tenant_group_id = TreeNodeMultipleChoiceFilter(
+ queryset=TenantGroup.objects.all(),
+ field_name='tenant__group',
+ lookup_expr='in',
+ label='Tenant Group (ID)',
+ )
+ tenant_group = TreeNodeMultipleChoiceFilter(
+ queryset=TenantGroup.objects.all(),
+ field_name='tenant__group',
+ to_field_name='slug',
+ lookup_expr='in',
+ label='Tenant Group (slug)',
+ )
+ tenant_id = django_filters.ModelMultipleChoiceFilter(
+ queryset=Tenant.objects.all(),
+ label='Tenant (ID)',
+ )
+ tenant = django_filters.ModelMultipleChoiceFilter(
+ queryset=Tenant.objects.all(),
+ field_name='tenant__slug',
+ to_field_name='slug',
+ label='Tenant (slug)',
+ )
diff --git a/netbox/tenancy/forms/filtersets.py b/netbox/tenancy/forms/filtersets.py
index 7849e2171..ada279d9d 100644
--- a/netbox/tenancy/forms/filtersets.py
+++ b/netbox/tenancy/forms/filtersets.py
@@ -2,6 +2,7 @@ from django.utils.translation import gettext as _
from extras.forms import CustomFieldModelFilterForm
from tenancy.models import *
+from tenancy.forms import ContactModelFilterForm
from utilities.forms import DynamicModelMultipleChoiceField, TagFilterField
__all__ = (
@@ -27,11 +28,12 @@ class TenantGroupFilterForm(CustomFieldModelFilterForm):
tag = TagFilterField(model)
-class TenantFilterForm(CustomFieldModelFilterForm):
+class TenantFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm):
model = Tenant
field_groups = (
('q', 'tag'),
('group_id',),
+ ('contact', 'contact_role')
)
group_id = DynamicModelMultipleChoiceField(
queryset=TenantGroup.objects.all(),
diff --git a/netbox/tenancy/forms/forms.py b/netbox/tenancy/forms/forms.py
index 9a3d00e05..5dcad1d43 100644
--- a/netbox/tenancy/forms/forms.py
+++ b/netbox/tenancy/forms/forms.py
@@ -1,10 +1,11 @@
from django import forms
from django.utils.translation import gettext as _
-from tenancy.models import Tenant, TenantGroup
+from tenancy.models import *
from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField
__all__ = (
+ 'ContactModelFilterForm',
'TenancyForm',
'TenancyFilterForm',
)
@@ -44,3 +45,16 @@ class TenancyFilterForm(forms.Form):
},
label=_('Tenant')
)
+
+
+class ContactModelFilterForm(forms.Form):
+ contact = DynamicModelMultipleChoiceField(
+ queryset=Contact.objects.all(),
+ required=False,
+ label=_('Contact')
+ )
+ contact_role = DynamicModelMultipleChoiceField(
+ queryset=ContactRole.objects.all(),
+ required=False,
+ label=_('Contact Role')
+ )
diff --git a/netbox/tenancy/models/contacts.py b/netbox/tenancy/models/contacts.py
index 42a7ffe7d..49e690fd3 100644
--- a/netbox/tenancy/models/contacts.py
+++ b/netbox/tenancy/models/contacts.py
@@ -166,3 +166,6 @@ class ContactAssignment(ChangeLoggedModel):
if self.priority:
return f"{self.contact} ({self.get_priority_display()})"
return str(self.contact)
+
+ def get_absolute_url(self):
+ return reverse('tenancy:contact', args=[self.contact.pk])
diff --git a/netbox/tenancy/tables.py b/netbox/tenancy/tables.py
index 11893481c..6b0d9fc9e 100644
--- a/netbox/tenancy/tables.py
+++ b/netbox/tenancy/tables.py
@@ -77,6 +77,9 @@ class TenantTable(BaseTable):
group = tables.Column(
linkify=True
)
+ contacts = tables.ManyToManyColumn(
+ linkify_item=True
+ )
comments = MarkdownColumn()
tags = TagColumn(
url_name='tenancy:tenant_list'
@@ -84,7 +87,7 @@ class TenantTable(BaseTable):
class Meta(BaseTable.Meta):
model = Tenant
- fields = ('pk', 'id', 'name', 'slug', 'group', 'description', 'comments', 'tags', 'created', 'last_updated',)
+ fields = ('pk', 'id', 'name', 'slug', 'group', 'description', 'comments', 'contacts', 'tags', 'created', 'last_updated',)
default_columns = ('pk', 'name', 'group', 'description')
diff --git a/netbox/utilities/templates/search/searchbar.html b/netbox/utilities/templates/search/searchbar.html
index d71fd8e69..74d12e9b9 100644
--- a/netbox/utilities/templates/search/searchbar.html
+++ b/netbox/utilities/templates/search/searchbar.html
@@ -5,7 +5,7 @@
aria-label="Search"
placeholder="Search"
class="form-control"
- value="{{ request.GET.q }}"
+ value="{{ request.GET.q|escape }}"
/>
diff --git a/netbox/utilities/templatetags/search.py b/netbox/utilities/templatetags/search.py
index aad533e7e..5726ae5d5 100644
--- a/netbox/utilities/templatetags/search.py
+++ b/netbox/utilities/templatetags/search.py
@@ -8,6 +8,9 @@ search_form = SearchForm()
@register.inclusion_tag("search/searchbar.html")
-def search_options() -> Dict:
+def search_options(request) -> Dict:
"""Provide search options to template."""
- return {"options": search_form.options}
+ return {
+ 'options': search_form.options,
+ 'request': request,
+ }
diff --git a/netbox/virtualization/filtersets.py b/netbox/virtualization/filtersets.py
index d9f34d619..d7fc28f6c 100644
--- a/netbox/virtualization/filtersets.py
+++ b/netbox/virtualization/filtersets.py
@@ -5,7 +5,7 @@ from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup
from extras.filters import TagFilter
from extras.filtersets import LocalConfigContextFilterSet
from netbox.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet
-from tenancy.filtersets import TenancyFilterSet
+from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
from utilities.filters import MultiValueMACAddressFilter, TreeNodeMultipleChoiceFilter
from .choices import *
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
@@ -27,7 +27,7 @@ class ClusterTypeFilterSet(OrganizationalModelFilterSet):
fields = ['id', 'name', 'slug', 'description']
-class ClusterGroupFilterSet(OrganizationalModelFilterSet):
+class ClusterGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
tag = TagFilter()
class Meta:
@@ -35,7 +35,7 @@ class ClusterGroupFilterSet(OrganizationalModelFilterSet):
fields = ['id', 'name', 'slug', 'description']
-class ClusterFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
+class ClusterFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -111,7 +111,7 @@ class ClusterFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
)
-class VirtualMachineFilterSet(PrimaryModelFilterSet, TenancyFilterSet, LocalConfigContextFilterSet):
+class VirtualMachineFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet, LocalConfigContextFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
diff --git a/netbox/virtualization/forms/filtersets.py b/netbox/virtualization/forms/filtersets.py
index 9ca8eba6e..908fa17c8 100644
--- a/netbox/virtualization/forms/filtersets.py
+++ b/netbox/virtualization/forms/filtersets.py
@@ -3,7 +3,7 @@ from django.utils.translation import gettext as _
from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup
from extras.forms import CustomFieldModelFilterForm, LocalConfigContextFilterForm
-from tenancy.forms import TenancyFilterForm
+from tenancy.forms import TenancyFilterForm, ContactModelFilterForm
from utilities.forms import (
DynamicModelMultipleChoiceField, StaticSelect, StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
)
@@ -24,18 +24,19 @@ class ClusterTypeFilterForm(CustomFieldModelFilterForm):
tag = TagFilterField(model)
-class ClusterGroupFilterForm(CustomFieldModelFilterForm):
+class ClusterGroupFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm):
model = ClusterGroup
tag = TagFilterField(model)
-class ClusterFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
+class ClusterFilterForm(TenancyFilterForm, ContactModelFilterForm, CustomFieldModelFilterForm):
model = Cluster
field_groups = [
['q', 'tag'],
['group_id', 'type_id'],
['region_id', 'site_group_id', 'site_id'],
['tenant_group_id', 'tenant_id'],
+ ['contact', 'contact_role'],
]
type_id = DynamicModelMultipleChoiceField(
queryset=ClusterType.objects.all(),
@@ -71,7 +72,7 @@ class ClusterFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
tag = TagFilterField(model)
-class VirtualMachineFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, CustomFieldModelFilterForm):
+class VirtualMachineFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, ContactModelFilterForm, CustomFieldModelFilterForm):
model = VirtualMachine
field_groups = [
['q', 'tag'],
@@ -79,6 +80,7 @@ class VirtualMachineFilterForm(LocalConfigContextFilterForm, TenancyFilterForm,
['region_id', 'site_group_id', 'site_id'],
['status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'local_context_data'],
['tenant_group_id', 'tenant_id'],
+ ['contact', 'contact_role'],
]
cluster_group_id = DynamicModelMultipleChoiceField(
queryset=ClusterGroup.objects.all(),
diff --git a/netbox/virtualization/tables.py b/netbox/virtualization/tables.py
index dfa46047e..afc1d038b 100644
--- a/netbox/virtualization/tables.py
+++ b/netbox/virtualization/tables.py
@@ -62,6 +62,9 @@ class ClusterGroupTable(BaseTable):
cluster_count = tables.Column(
verbose_name='Clusters'
)
+ contacts = tables.ManyToManyColumn(
+ linkify_item=True
+ )
tags = TagColumn(
url_name='virtualization:clustergroup_list'
)
@@ -70,7 +73,7 @@ class ClusterGroupTable(BaseTable):
class Meta(BaseTable.Meta):
model = ClusterGroup
fields = (
- 'pk', 'id', 'name', 'slug', 'cluster_count', 'description', 'tags', 'actions', 'created', 'last_updated',
+ 'pk', 'id', 'name', 'slug', 'cluster_count', 'description', 'contacts', 'tags', 'actions', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'cluster_count', 'description', 'actions')
@@ -106,6 +109,9 @@ class ClusterTable(BaseTable):
url_params={'cluster_id': 'pk'},
verbose_name='VMs'
)
+ contacts = tables.ManyToManyColumn(
+ linkify_item=True
+ )
comments = MarkdownColumn()
tags = TagColumn(
url_name='virtualization:cluster_list'
@@ -114,7 +120,7 @@ class ClusterTable(BaseTable):
class Meta(BaseTable.Meta):
model = Cluster
fields = (
- 'pk', 'id', 'name', 'type', 'group', 'tenant', 'site', 'comments', 'device_count', 'vm_count', 'tags',
+ 'pk', 'id', 'name', 'type', 'group', 'tenant', 'site', 'comments', 'device_count', 'vm_count', 'contacts', 'tags',
'created', 'last_updated',
)
default_columns = ('pk', 'name', 'type', 'group', 'tenant', 'site', 'device_count', 'vm_count')
@@ -150,6 +156,9 @@ class VirtualMachineTable(BaseTable):
order_by=('primary_ip4', 'primary_ip6'),
verbose_name='IP Address'
)
+ contacts = tables.ManyToManyColumn(
+ linkify_item=True
+ )
tags = TagColumn(
url_name='virtualization:virtualmachine_list'
)
@@ -158,7 +167,7 @@ class VirtualMachineTable(BaseTable):
model = VirtualMachine
fields = (
'pk', 'id', 'name', 'status', 'cluster', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk',
- 'primary_ip4', 'primary_ip6', 'primary_ip', 'comments', 'tags', 'created', 'last_updated',
+ 'primary_ip4', 'primary_ip6', 'primary_ip', 'comments', 'contacts', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'status', 'cluster', 'role', 'tenant', 'vcpus', 'memory', 'disk', 'primary_ip',