Merge branch 'develop' into develop

This commit is contained in:
PieterL75 2022-03-21 11:51:23 +01:00 committed by GitHub
commit 010e0d5ac0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 357 additions and 190 deletions

View File

@ -56,7 +56,7 @@ jobs:
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install -r requirements.txt pip install -r requirements.txt
pip install pycodestyle coverage pip install pycodestyle coverage tblib
ln -s configuration.testing.py netbox/netbox/configuration.py ln -s configuration.testing.py netbox/netbox/configuration.py
- name: Build documentation - name: Build documentation

View File

@ -5,10 +5,14 @@
### Enhancements ### Enhancements
* [#8233](https://github.com/netbox-community/netbox/issues/8233) - Restrict API key usage by source IP * [#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 * [#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 ### 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 * [#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 * [#8850](https://github.com/netbox-community/netbox/issues/8850) - Show airflow field on device REST API serializer when config context data is included

View File

@ -5,7 +5,7 @@ from dcim.filtersets import CableTerminationFilterSet
from dcim.models import Region, Site, SiteGroup from dcim.models import Region, Site, SiteGroup
from extras.filters import TagFilter from extras.filters import TagFilter
from netbox.filtersets import ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet from netbox.filtersets import ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet
from tenancy.filtersets import TenancyFilterSet from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
from utilities.filters import TreeNodeMultipleChoiceFilter from utilities.filters import TreeNodeMultipleChoiceFilter
from .choices import * from .choices import *
from .models import * from .models import *
@ -19,7 +19,7 @@ __all__ = (
) )
class ProviderFilterSet(PrimaryModelFilterSet): class ProviderFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@ -118,7 +118,7 @@ class CircuitTypeFilterSet(OrganizationalModelFilterSet):
fields = ['id', 'name', 'slug', 'description'] fields = ['id', 'name', 'slug', 'description']
class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet): class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',

View File

@ -5,7 +5,7 @@ from circuits.choices import CircuitStatusChoices
from circuits.models import * from circuits.models import *
from dcim.models import Region, Site, SiteGroup from dcim.models import Region, Site, SiteGroup
from extras.forms import CustomFieldModelFilterForm from extras.forms import CustomFieldModelFilterForm
from tenancy.forms import TenancyFilterForm from tenancy.forms import TenancyFilterForm, ContactModelFilterForm
from utilities.forms import DynamicModelMultipleChoiceField, StaticSelectMultiple, TagFilterField from utilities.forms import DynamicModelMultipleChoiceField, StaticSelectMultiple, TagFilterField
__all__ = ( __all__ = (
@ -16,12 +16,13 @@ __all__ = (
) )
class ProviderFilterForm(CustomFieldModelFilterForm): class ProviderFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm):
model = Provider model = Provider
field_groups = [ field_groups = [
['q', 'tag'], ['q', 'tag'],
['region_id', 'site_group_id', 'site_id'], ['region_id', 'site_group_id', 'site_id'],
['asn'], ['asn'],
['contact', 'contact_role']
] ]
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
@ -68,7 +69,7 @@ class CircuitTypeFilterForm(CustomFieldModelFilterForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class CircuitFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, CustomFieldModelFilterForm):
model = Circuit model = Circuit
field_groups = [ field_groups = [
['q', 'tag'], ['q', 'tag'],
@ -76,6 +77,7 @@ class CircuitFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
['type_id', 'status', 'commit_rate'], ['type_id', 'status', 'commit_rate'],
['region_id', 'site_group_id', 'site_id'], ['region_id', 'site_group_id', 'site_id'],
['tenant_group_id', 'tenant_id'], ['tenant_group_id', 'tenant_id'],
['contact', 'contact_role']
] ]
type_id = DynamicModelMultipleChoiceField( type_id = DynamicModelMultipleChoiceField(
queryset=CircuitType.objects.all(), queryset=CircuitType.objects.all(),

View File

@ -58,6 +58,9 @@ class ProviderTable(BaseTable):
verbose_name='Circuits' verbose_name='Circuits'
) )
comments = MarkdownColumn() comments = MarkdownColumn()
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = TagColumn( tags = TagColumn(
url_name='circuits:provider_list' url_name='circuits:provider_list'
) )
@ -66,7 +69,7 @@ class ProviderTable(BaseTable):
model = Provider model = Provider
fields = ( fields = (
'pk', 'id', 'name', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'circuit_count', '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') default_columns = ('pk', 'name', 'asn', 'account', 'circuit_count')
@ -142,6 +145,9 @@ class CircuitTable(BaseTable):
) )
commit_rate = CommitRateColumn() commit_rate = CommitRateColumn()
comments = MarkdownColumn() comments = MarkdownColumn()
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = TagColumn( tags = TagColumn(
url_name='circuits:circuit_list' url_name='circuits:circuit_list'
) )
@ -150,7 +156,7 @@ class CircuitTable(BaseTable):
model = Circuit model = Circuit
fields = ( fields = (
'pk', 'id', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'install_date', '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 = ( default_columns = (
'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'description', 'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'description',

View File

@ -7,8 +7,8 @@ from ipam.models import ASN
from netbox.filtersets import ( from netbox.filtersets import (
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet, BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet,
) )
from tenancy.filtersets import TenancyFilterSet from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
from tenancy.models import Tenant from tenancy.models import *
from utilities.choices import ColorChoices from utilities.choices import ColorChoices
from utilities.filters import ( from utilities.filters import (
ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter, ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter,
@ -62,7 +62,7 @@ __all__ = (
) )
class RegionFilterSet(OrganizationalModelFilterSet): class RegionFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
parent_id = django_filters.ModelMultipleChoiceFilter( parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
label='Parent region (ID)', label='Parent region (ID)',
@ -80,7 +80,7 @@ class RegionFilterSet(OrganizationalModelFilterSet):
fields = ['id', 'name', 'slug', 'description'] fields = ['id', 'name', 'slug', 'description']
class SiteGroupFilterSet(OrganizationalModelFilterSet): class SiteGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
parent_id = django_filters.ModelMultipleChoiceFilter( parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=SiteGroup.objects.all(), queryset=SiteGroup.objects.all(),
label='Parent site group (ID)', label='Parent site group (ID)',
@ -98,7 +98,7 @@ class SiteGroupFilterSet(OrganizationalModelFilterSet):
fields = ['id', 'name', 'slug', 'description'] fields = ['id', 'name', 'slug', 'description']
class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet): class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@ -167,7 +167,7 @@ class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
class LocationFilterSet(TenancyFilterSet, OrganizationalModelFilterSet): class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalModelFilterSet):
region_id = TreeNodeMultipleChoiceFilter( region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
field_name='site__region', field_name='site__region',
@ -240,7 +240,7 @@ class RackRoleFilterSet(OrganizationalModelFilterSet):
fields = ['id', 'name', 'slug', 'color', 'description'] fields = ['id', 'name', 'slug', 'color', 'description']
class RackFilterSet(PrimaryModelFilterSet, TenancyFilterSet): class RackFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@ -398,7 +398,7 @@ class RackReservationFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
) )
class ManufacturerFilterSet(OrganizationalModelFilterSet): class ManufacturerFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
tag = TagFilter() tag = TagFilter()
class Meta: class Meta:
@ -608,7 +608,7 @@ class PlatformFilterSet(OrganizationalModelFilterSet):
fields = ['id', 'name', 'slug', 'napalm_driver', 'description'] fields = ['id', 'name', 'slug', 'napalm_driver', 'description']
class DeviceFilterSet(PrimaryModelFilterSet, TenancyFilterSet, LocalConfigContextFilterSet): class DeviceFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet, LocalConfigContextFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@ -1289,7 +1289,7 @@ class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
return queryset return queryset
class PowerPanelFilterSet(PrimaryModelFilterSet): class PowerPanelFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',

View File

@ -5,9 +5,10 @@ from django.utils.translation import gettext as _
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.models import * from dcim.models import *
from tenancy.models import *
from extras.forms import CustomFieldModelFilterForm, LocalConfigContextFilterForm from extras.forms import CustomFieldModelFilterForm, LocalConfigContextFilterForm
from ipam.models import ASN from ipam.models import ASN
from tenancy.forms import TenancyFilterForm from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
from utilities.forms import ( from utilities.forms import (
APISelectMultiple, add_blank_choice, ColorField, DynamicModelMultipleChoiceField, FilterForm, StaticSelect, APISelectMultiple, add_blank_choice, ColorField, DynamicModelMultipleChoiceField, FilterForm, StaticSelect,
StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
@ -98,8 +99,13 @@ class DeviceComponentFilterForm(CustomFieldModelFilterForm):
) )
class RegionFilterForm(CustomFieldModelFilterForm): class RegionFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm):
model = Region model = Region
field_groups = [
['q', 'tag'],
['parent_id'],
['contact', 'contact_role'],
]
parent_id = DynamicModelMultipleChoiceField( parent_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
required=False, required=False,
@ -108,8 +114,13 @@ class RegionFilterForm(CustomFieldModelFilterForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class SiteGroupFilterForm(CustomFieldModelFilterForm): class SiteGroupFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm):
model = SiteGroup model = SiteGroup
field_groups = [
['q', 'tag'],
['parent_id'],
['contact', 'contact_role'],
]
parent_id = DynamicModelMultipleChoiceField( parent_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(), queryset=SiteGroup.objects.all(),
required=False, required=False,
@ -118,13 +129,14 @@ class SiteGroupFilterForm(CustomFieldModelFilterForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class SiteFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, CustomFieldModelFilterForm):
model = Site model = Site
field_groups = [ field_groups = [
['q', 'tag'], ['q', 'tag'],
['status', 'region_id', 'group_id'], ['status', 'region_id', 'group_id'],
['tenant_group_id', 'tenant_id'], ['tenant_group_id', 'tenant_id'],
['asn_id'] ['asn_id'],
['contact', 'contact_role'],
] ]
status = forms.MultipleChoiceField( status = forms.MultipleChoiceField(
choices=SiteStatusChoices, choices=SiteStatusChoices,
@ -149,12 +161,13 @@ class SiteFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class LocationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, CustomFieldModelFilterForm):
model = Location model = Location
field_groups = [ field_groups = [
['q', 'tag'], ['q', 'tag'],
['region_id', 'site_group_id', 'site_id', 'parent_id'], ['region_id', 'site_group_id', 'site_id', 'parent_id'],
['tenant_group_id', 'tenant_id'], ['tenant_group_id', 'tenant_id'],
['contact', 'contact_role'],
] ]
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
@ -192,7 +205,7 @@ class RackRoleFilterForm(CustomFieldModelFilterForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class RackFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, CustomFieldModelFilterForm):
model = Rack model = Rack
field_groups = [ field_groups = [
['q', 'tag'], ['q', 'tag'],
@ -200,6 +213,7 @@ class RackFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
['status', 'role_id'], ['status', 'role_id'],
['type', 'width', 'serial', 'asset_tag'], ['type', 'width', 'serial', 'asset_tag'],
['tenant_group_id', 'tenant_id'], ['tenant_group_id', 'tenant_id'],
['contact', 'contact_role']
] ]
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
@ -303,8 +317,12 @@ class RackReservationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class ManufacturerFilterForm(CustomFieldModelFilterForm): class ManufacturerFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm):
model = Manufacturer model = Manufacturer
field_groups = [
['q', 'tag'],
['contact', 'contact_role'],
]
tag = TagFilterField(model) tag = TagFilterField(model)
@ -390,7 +408,7 @@ class PlatformFilterForm(CustomFieldModelFilterForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, CustomFieldModelFilterForm): class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, ContactModelFilterForm, CustomFieldModelFilterForm):
model = Device model = Device
field_groups = [ field_groups = [
['q', 'tag'], ['q', 'tag'],
@ -402,6 +420,7 @@ class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, CustomFi
'has_primary_ip', 'virtual_chassis_member', 'console_ports', 'console_server_ports', 'power_ports', 'has_primary_ip', 'virtual_chassis_member', 'console_ports', 'console_server_ports', 'power_ports',
'power_outlets', 'interfaces', 'pass_through_ports', 'local_context_data', 'power_outlets', 'interfaces', 'pass_through_ports', 'local_context_data',
], ],
['contact', 'contact_role'],
] ]
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
@ -636,11 +655,12 @@ class CableFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class PowerPanelFilterForm(CustomFieldModelFilterForm): class PowerPanelFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm):
model = PowerPanel model = PowerPanel
field_groups = ( field_groups = (
('q', 'tag'), ('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( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),

View File

@ -23,6 +23,12 @@ class CableTable(BaseTable):
orderable=False, orderable=False,
verbose_name='Side A' 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( termination_a = tables.Column(
accessor=Accessor('termination_a'), accessor=Accessor('termination_a'),
orderable=False, orderable=False,
@ -35,6 +41,12 @@ class CableTable(BaseTable):
orderable=False, orderable=False,
verbose_name='Side B' 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( termination_b = tables.Column(
accessor=Accessor('termination_b'), accessor=Accessor('termination_b'),
orderable=False, orderable=False,
@ -55,7 +67,7 @@ class CableTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Cable model = Cable
fields = ( 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', 'status', 'type', 'tenant', 'color', 'length', 'tags', 'created', 'last_updated',
) )
default_columns = ( default_columns = (

View File

@ -194,6 +194,9 @@ class DeviceTable(BaseTable):
vc_priority = tables.Column( vc_priority = tables.Column(
verbose_name='VC Priority' verbose_name='VC Priority'
) )
contacts = tables.ManyToManyColumn(
linkify_item=True
)
comments = MarkdownColumn() comments = MarkdownColumn()
tags = TagColumn( tags = TagColumn(
url_name='dcim:device_list' url_name='dcim:device_list'
@ -204,8 +207,8 @@ class DeviceTable(BaseTable):
fields = ( fields = (
'pk', 'id', 'name', 'status', 'tenant', 'device_role', 'manufacturer', 'device_type', 'platform', 'serial', 'pk', 'id', 'name', 'status', 'tenant', 'device_role', 'manufacturer', 'device_type', 'platform', 'serial',
'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'primary_ip', 'airflow', 'primary_ip4', 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'primary_ip', 'airflow', 'primary_ip4',
'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags', 'created', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'contacts', 'tags',
'last_updated', 'created', 'last_updated',
) )
default_columns = ( default_columns = (
'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type', 'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type',

View File

@ -41,6 +41,9 @@ class ManufacturerTable(BaseTable):
verbose_name='Platforms' verbose_name='Platforms'
) )
slug = tables.Column() slug = tables.Column()
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = TagColumn( tags = TagColumn(
url_name='dcim:manufacturer_list' url_name='dcim:manufacturer_list'
) )
@ -50,7 +53,7 @@ class ManufacturerTable(BaseTable):
model = Manufacturer model = Manufacturer
fields = ( fields = (
'pk', 'id', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug', 'pk', 'id', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug',
'actions', 'created', 'last_updated', 'contacts', 'actions', 'created', 'last_updated',
) )
default_columns = ( default_columns = (
'pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug', 'actions', 'pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug', 'actions',

View File

@ -27,13 +27,16 @@ class PowerPanelTable(BaseTable):
url_params={'power_panel_id': 'pk'}, url_params={'power_panel_id': 'pk'},
verbose_name='Feeds' verbose_name='Feeds'
) )
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = TagColumn( tags = TagColumn(
url_name='dcim:powerpanel_list' url_name='dcim:powerpanel_list'
) )
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = PowerPanel 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') default_columns = ('pk', 'name', 'site', 'location', 'powerfeed_count')

View File

@ -75,6 +75,9 @@ class RackTable(BaseTable):
orderable=False, orderable=False,
verbose_name='Power' verbose_name='Power'
) )
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = TagColumn( tags = TagColumn(
url_name='dcim:rack_list' url_name='dcim:rack_list'
) )
@ -92,7 +95,7 @@ class RackTable(BaseTable):
fields = ( fields = (
'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', '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', '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 = ( default_columns = (
'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count', 'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count',

View File

@ -29,6 +29,9 @@ class RegionTable(BaseTable):
url_params={'region_id': 'pk'}, url_params={'region_id': 'pk'},
verbose_name='Sites' verbose_name='Sites'
) )
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = TagColumn( tags = TagColumn(
url_name='dcim:region_list' url_name='dcim:region_list'
) )
@ -36,7 +39,7 @@ class RegionTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Region 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') default_columns = ('pk', 'name', 'site_count', 'description', 'actions')
@ -54,6 +57,9 @@ class SiteGroupTable(BaseTable):
url_params={'group_id': 'pk'}, url_params={'group_id': 'pk'},
verbose_name='Sites' verbose_name='Sites'
) )
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = TagColumn( tags = TagColumn(
url_name='dcim:sitegroup_list' url_name='dcim:sitegroup_list'
) )
@ -61,7 +67,7 @@ class SiteGroupTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = SiteGroup 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') default_columns = ('pk', 'name', 'site_count', 'description', 'actions')
@ -92,6 +98,9 @@ class SiteTable(BaseTable):
verbose_name='ASNs' verbose_name='ASNs'
) )
tenant = TenantColumn() tenant = TenantColumn()
contacts = tables.ManyToManyColumn(
linkify_item=True
)
comments = MarkdownColumn() comments = MarkdownColumn()
tags = TagColumn( tags = TagColumn(
url_name='dcim:site_list' url_name='dcim:site_list'
@ -102,7 +111,7 @@ class SiteTable(BaseTable):
fields = ( fields = (
'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asns', 'asn_count', 'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asns', 'asn_count',
'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', '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') default_columns = ('pk', 'name', 'status', 'facility', 'region', 'group', 'tenant', 'description')
@ -130,6 +139,9 @@ class LocationTable(BaseTable):
url_params={'location_id': 'pk'}, url_params={'location_id': 'pk'},
verbose_name='Devices' verbose_name='Devices'
) )
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = TagColumn( tags = TagColumn(
url_name='dcim:location_list' url_name='dcim:location_list'
) )
@ -141,7 +153,7 @@ class LocationTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Location model = Location
fields = ( fields = (
'pk', 'id', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'tags', 'pk', 'id', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'contacts',
'actions', 'created', 'last_updated', 'tags', 'actions', 'created', 'last_updated',
) )
default_columns = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'actions') default_columns = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'actions')

View File

@ -328,6 +328,11 @@ class SiteView(generic.ObjectView):
'device_count', 'device_count',
cumulative=True cumulative=True
).restrict(request.user, 'view').filter(site=instance) ).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) asns = ASN.objects.restrict(request.user, 'view').filter(sites=instance)
asn_count = asns.count() asn_count = asns.count()
@ -338,6 +343,7 @@ class SiteView(generic.ObjectView):
'stats': stats, 'stats': stats,
'locations': locations, 'locations': locations,
'asns': asns, 'asns': asns,
'nonracked_devices': nonracked_devices,
} }
@ -415,11 +421,17 @@ class LocationView(generic.ObjectView):
).filter(pk__in=location_ids).exclude(pk=instance.pk) ).filter(pk__in=location_ids).exclude(pk=instance.pk)
child_locations_table = tables.LocationTable(child_locations) child_locations_table = tables.LocationTable(child_locations)
paginate_table(child_locations_table, request) 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 { return {
'rack_count': rack_count, 'rack_count': rack_count,
'device_count': device_count, 'device_count': device_count,
'child_locations_table': child_locations_table, 'child_locations_table': child_locations_table,
'nonracked_devices': nonracked_devices,
} }

View File

@ -33,7 +33,7 @@
</button> </button>
</div> </div>
<div class="d-flex my-1 flex-grow-1 justify-content-center w-100"> <div class="d-flex my-1 flex-grow-1 justify-content-center w-100">
{% search_options %} {% search_options request %}
</div> </div>
</div> </div>
@ -45,7 +45,7 @@
{# Search bar #} {# Search bar #}
<div class="col-6 d-flex flex-grow-1 justify-content-center"> <div class="col-6 d-flex flex-grow-1 justify-content-center">
{% search_options %} {% search_options request %}
</div> </div>
{# Proflie/login button #} {# Proflie/login button #}

View File

@ -8,6 +8,22 @@
<a href="{{ termination.device.get_absolute_url }}">{{ termination.device }}</a> <a href="{{ termination.device.get_absolute_url }}">{{ termination.device }}</a>
</td> </td>
</tr> </tr>
{% if termination.device.site %}
<tr>
<td>Site</td>
<td>
<a href="{{ termination.device.site.get_absolute_url }}">{{ termination.device.site }}</a>
</td>
</tr>
{% endif %}
{% if termination.device.rack %}
<tr>
<td>Rack</td>
<td>
<a href="{{ termination.device.rack.get_absolute_url }}">{{ termination.device.rack }}</a>
</td>
</tr>
{% endif %}
<tr> <tr>
<td>Type</td> <td>Type</td>
<td> <td>

View File

@ -0,0 +1,62 @@
{% load helpers %}
<div class="card">
<h5 class="card-header">
Non-Racked Devices
</h5>
<div class="card-body">
{% if nonracked_devices %}
<table class="table table-hover">
<tr>
<th>Name</th>
<th>Role</th>
<th>Type</th>
<th colspan="2">Parent Device</th>
</tr>
{% for device in nonracked_devices %}
<tr{% if device.device_type.u_height %} class="warning"{% endif %}>
<td>
<a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a>
</td>
<td>{{ device.device_role }}</td>
<td>{{ device.device_type }}</td>
{% if device.parent_bay %}
<td><a href="{{ device.parent_bay.device.get_absolute_url }}">{{ device.parent_bay.device }}</a></td>
<td>{{ device.parent_bay }}</td>
{% else %}
<td colspan="2" class="text-muted">&mdash;</td>
{% endif %}
</tr>
{% endfor %}
</table>
{% else %}
<div class="text-muted">
None
</div>
{% endif %}
</div>
{% if perms.dcim.add_device %}
{% if object|meta:'verbose_name' == 'rack' %}
<div class="card-footer text-end noprint">
<a href="{% url 'dcim:device_add' %}?site={{ object.site.pk }}&rack={{ object.pk }}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i>
Add a Non-Racked Device
</a>
</div>
{% elif object|meta:'verbose_name' == 'site' %}
<div class="card-footer text-end noprint">
<a href="{% url 'dcim:device_add' %}?site={{ object.pk }}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i>
Add a Non-Racked Device
</a>
</div>
{% elif object|meta:'verbose_name' == 'location' %}
<div class="card-footer text-end noprint">
<a href="{% url 'dcim:device_add' %}?site={{ object.site.pk }}&location={{ object.pk }}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i>
Add a Non-Racked Device
</a>
</div>
{% endif %}
{% endif %}
</div>

View File

@ -90,6 +90,7 @@
<div class="col col-md-6"> <div class="col col-md-6">
{% include 'inc/panels/custom_fields.html' %} {% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/contacts.html' %} {% include 'inc/panels/contacts.html' %}
{% include 'dcim/inc/nonracked_devices.html' %}
{% include 'inc/panels/image_attachments.html' %} {% include 'inc/panels/image_attachments.html' %}
{% plugin_right_page object %} {% plugin_right_page object %}
</div> </div>

View File

@ -288,50 +288,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="card"> {% include 'dcim/inc/nonracked_devices.html' %}
<h5 class="card-header">
Non-Racked Devices
</h5>
<div class="card-body">
{% if nonracked_devices %}
<table class="table table-hover">
<tr>
<th>Name</th>
<th>Role</th>
<th>Type</th>
<th colspan="2">Parent Device</th>
</tr>
{% for device in nonracked_devices %}
<tr{% if device.device_type.u_height %} class="warning"{% endif %}>
<td>
<a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a>
</td>
<td>{{ device.device_role }}</td>
<td>{{ device.device_type }}</td>
{% if device.parent_bay %}
<td><a href="{{ device.parent_bay.device.get_absolute_url }}">{{ device.parent_bay.device }}</a></td>
<td>{{ device.parent_bay }}</td>
{% else %}
<td colspan="2" class="text-muted">&mdash;</td>
{% endif %}
</tr>
{% endfor %}
</table>
{% else %}
<div class="text-muted">
None
</div>
{% endif %}
</div>
{% if perms.dcim.add_device %}
<div class="card-footer text-end noprint">
<a href="{% url 'dcim:device_add' %}?site={{ object.site.pk }}&rack={{ object.pk }}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i>
Add a Non-Racked Device
</a>
</div>
{% endif %}
</div>
{% include 'inc/panels/contacts.html' %} {% include 'inc/panels/contacts.html' %}
{% plugin_right_page object %} {% plugin_right_page object %}
</div> </div>

View File

@ -277,6 +277,7 @@
</table> </table>
</div> </div>
</div> </div>
{% include 'dcim/inc/nonracked_devices.html' %}
{% include 'inc/panels/contacts.html' %} {% include 'inc/panels/contacts.html' %}
<div class="card"> <div class="card">
<h5 class="card-header">Locations</h5> <h5 class="card-header">Locations</h5>

View File

@ -11,6 +11,7 @@ __all__ = (
'ContactAssignmentFilterSet', 'ContactAssignmentFilterSet',
'ContactFilterSet', 'ContactFilterSet',
'ContactGroupFilterSet', 'ContactGroupFilterSet',
'ContactModelFilterSet',
'ContactRoleFilterSet', 'ContactRoleFilterSet',
'TenancyFilterSet', 'TenancyFilterSet',
'TenantFilterSet', '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 # Contacts
# #
@ -191,3 +106,102 @@ class ContactAssignmentFilterSet(ChangeLoggedModelFilterSet):
class Meta: class Meta:
model = ContactAssignment model = ContactAssignment
fields = ['id', 'content_type_id', 'object_id', 'priority'] 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)',
)

View File

@ -2,6 +2,7 @@ from django.utils.translation import gettext as _
from extras.forms import CustomFieldModelFilterForm from extras.forms import CustomFieldModelFilterForm
from tenancy.models import * from tenancy.models import *
from tenancy.forms import ContactModelFilterForm
from utilities.forms import DynamicModelMultipleChoiceField, TagFilterField from utilities.forms import DynamicModelMultipleChoiceField, TagFilterField
__all__ = ( __all__ = (
@ -27,11 +28,12 @@ class TenantGroupFilterForm(CustomFieldModelFilterForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class TenantFilterForm(CustomFieldModelFilterForm): class TenantFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm):
model = Tenant model = Tenant
field_groups = ( field_groups = (
('q', 'tag'), ('q', 'tag'),
('group_id',), ('group_id',),
('contact', 'contact_role')
) )
group_id = DynamicModelMultipleChoiceField( group_id = DynamicModelMultipleChoiceField(
queryset=TenantGroup.objects.all(), queryset=TenantGroup.objects.all(),

View File

@ -1,10 +1,11 @@
from django import forms from django import forms
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from tenancy.models import Tenant, TenantGroup from tenancy.models import *
from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField
__all__ = ( __all__ = (
'ContactModelFilterForm',
'TenancyForm', 'TenancyForm',
'TenancyFilterForm', 'TenancyFilterForm',
) )
@ -44,3 +45,16 @@ class TenancyFilterForm(forms.Form):
}, },
label=_('Tenant') 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')
)

View File

@ -166,3 +166,6 @@ class ContactAssignment(ChangeLoggedModel):
if self.priority: if self.priority:
return f"{self.contact} ({self.get_priority_display()})" return f"{self.contact} ({self.get_priority_display()})"
return str(self.contact) return str(self.contact)
def get_absolute_url(self):
return reverse('tenancy:contact', args=[self.contact.pk])

View File

@ -77,6 +77,9 @@ class TenantTable(BaseTable):
group = tables.Column( group = tables.Column(
linkify=True linkify=True
) )
contacts = tables.ManyToManyColumn(
linkify_item=True
)
comments = MarkdownColumn() comments = MarkdownColumn()
tags = TagColumn( tags = TagColumn(
url_name='tenancy:tenant_list' url_name='tenancy:tenant_list'
@ -84,7 +87,7 @@ class TenantTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Tenant 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') default_columns = ('pk', 'name', 'group', 'description')

View File

@ -5,7 +5,7 @@
aria-label="Search" aria-label="Search"
placeholder="Search" placeholder="Search"
class="form-control" class="form-control"
value="{{ request.GET.q }}" value="{{ request.GET.q|escape }}"
/> />
<input name="obj_type" hidden type="text" class="search-obj-type" /> <input name="obj_type" hidden type="text" class="search-obj-type" />

View File

@ -8,6 +8,9 @@ search_form = SearchForm()
@register.inclusion_tag("search/searchbar.html") @register.inclusion_tag("search/searchbar.html")
def search_options() -> Dict: def search_options(request) -> Dict:
"""Provide search options to template.""" """Provide search options to template."""
return {"options": search_form.options} return {
'options': search_form.options,
'request': request,
}

View File

@ -5,7 +5,7 @@ from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup
from extras.filters import TagFilter from extras.filters import TagFilter
from extras.filtersets import LocalConfigContextFilterSet from extras.filtersets import LocalConfigContextFilterSet
from netbox.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet from netbox.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet
from tenancy.filtersets import TenancyFilterSet from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
from utilities.filters import MultiValueMACAddressFilter, TreeNodeMultipleChoiceFilter from utilities.filters import MultiValueMACAddressFilter, TreeNodeMultipleChoiceFilter
from .choices import * from .choices import *
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
@ -27,7 +27,7 @@ class ClusterTypeFilterSet(OrganizationalModelFilterSet):
fields = ['id', 'name', 'slug', 'description'] fields = ['id', 'name', 'slug', 'description']
class ClusterGroupFilterSet(OrganizationalModelFilterSet): class ClusterGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
tag = TagFilter() tag = TagFilter()
class Meta: class Meta:
@ -35,7 +35,7 @@ class ClusterGroupFilterSet(OrganizationalModelFilterSet):
fields = ['id', 'name', 'slug', 'description'] fields = ['id', 'name', 'slug', 'description']
class ClusterFilterSet(PrimaryModelFilterSet, TenancyFilterSet): class ClusterFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='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( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',

View File

@ -3,7 +3,7 @@ from django.utils.translation import gettext as _
from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup
from extras.forms import CustomFieldModelFilterForm, LocalConfigContextFilterForm from extras.forms import CustomFieldModelFilterForm, LocalConfigContextFilterForm
from tenancy.forms import TenancyFilterForm from tenancy.forms import TenancyFilterForm, ContactModelFilterForm
from utilities.forms import ( from utilities.forms import (
DynamicModelMultipleChoiceField, StaticSelect, StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, DynamicModelMultipleChoiceField, StaticSelect, StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
) )
@ -24,18 +24,19 @@ class ClusterTypeFilterForm(CustomFieldModelFilterForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class ClusterGroupFilterForm(CustomFieldModelFilterForm): class ClusterGroupFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm):
model = ClusterGroup model = ClusterGroup
tag = TagFilterField(model) tag = TagFilterField(model)
class ClusterFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): class ClusterFilterForm(TenancyFilterForm, ContactModelFilterForm, CustomFieldModelFilterForm):
model = Cluster model = Cluster
field_groups = [ field_groups = [
['q', 'tag'], ['q', 'tag'],
['group_id', 'type_id'], ['group_id', 'type_id'],
['region_id', 'site_group_id', 'site_id'], ['region_id', 'site_group_id', 'site_id'],
['tenant_group_id', 'tenant_id'], ['tenant_group_id', 'tenant_id'],
['contact', 'contact_role'],
] ]
type_id = DynamicModelMultipleChoiceField( type_id = DynamicModelMultipleChoiceField(
queryset=ClusterType.objects.all(), queryset=ClusterType.objects.all(),
@ -71,7 +72,7 @@ class ClusterFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class VirtualMachineFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, CustomFieldModelFilterForm): class VirtualMachineFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, ContactModelFilterForm, CustomFieldModelFilterForm):
model = VirtualMachine model = VirtualMachine
field_groups = [ field_groups = [
['q', 'tag'], ['q', 'tag'],
@ -79,6 +80,7 @@ class VirtualMachineFilterForm(LocalConfigContextFilterForm, TenancyFilterForm,
['region_id', 'site_group_id', 'site_id'], ['region_id', 'site_group_id', 'site_id'],
['status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'local_context_data'], ['status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'local_context_data'],
['tenant_group_id', 'tenant_id'], ['tenant_group_id', 'tenant_id'],
['contact', 'contact_role'],
] ]
cluster_group_id = DynamicModelMultipleChoiceField( cluster_group_id = DynamicModelMultipleChoiceField(
queryset=ClusterGroup.objects.all(), queryset=ClusterGroup.objects.all(),

View File

@ -62,6 +62,9 @@ class ClusterGroupTable(BaseTable):
cluster_count = tables.Column( cluster_count = tables.Column(
verbose_name='Clusters' verbose_name='Clusters'
) )
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = TagColumn( tags = TagColumn(
url_name='virtualization:clustergroup_list' url_name='virtualization:clustergroup_list'
) )
@ -70,7 +73,7 @@ class ClusterGroupTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = ClusterGroup model = ClusterGroup
fields = ( 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') default_columns = ('pk', 'name', 'cluster_count', 'description', 'actions')
@ -106,6 +109,9 @@ class ClusterTable(BaseTable):
url_params={'cluster_id': 'pk'}, url_params={'cluster_id': 'pk'},
verbose_name='VMs' verbose_name='VMs'
) )
contacts = tables.ManyToManyColumn(
linkify_item=True
)
comments = MarkdownColumn() comments = MarkdownColumn()
tags = TagColumn( tags = TagColumn(
url_name='virtualization:cluster_list' url_name='virtualization:cluster_list'
@ -114,7 +120,7 @@ class ClusterTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Cluster model = Cluster
fields = ( 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', 'created', 'last_updated',
) )
default_columns = ('pk', 'name', 'type', 'group', 'tenant', 'site', 'device_count', 'vm_count') 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'), order_by=('primary_ip4', 'primary_ip6'),
verbose_name='IP Address' verbose_name='IP Address'
) )
contacts = tables.ManyToManyColumn(
linkify_item=True
)
tags = TagColumn( tags = TagColumn(
url_name='virtualization:virtualmachine_list' url_name='virtualization:virtualmachine_list'
) )
@ -158,7 +167,7 @@ class VirtualMachineTable(BaseTable):
model = VirtualMachine model = VirtualMachine
fields = ( fields = (
'pk', 'id', 'name', 'status', 'cluster', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', '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 = ( default_columns = (
'pk', 'name', 'status', 'cluster', 'role', 'tenant', 'vcpus', 'memory', 'disk', 'primary_ip', 'pk', 'name', 'status', 'cluster', 'role', 'tenant', 'vcpus', 'memory', 'disk', 'primary_ip',