From 36d6dd1ca93a531b29b6849c2ad8d05f29dc72f2 Mon Sep 17 00:00:00 2001 From: Alex Gittings Date: Thu, 24 Feb 2022 17:08:38 +0000 Subject: [PATCH 01/10] Fixes #8645; Allow filtering on core models in the UI and API for contact assignments --- netbox/circuits/api/serializers.py | 16 +- netbox/circuits/filtersets.py | 6 +- netbox/circuits/forms/filtersets.py | 8 +- netbox/dcim/api/serializers.py | 66 ++++++-- netbox/dcim/filtersets.py | 20 +-- netbox/dcim/forms/filtersets.py | 31 ++-- netbox/tenancy/api/serializers.py | 7 +- netbox/tenancy/filtersets.py | 186 ++++++++++++---------- netbox/tenancy/forms/filtersets.py | 4 +- netbox/tenancy/forms/forms.py | 16 +- netbox/virtualization/api/serializers.py | 30 +++- netbox/virtualization/filtersets.py | 8 +- netbox/virtualization/forms/filtersets.py | 10 +- 13 files changed, 264 insertions(+), 144 deletions(-) diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index 42f9d9322..0ad25edca 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -6,7 +6,7 @@ from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSeriali from dcim.api.serializers import LinkTerminationSerializer from netbox.api import ChoiceField from netbox.api.serializers import PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer -from tenancy.api.nested_serializers import NestedTenantSerializer +from tenancy.api.nested_serializers import NestedTenantSerializer, NestedContactAssignmentSerializer from .nested_serializers import * @@ -17,12 +17,17 @@ from .nested_serializers import * class ProviderSerializer(PrimaryModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail') circuit_count = serializers.IntegerField(read_only=True) + contacts = NestedContactAssignmentSerializer( + required=False, + allow_null=True, + many=True + ) class Meta: model = Provider fields = [ 'id', 'url', 'display', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', - 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count', + 'comments', 'contacts', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count', ] @@ -78,12 +83,17 @@ class CircuitSerializer(PrimaryModelSerializer): tenant = NestedTenantSerializer(required=False, allow_null=True) termination_a = CircuitCircuitTerminationSerializer(read_only=True) termination_z = CircuitCircuitTerminationSerializer(read_only=True) + contacts = NestedContactAssignmentSerializer( + required=False, + allow_null=True, + many=True + ) class Meta: model = Circuit fields = [ 'id', 'url', 'display', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', - 'description', 'termination_a', 'termination_z', 'comments', 'tags', 'custom_fields', 'created', + 'description', 'termination_a', 'termination_z', 'comments', 'contacts', 'tags', 'custom_fields', 'created', 'last_updated', ] diff --git a/netbox/circuits/filtersets.py b/netbox/circuits/filtersets.py index fd582dd99..5e3e00301 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 (TenancyFilterSet, ContactModelFilterSet) 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'] -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/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 6549e51a1..b041f990b 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -6,6 +6,7 @@ from timezone_field.rest_framework import TimeZoneSerializerField from dcim.choices import * from dcim.constants import * from dcim.models import * +from tenancy.models import ContactAssignment from ipam.api.nested_serializers import NestedASNSerializer, NestedIPAddressSerializer, NestedVLANSerializer from ipam.models import ASN, VLAN from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField @@ -13,7 +14,7 @@ from netbox.api.serializers import ( NestedGroupModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer, ) from netbox.config import ConfigItem -from tenancy.api.nested_serializers import NestedTenantSerializer +from tenancy.api.nested_serializers import NestedTenantSerializer, NestedContactAssignmentSerializer from users.api.nested_serializers import NestedUserSerializer from utilities.api import get_serializer_for_model from virtualization.api.nested_serializers import NestedClusterSerializer @@ -85,11 +86,16 @@ class RegionSerializer(NestedGroupModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail') parent = NestedRegionSerializer(required=False, allow_null=True, default=None) site_count = serializers.IntegerField(read_only=True) + contacts = NestedContactAssignmentSerializer( + required=False, + allow_null=True, + many=True + ) class Meta: model = Region fields = [ - 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created', + 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'contacts', 'tags', 'custom_fields', 'created', 'last_updated', 'site_count', '_depth', ] @@ -98,11 +104,16 @@ class SiteGroupSerializer(NestedGroupModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail') parent = NestedSiteGroupSerializer(required=False, allow_null=True, default=None) site_count = serializers.IntegerField(read_only=True) + contacts = NestedContactAssignmentSerializer( + required=False, + allow_null=True, + many=True + ) class Meta: model = SiteGroup fields = [ - 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created', + 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'contacts', 'tags', 'custom_fields', 'created', 'last_updated', 'site_count', '_depth', ] @@ -113,6 +124,13 @@ class SiteSerializer(PrimaryModelSerializer): region = NestedRegionSerializer(required=False, allow_null=True) group = NestedSiteGroupSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True) + contacts = SerializedPKRelatedField( + queryset=ContactAssignment.objects.all(), + serializer=NestedContactAssignmentSerializer, + required=False, + allow_null=True, + many=True + ) time_zone = TimeZoneSerializerField(required=False) asns = SerializedPKRelatedField( queryset=ASN.objects.all(), @@ -126,7 +144,7 @@ class SiteSerializer(PrimaryModelSerializer): device_count = serializers.IntegerField(read_only=True) prefix_count = serializers.IntegerField(read_only=True) rack_count = serializers.IntegerField(read_only=True) - virtualmachine_count = serializers.IntegerField(read_only=True) + virtualmachine_count = serializers.IntegerField(read_only=True,) vlan_count = serializers.IntegerField(read_only=True) class Meta: @@ -134,7 +152,7 @@ class SiteSerializer(PrimaryModelSerializer): fields = [ 'id', 'url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'asn', 'asns', 'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', - 'contact_phone', 'contact_email', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + 'contact_phone', 'contact_email', 'comments', 'contacts', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count', 'device_count', 'prefix_count', 'rack_count', 'virtualmachine_count', 'vlan_count', ] @@ -150,11 +168,16 @@ class LocationSerializer(NestedGroupModelSerializer): tenant = NestedTenantSerializer(required=False, allow_null=True) rack_count = serializers.IntegerField(read_only=True) device_count = serializers.IntegerField(read_only=True) + contacts = NestedContactAssignmentSerializer( + required=False, + allow_null=True, + many=True + ) class Meta: model = Location fields = [ - 'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'tenant', 'description', 'tags', 'custom_fields', + 'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'tenant', 'description', 'contacts', 'tags', 'custom_fields', 'created', 'last_updated', 'rack_count', 'device_count', '_depth', ] @@ -185,13 +208,18 @@ class RackSerializer(PrimaryModelSerializer): outer_unit = ChoiceField(choices=RackDimensionUnitChoices, allow_blank=True, required=False) device_count = serializers.IntegerField(read_only=True) powerfeed_count = serializers.IntegerField(read_only=True) + contacts = NestedContactAssignmentSerializer( + required=False, + allow_null=True, + many=True + ) class Meta: model = Rack fields = [ 'id', 'url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status', 'role', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', - 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'powerfeed_count', + 'comments', 'contacts', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'powerfeed_count', ] @@ -269,11 +297,17 @@ class ManufacturerSerializer(PrimaryModelSerializer): devicetype_count = serializers.IntegerField(read_only=True) inventoryitem_count = serializers.IntegerField(read_only=True) platform_count = serializers.IntegerField(read_only=True) + contacts = NestedContactAssignmentSerializer( + required=False, + allow_null=True, + many=True + ) + class Meta: model = Manufacturer fields = [ - 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated', + 'id', 'url', 'display', 'name', 'slug', 'description', 'contacts', 'tags', 'custom_fields', 'created', 'last_updated', 'devicetype_count', 'inventoryitem_count', 'platform_count', ] @@ -469,6 +503,11 @@ class DeviceSerializer(PrimaryModelSerializer): cluster = NestedClusterSerializer(required=False, allow_null=True) virtual_chassis = NestedVirtualChassisSerializer(required=False, allow_null=True, default=None) vc_position = serializers.IntegerField(allow_null=True, max_value=255, min_value=0, default=None) + contacts = NestedContactAssignmentSerializer( + required=False, + allow_null=True, + many=True + ) class Meta: model = Device @@ -476,7 +515,7 @@ class DeviceSerializer(PrimaryModelSerializer): 'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', - 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', + 'local_context_data', 'contacts', 'tags', 'custom_fields', 'created', 'last_updated', ] @swagger_serializer_method(serializer_or_field=NestedDeviceSerializer) @@ -498,7 +537,7 @@ class DeviceWithConfigContextSerializer(DeviceSerializer): fields = [ 'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', - 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'local_context_data', + 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'local_context_data', 'contacts', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated', ] @@ -875,11 +914,16 @@ class PowerPanelSerializer(PrimaryModelSerializer): default=None ) powerfeed_count = serializers.IntegerField(read_only=True) + contacts = NestedContactAssignmentSerializer( + required=False, + allow_null=True, + many=True + ) class Meta: model = PowerPanel fields = [ - 'id', 'url', 'display', 'site', 'location', 'name', 'tags', 'custom_fields', 'powerfeed_count', + 'id', 'url', 'display', 'site', 'location', 'name', 'contacts', 'tags', 'custom_fields', 'powerfeed_count', 'created', 'last_updated', ] diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index d9c75d3fa..c198335f4 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'] -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..a6cccb00e 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -5,9 +5,12 @@ 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 ( + TenancyFilterForm, ContactModelFilterForm +) from utilities.forms import ( APISelectMultiple, add_blank_choice, ColorField, DynamicModelMultipleChoiceField, FilterForm, StaticSelect, StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, @@ -98,7 +101,7 @@ class DeviceComponentFilterForm(CustomFieldModelFilterForm): ) -class RegionFilterForm(CustomFieldModelFilterForm): +class RegionFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm): model = Region parent_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), @@ -108,7 +111,7 @@ class RegionFilterForm(CustomFieldModelFilterForm): tag = TagFilterField(model) -class SiteGroupFilterForm(CustomFieldModelFilterForm): +class SiteGroupFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm): model = SiteGroup parent_id = DynamicModelMultipleChoiceField( queryset=SiteGroup.objects.all(), @@ -118,13 +121,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, @@ -148,13 +152,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 +196,7 @@ class RackRoleFilterForm(CustomFieldModelFilterForm): tag = TagFilterField(model) -class RackFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): +class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, CustomFieldModelFilterForm): model = Rack field_groups = [ ['q', 'tag'], @@ -200,6 +204,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,7 +308,7 @@ class RackReservationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): tag = TagFilterField(model) -class ManufacturerFilterForm(CustomFieldModelFilterForm): +class ManufacturerFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm): model = Manufacturer tag = TagFilterField(model) @@ -390,7 +395,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 +407,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 +642,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/tenancy/api/serializers.py b/netbox/tenancy/api/serializers.py index a0482aa1d..7afab04e7 100644 --- a/netbox/tenancy/api/serializers.py +++ b/netbox/tenancy/api/serializers.py @@ -40,11 +40,16 @@ class TenantSerializer(PrimaryModelSerializer): vlan_count = serializers.IntegerField(read_only=True) vrf_count = serializers.IntegerField(read_only=True) cluster_count = serializers.IntegerField(read_only=True) + contacts = NestedContactAssignmentSerializer( + required=False, + allow_null=True, + many=True + ) class Meta: model = Tenant fields = [ - 'id', 'url', 'display', 'name', 'slug', 'group', 'description', 'comments', 'tags', 'custom_fields', + 'id', 'url', 'display', 'name', 'slug', 'group', 'description', 'comments', 'contacts', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count', 'device_count', 'ipaddress_count', 'prefix_count', 'rack_count', 'site_count', 'virtualmachine_count', 'vlan_count', 'vrf_count', 'cluster_count', ] diff --git a/netbox/tenancy/filtersets.py b/netbox/tenancy/filtersets.py index c8af89143..0fc709eb6 100644 --- a/netbox/tenancy/filtersets.py +++ b/netbox/tenancy/filtersets.py @@ -15,95 +15,10 @@ __all__ = ( 'TenancyFilterSet', 'TenantFilterSet', 'TenantGroupFilterSet', + 'ContactModelFilterSet' ) -# -# 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'] - - 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'] + + 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..d979a3c96 100644 --- a/netbox/tenancy/forms/forms.py +++ b/netbox/tenancy/forms/forms.py @@ -1,12 +1,13 @@ 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__ = ( 'TenancyForm', 'TenancyFilterForm', + 'ContactModelFilterForm' ) @@ -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') + ) \ No newline at end of file diff --git a/netbox/virtualization/api/serializers.py b/netbox/virtualization/api/serializers.py index 866b8f9bb..f3c487bed 100644 --- a/netbox/virtualization/api/serializers.py +++ b/netbox/virtualization/api/serializers.py @@ -7,7 +7,7 @@ from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSer from ipam.models import VLAN from netbox.api import ChoiceField, SerializedPKRelatedField from netbox.api.serializers import PrimaryModelSerializer -from tenancy.api.nested_serializers import NestedTenantSerializer +from tenancy.api.nested_serializers import NestedTenantSerializer, NestedContactAssignmentSerializer from virtualization.choices import * from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface from .nested_serializers import * @@ -32,11 +32,16 @@ class ClusterTypeSerializer(PrimaryModelSerializer): class ClusterGroupSerializer(PrimaryModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail') cluster_count = serializers.IntegerField(read_only=True) + contacts = NestedContactAssignmentSerializer( + required=False, + allow_null=True, + many=True + ) class Meta: model = ClusterGroup fields = [ - 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated', + 'id', 'url', 'display', 'name', 'slug', 'description', 'contacts', 'tags', 'custom_fields', 'created', 'last_updated', 'cluster_count', ] @@ -49,11 +54,16 @@ class ClusterSerializer(PrimaryModelSerializer): site = NestedSiteSerializer(required=False, allow_null=True, default=None) device_count = serializers.IntegerField(read_only=True) virtualmachine_count = serializers.IntegerField(read_only=True) + contacts = NestedContactAssignmentSerializer( + required=False, + allow_null=True, + many=True + ) class Meta: model = Cluster fields = [ - 'id', 'url', 'display', 'name', 'type', 'group', 'tenant', 'site', 'comments', 'tags', 'custom_fields', + 'id', 'url', 'display', 'name', 'type', 'group', 'tenant', 'site', 'comments', 'contacts', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count', ] @@ -73,12 +83,17 @@ class VirtualMachineSerializer(PrimaryModelSerializer): primary_ip = NestedIPAddressSerializer(read_only=True) primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True) primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True) + contacts = NestedContactAssignmentSerializer( + required=False, + allow_null=True, + many=True + ) class Meta: model = VirtualMachine fields = [ 'id', 'url', 'display', 'name', 'status', 'site', 'cluster', 'role', 'tenant', 'platform', 'primary_ip', - 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'local_context_data', 'tags', + 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'local_context_data', 'contacts', 'tags', 'custom_fields', 'created', 'last_updated', ] validators = [] @@ -86,11 +101,16 @@ class VirtualMachineSerializer(PrimaryModelSerializer): class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer): config_context = serializers.SerializerMethodField() + contacts = NestedContactAssignmentSerializer( + required=False, + allow_null=True, + many=True + ) class Meta(VirtualMachineSerializer.Meta): fields = [ 'id', 'url', 'display', 'name', 'status', 'site', 'cluster', 'role', 'tenant', 'platform', 'primary_ip', - 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'local_context_data', 'tags', + 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'local_context_data', 'contacts', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated', ] diff --git a/netbox/virtualization/filtersets.py b/netbox/virtualization/filtersets.py index ed2775de2..dadf781a3 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(), From bf22b820bf9e4c70b7cb420c9ef7c1660da09c29 Mon Sep 17 00:00:00 2001 From: Alex Gittings Date: Wed, 9 Mar 2022 16:35:47 +0000 Subject: [PATCH 02/10] Fixes #8645; Allow filtering on core models in the UI --- netbox/circuits/filtersets.py | 6 +- netbox/circuits/forms/filtersets.py | 8 +- netbox/dcim/filtersets.py | 20 +-- netbox/dcim/forms/filtersets.py | 31 ++-- netbox/tenancy/filtersets.py | 186 ++++++++++++---------- netbox/tenancy/forms/filtersets.py | 4 +- netbox/tenancy/forms/forms.py | 16 +- netbox/virtualization/filtersets.py | 8 +- netbox/virtualization/forms/filtersets.py | 10 +- 9 files changed, 165 insertions(+), 124 deletions(-) diff --git a/netbox/circuits/filtersets.py b/netbox/circuits/filtersets.py index fd582dd99..5e3e00301 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 (TenancyFilterSet, ContactModelFilterSet) 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'] -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/dcim/filtersets.py b/netbox/dcim/filtersets.py index d9c75d3fa..c198335f4 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'] -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..a6cccb00e 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -5,9 +5,12 @@ 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 ( + TenancyFilterForm, ContactModelFilterForm +) from utilities.forms import ( APISelectMultiple, add_blank_choice, ColorField, DynamicModelMultipleChoiceField, FilterForm, StaticSelect, StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, @@ -98,7 +101,7 @@ class DeviceComponentFilterForm(CustomFieldModelFilterForm): ) -class RegionFilterForm(CustomFieldModelFilterForm): +class RegionFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm): model = Region parent_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), @@ -108,7 +111,7 @@ class RegionFilterForm(CustomFieldModelFilterForm): tag = TagFilterField(model) -class SiteGroupFilterForm(CustomFieldModelFilterForm): +class SiteGroupFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm): model = SiteGroup parent_id = DynamicModelMultipleChoiceField( queryset=SiteGroup.objects.all(), @@ -118,13 +121,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, @@ -148,13 +152,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 +196,7 @@ class RackRoleFilterForm(CustomFieldModelFilterForm): tag = TagFilterField(model) -class RackFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): +class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, CustomFieldModelFilterForm): model = Rack field_groups = [ ['q', 'tag'], @@ -200,6 +204,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,7 +308,7 @@ class RackReservationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): tag = TagFilterField(model) -class ManufacturerFilterForm(CustomFieldModelFilterForm): +class ManufacturerFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm): model = Manufacturer tag = TagFilterField(model) @@ -390,7 +395,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 +407,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 +642,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/tenancy/filtersets.py b/netbox/tenancy/filtersets.py index c8af89143..0fc709eb6 100644 --- a/netbox/tenancy/filtersets.py +++ b/netbox/tenancy/filtersets.py @@ -15,95 +15,10 @@ __all__ = ( 'TenancyFilterSet', 'TenantFilterSet', 'TenantGroupFilterSet', + 'ContactModelFilterSet' ) -# -# 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'] - - 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'] + + 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..d979a3c96 100644 --- a/netbox/tenancy/forms/forms.py +++ b/netbox/tenancy/forms/forms.py @@ -1,12 +1,13 @@ 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__ = ( 'TenancyForm', 'TenancyFilterForm', + 'ContactModelFilterForm' ) @@ -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') + ) \ No newline at end of file diff --git a/netbox/virtualization/filtersets.py b/netbox/virtualization/filtersets.py index ed2775de2..dadf781a3 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(), From 73af3ba095059d0be09bf0c63449a24b85b2dba6 Mon Sep 17 00:00:00 2001 From: Alex Gittings Date: Wed, 9 Mar 2022 16:45:19 +0000 Subject: [PATCH 03/10] remove contacts from api endpoints --- netbox/circuits/api/serializers.py | 16 ++---- netbox/dcim/api/serializers.py | 66 ++++-------------------- netbox/tenancy/api/nested_serializers.py | 11 ---- netbox/tenancy/api/serializers.py | 7 +-- netbox/virtualization/api/serializers.py | 30 ++--------- 5 files changed, 20 insertions(+), 110 deletions(-) diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index 0ad25edca..42f9d9322 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -6,7 +6,7 @@ from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSeriali from dcim.api.serializers import LinkTerminationSerializer from netbox.api import ChoiceField from netbox.api.serializers import PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer -from tenancy.api.nested_serializers import NestedTenantSerializer, NestedContactAssignmentSerializer +from tenancy.api.nested_serializers import NestedTenantSerializer from .nested_serializers import * @@ -17,17 +17,12 @@ from .nested_serializers import * class ProviderSerializer(PrimaryModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail') circuit_count = serializers.IntegerField(read_only=True) - contacts = NestedContactAssignmentSerializer( - required=False, - allow_null=True, - many=True - ) class Meta: model = Provider fields = [ 'id', 'url', 'display', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', - 'comments', 'contacts', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count', + 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count', ] @@ -83,17 +78,12 @@ class CircuitSerializer(PrimaryModelSerializer): tenant = NestedTenantSerializer(required=False, allow_null=True) termination_a = CircuitCircuitTerminationSerializer(read_only=True) termination_z = CircuitCircuitTerminationSerializer(read_only=True) - contacts = NestedContactAssignmentSerializer( - required=False, - allow_null=True, - many=True - ) class Meta: model = Circuit fields = [ 'id', 'url', 'display', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', - 'description', 'termination_a', 'termination_z', 'comments', 'contacts', 'tags', 'custom_fields', 'created', + 'description', 'termination_a', 'termination_z', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', ] diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index b041f990b..6549e51a1 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -6,7 +6,6 @@ from timezone_field.rest_framework import TimeZoneSerializerField from dcim.choices import * from dcim.constants import * from dcim.models import * -from tenancy.models import ContactAssignment from ipam.api.nested_serializers import NestedASNSerializer, NestedIPAddressSerializer, NestedVLANSerializer from ipam.models import ASN, VLAN from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField @@ -14,7 +13,7 @@ from netbox.api.serializers import ( NestedGroupModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer, ) from netbox.config import ConfigItem -from tenancy.api.nested_serializers import NestedTenantSerializer, NestedContactAssignmentSerializer +from tenancy.api.nested_serializers import NestedTenantSerializer from users.api.nested_serializers import NestedUserSerializer from utilities.api import get_serializer_for_model from virtualization.api.nested_serializers import NestedClusterSerializer @@ -86,16 +85,11 @@ class RegionSerializer(NestedGroupModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail') parent = NestedRegionSerializer(required=False, allow_null=True, default=None) site_count = serializers.IntegerField(read_only=True) - contacts = NestedContactAssignmentSerializer( - required=False, - allow_null=True, - many=True - ) class Meta: model = Region fields = [ - 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'contacts', 'tags', 'custom_fields', 'created', + 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'site_count', '_depth', ] @@ -104,16 +98,11 @@ class SiteGroupSerializer(NestedGroupModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail') parent = NestedSiteGroupSerializer(required=False, allow_null=True, default=None) site_count = serializers.IntegerField(read_only=True) - contacts = NestedContactAssignmentSerializer( - required=False, - allow_null=True, - many=True - ) class Meta: model = SiteGroup fields = [ - 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'contacts', 'tags', 'custom_fields', 'created', + 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'site_count', '_depth', ] @@ -124,13 +113,6 @@ class SiteSerializer(PrimaryModelSerializer): region = NestedRegionSerializer(required=False, allow_null=True) group = NestedSiteGroupSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True) - contacts = SerializedPKRelatedField( - queryset=ContactAssignment.objects.all(), - serializer=NestedContactAssignmentSerializer, - required=False, - allow_null=True, - many=True - ) time_zone = TimeZoneSerializerField(required=False) asns = SerializedPKRelatedField( queryset=ASN.objects.all(), @@ -144,7 +126,7 @@ class SiteSerializer(PrimaryModelSerializer): device_count = serializers.IntegerField(read_only=True) prefix_count = serializers.IntegerField(read_only=True) rack_count = serializers.IntegerField(read_only=True) - virtualmachine_count = serializers.IntegerField(read_only=True,) + virtualmachine_count = serializers.IntegerField(read_only=True) vlan_count = serializers.IntegerField(read_only=True) class Meta: @@ -152,7 +134,7 @@ class SiteSerializer(PrimaryModelSerializer): fields = [ 'id', 'url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'asn', 'asns', 'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', - 'contact_phone', 'contact_email', 'comments', 'contacts', 'tags', 'custom_fields', 'created', 'last_updated', + 'contact_phone', 'contact_email', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count', 'device_count', 'prefix_count', 'rack_count', 'virtualmachine_count', 'vlan_count', ] @@ -168,16 +150,11 @@ class LocationSerializer(NestedGroupModelSerializer): tenant = NestedTenantSerializer(required=False, allow_null=True) rack_count = serializers.IntegerField(read_only=True) device_count = serializers.IntegerField(read_only=True) - contacts = NestedContactAssignmentSerializer( - required=False, - allow_null=True, - many=True - ) class Meta: model = Location fields = [ - 'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'tenant', 'description', 'contacts', 'tags', 'custom_fields', + 'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'tenant', 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'rack_count', 'device_count', '_depth', ] @@ -208,18 +185,13 @@ class RackSerializer(PrimaryModelSerializer): outer_unit = ChoiceField(choices=RackDimensionUnitChoices, allow_blank=True, required=False) device_count = serializers.IntegerField(read_only=True) powerfeed_count = serializers.IntegerField(read_only=True) - contacts = NestedContactAssignmentSerializer( - required=False, - allow_null=True, - many=True - ) class Meta: model = Rack fields = [ 'id', 'url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status', 'role', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', - 'comments', 'contacts', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'powerfeed_count', + 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'powerfeed_count', ] @@ -297,17 +269,11 @@ class ManufacturerSerializer(PrimaryModelSerializer): devicetype_count = serializers.IntegerField(read_only=True) inventoryitem_count = serializers.IntegerField(read_only=True) platform_count = serializers.IntegerField(read_only=True) - contacts = NestedContactAssignmentSerializer( - required=False, - allow_null=True, - many=True - ) - class Meta: model = Manufacturer fields = [ - 'id', 'url', 'display', 'name', 'slug', 'description', 'contacts', 'tags', 'custom_fields', 'created', 'last_updated', + 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'devicetype_count', 'inventoryitem_count', 'platform_count', ] @@ -503,11 +469,6 @@ class DeviceSerializer(PrimaryModelSerializer): cluster = NestedClusterSerializer(required=False, allow_null=True) virtual_chassis = NestedVirtualChassisSerializer(required=False, allow_null=True, default=None) vc_position = serializers.IntegerField(allow_null=True, max_value=255, min_value=0, default=None) - contacts = NestedContactAssignmentSerializer( - required=False, - allow_null=True, - many=True - ) class Meta: model = Device @@ -515,7 +476,7 @@ class DeviceSerializer(PrimaryModelSerializer): 'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', - 'local_context_data', 'contacts', 'tags', 'custom_fields', 'created', 'last_updated', + 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', ] @swagger_serializer_method(serializer_or_field=NestedDeviceSerializer) @@ -537,7 +498,7 @@ class DeviceWithConfigContextSerializer(DeviceSerializer): fields = [ 'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', - 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'local_context_data', 'contacts', + 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated', ] @@ -914,16 +875,11 @@ class PowerPanelSerializer(PrimaryModelSerializer): default=None ) powerfeed_count = serializers.IntegerField(read_only=True) - contacts = NestedContactAssignmentSerializer( - required=False, - allow_null=True, - many=True - ) class Meta: model = PowerPanel fields = [ - 'id', 'url', 'display', 'site', 'location', 'name', 'contacts', 'tags', 'custom_fields', 'powerfeed_count', + 'id', 'url', 'display', 'site', 'location', 'name', 'tags', 'custom_fields', 'powerfeed_count', 'created', 'last_updated', ] diff --git a/netbox/tenancy/api/nested_serializers.py b/netbox/tenancy/api/nested_serializers.py index 00ac6ff84..a072331f5 100644 --- a/netbox/tenancy/api/nested_serializers.py +++ b/netbox/tenancy/api/nested_serializers.py @@ -5,7 +5,6 @@ from tenancy.models import * __all__ = [ 'NestedContactSerializer', - 'NestedContactAssignmentSerializer', 'NestedContactGroupSerializer', 'NestedContactRoleSerializer', 'NestedTenantGroupSerializer', @@ -63,13 +62,3 @@ class NestedContactSerializer(WritableNestedSerializer): class Meta: model = Contact fields = ['id', 'url', 'display', 'name'] - - -class NestedContactAssignmentSerializer(WritableNestedSerializer): - url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactassignment-detail') - contact = NestedContactSerializer() - role = NestedContactRoleSerializer - - class Meta: - model = ContactAssignment - fields = ['id', 'url', 'display', 'contact', 'role', 'priority'] diff --git a/netbox/tenancy/api/serializers.py b/netbox/tenancy/api/serializers.py index 7afab04e7..a0482aa1d 100644 --- a/netbox/tenancy/api/serializers.py +++ b/netbox/tenancy/api/serializers.py @@ -40,16 +40,11 @@ class TenantSerializer(PrimaryModelSerializer): vlan_count = serializers.IntegerField(read_only=True) vrf_count = serializers.IntegerField(read_only=True) cluster_count = serializers.IntegerField(read_only=True) - contacts = NestedContactAssignmentSerializer( - required=False, - allow_null=True, - many=True - ) class Meta: model = Tenant fields = [ - 'id', 'url', 'display', 'name', 'slug', 'group', 'description', 'comments', 'contacts', 'tags', 'custom_fields', + 'id', 'url', 'display', 'name', 'slug', 'group', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count', 'device_count', 'ipaddress_count', 'prefix_count', 'rack_count', 'site_count', 'virtualmachine_count', 'vlan_count', 'vrf_count', 'cluster_count', ] diff --git a/netbox/virtualization/api/serializers.py b/netbox/virtualization/api/serializers.py index f3c487bed..866b8f9bb 100644 --- a/netbox/virtualization/api/serializers.py +++ b/netbox/virtualization/api/serializers.py @@ -7,7 +7,7 @@ from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSer from ipam.models import VLAN from netbox.api import ChoiceField, SerializedPKRelatedField from netbox.api.serializers import PrimaryModelSerializer -from tenancy.api.nested_serializers import NestedTenantSerializer, NestedContactAssignmentSerializer +from tenancy.api.nested_serializers import NestedTenantSerializer from virtualization.choices import * from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface from .nested_serializers import * @@ -32,16 +32,11 @@ class ClusterTypeSerializer(PrimaryModelSerializer): class ClusterGroupSerializer(PrimaryModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail') cluster_count = serializers.IntegerField(read_only=True) - contacts = NestedContactAssignmentSerializer( - required=False, - allow_null=True, - many=True - ) class Meta: model = ClusterGroup fields = [ - 'id', 'url', 'display', 'name', 'slug', 'description', 'contacts', 'tags', 'custom_fields', 'created', 'last_updated', + 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'cluster_count', ] @@ -54,16 +49,11 @@ class ClusterSerializer(PrimaryModelSerializer): site = NestedSiteSerializer(required=False, allow_null=True, default=None) device_count = serializers.IntegerField(read_only=True) virtualmachine_count = serializers.IntegerField(read_only=True) - contacts = NestedContactAssignmentSerializer( - required=False, - allow_null=True, - many=True - ) class Meta: model = Cluster fields = [ - 'id', 'url', 'display', 'name', 'type', 'group', 'tenant', 'site', 'comments', 'contacts', 'tags', 'custom_fields', + 'id', 'url', 'display', 'name', 'type', 'group', 'tenant', 'site', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count', ] @@ -83,17 +73,12 @@ class VirtualMachineSerializer(PrimaryModelSerializer): primary_ip = NestedIPAddressSerializer(read_only=True) primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True) primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True) - contacts = NestedContactAssignmentSerializer( - required=False, - allow_null=True, - many=True - ) class Meta: model = VirtualMachine fields = [ 'id', 'url', 'display', 'name', 'status', 'site', 'cluster', 'role', 'tenant', 'platform', 'primary_ip', - 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'local_context_data', 'contacts', 'tags', + 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', ] validators = [] @@ -101,16 +86,11 @@ class VirtualMachineSerializer(PrimaryModelSerializer): class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer): config_context = serializers.SerializerMethodField() - contacts = NestedContactAssignmentSerializer( - required=False, - allow_null=True, - many=True - ) class Meta(VirtualMachineSerializer.Meta): fields = [ 'id', 'url', 'display', 'name', 'status', 'site', 'cluster', 'role', 'tenant', 'platform', 'primary_ip', - 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'local_context_data', 'contacts', 'tags', + 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated', ] From 412c1df15a7dfbf1fdf1d2ba73a2f7dc76ca6475 Mon Sep 17 00:00:00 2001 From: Alex Gittings Date: Wed, 9 Mar 2022 16:48:29 +0000 Subject: [PATCH 04/10] acidentally removed NestedContactAssignmentSerializer in previous commit --- netbox/tenancy/api/nested_serializers.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/netbox/tenancy/api/nested_serializers.py b/netbox/tenancy/api/nested_serializers.py index a072331f5..00ac6ff84 100644 --- a/netbox/tenancy/api/nested_serializers.py +++ b/netbox/tenancy/api/nested_serializers.py @@ -5,6 +5,7 @@ from tenancy.models import * __all__ = [ 'NestedContactSerializer', + 'NestedContactAssignmentSerializer', 'NestedContactGroupSerializer', 'NestedContactRoleSerializer', 'NestedTenantGroupSerializer', @@ -62,3 +63,13 @@ class NestedContactSerializer(WritableNestedSerializer): class Meta: model = Contact fields = ['id', 'url', 'display', 'name'] + + +class NestedContactAssignmentSerializer(WritableNestedSerializer): + url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactassignment-detail') + contact = NestedContactSerializer() + role = NestedContactRoleSerializer + + class Meta: + model = ContactAssignment + fields = ['id', 'url', 'display', 'contact', 'role', 'priority'] From 27dab262defe700c6bc9c06eee72bcabe4b9cad7 Mon Sep 17 00:00:00 2001 From: Alex Gittings Date: Wed, 9 Mar 2022 17:35:25 +0000 Subject: [PATCH 05/10] add columns for each model table that has contacts --- netbox/dcim/tables/devices.py | 7 +++++-- netbox/dcim/tables/devicetypes.py | 5 ++++- netbox/dcim/tables/power.py | 5 ++++- netbox/dcim/tables/racks.py | 5 ++++- netbox/dcim/tables/sites.py | 22 +++++++++++++++++----- netbox/tenancy/models/contacts.py | 3 +++ netbox/tenancy/tables.py | 5 ++++- netbox/virtualization/tables.py | 15 ++++++++++++--- 8 files changed, 53 insertions(+), 14 deletions(-) diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index 3c2b3dace..80935b11c 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 e658f1caa..06753ff0f 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') @@ -88,6 +94,9 @@ class SiteTable(BaseTable): verbose_name='ASNs' ) tenant = TenantColumn() + contacts = tables.ManyToManyColumn( + linkify_item=True + ) comments = MarkdownColumn() tags = TagColumn( url_name='dcim:site_list' @@ -98,7 +107,7 @@ class SiteTable(BaseTable): fields = ( 'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', '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') @@ -126,6 +135,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' ) @@ -137,7 +149,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/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/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', From b779bbfc9d26067d368d8efec3b624f09279fbf1 Mon Sep 17 00:00:00 2001 From: Alex Gittings Date: Wed, 9 Mar 2022 17:49:02 +0000 Subject: [PATCH 06/10] add contacts to site table --- netbox/dcim/tables/sites.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/netbox/dcim/tables/sites.py b/netbox/dcim/tables/sites.py index 5d41a37af..9d0e73b64 100644 --- a/netbox/dcim/tables/sites.py +++ b/netbox/dcim/tables/sites.py @@ -109,15 +109,9 @@ class SiteTable(BaseTable): class Meta(BaseTable.Meta): model = Site fields = ( -<<<<<<< HEAD - 'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asn_count', 'time_zone', - 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', - 'contact_phone', 'contact_email', 'contacts', 'comments', 'tags', 'created', 'last_updated', -======= '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', ->>>>>>> develop + 'contact_phone', 'contact_email', 'contacts', 'comments', 'tags', 'created', 'last_updated', ) default_columns = ('pk', 'name', 'status', 'facility', 'region', 'group', 'tenant', 'description') From 342f1d31be0bf06fdb037fdb1fb6557a88513c76 Mon Sep 17 00:00:00 2001 From: Alex Gittings Date: Wed, 9 Mar 2022 17:55:45 +0000 Subject: [PATCH 07/10] fix pycodestyle issues --- netbox/dcim/forms/filtersets.py | 1 + netbox/dcim/tables/devices.py | 2 +- netbox/dcim/tables/sites.py | 2 +- netbox/tenancy/forms/forms.py | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index a6cccb00e..d806d852f 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -152,6 +152,7 @@ class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, CustomFieldModel ) tag = TagFilterField(model) + class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, CustomFieldModelFilterForm): model = Location field_groups = [ diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index 80935b11c..debc074d0 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -207,7 +207,7 @@ 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', 'contacts', 'tags', + 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'contacts', 'tags', 'created', 'last_updated', ) default_columns = ( diff --git a/netbox/dcim/tables/sites.py b/netbox/dcim/tables/sites.py index 9d0e73b64..b749315eb 100644 --- a/netbox/dcim/tables/sites.py +++ b/netbox/dcim/tables/sites.py @@ -153,7 +153,7 @@ class LocationTable(BaseTable): class Meta(BaseTable.Meta): model = Location fields = ( - 'pk', 'id', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'contacts', + '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/tenancy/forms/forms.py b/netbox/tenancy/forms/forms.py index d979a3c96..36284b69c 100644 --- a/netbox/tenancy/forms/forms.py +++ b/netbox/tenancy/forms/forms.py @@ -57,4 +57,4 @@ class ContactModelFilterForm(forms.Form): queryset=ContactRole.objects.all(), required=False, label=_('Contact Role') - ) \ No newline at end of file + ) From da37db1ea9411cd8dcf34767a39dcc02177b225b Mon Sep 17 00:00:00 2001 From: minitriga Date: Fri, 18 Mar 2022 14:39:22 +0000 Subject: [PATCH 08/10] Update netbox/circuits/filtersets.py Co-authored-by: Jeremy Stretch --- netbox/circuits/filtersets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/circuits/filtersets.py b/netbox/circuits/filtersets.py index 0680dd5a1..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, ContactModelFilterSet) +from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet from utilities.filters import TreeNodeMultipleChoiceFilter from .choices import * from .models import * From 8f5b14ec84509385440caf37d0de4ed3da85f649 Mon Sep 17 00:00:00 2001 From: minitriga Date: Fri, 18 Mar 2022 14:39:37 +0000 Subject: [PATCH 09/10] Update netbox/dcim/forms/filtersets.py Co-authored-by: Jeremy Stretch --- netbox/dcim/forms/filtersets.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index d806d852f..a7cc19b25 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -8,9 +8,7 @@ from dcim.models import * from tenancy.models import * from extras.forms import CustomFieldModelFilterForm, LocalConfigContextFilterForm from ipam.models import ASN -from tenancy.forms import ( - TenancyFilterForm, ContactModelFilterForm -) +from tenancy.forms import ContactModelFilterForm, TenancyFilterForm from utilities.forms import ( APISelectMultiple, add_blank_choice, ColorField, DynamicModelMultipleChoiceField, FilterForm, StaticSelect, StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, From 50bc0caccf13ddbfda2dd84f9cf9c018a5a850a2 Mon Sep 17 00:00:00 2001 From: Alex Gittings Date: Fri, 18 Mar 2022 14:58:51 +0000 Subject: [PATCH 10/10] Fix issues with ordering and add field_groups --- netbox/circuits/tables.py | 10 ++++++++-- netbox/dcim/forms/filtersets.py | 14 ++++++++++++++ netbox/tenancy/filtersets.py | 4 ++-- netbox/tenancy/forms/forms.py | 2 +- 4 files changed, 25 insertions(+), 5 deletions(-) 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/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index a7cc19b25..91d83ae53 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -101,6 +101,11 @@ class DeviceComponentFilterForm(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, @@ -111,6 +116,11 @@ class RegionFilterForm(ContactModelFilterForm, 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, @@ -309,6 +319,10 @@ class RackReservationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): class ManufacturerFilterForm(ContactModelFilterForm, CustomFieldModelFilterForm): model = Manufacturer + field_groups = [ + ['q', 'tag'], + ['contact', 'contact_role'], + ] tag = TagFilterField(model) diff --git a/netbox/tenancy/filtersets.py b/netbox/tenancy/filtersets.py index fe227e95d..3ff45ab5c 100644 --- a/netbox/tenancy/filtersets.py +++ b/netbox/tenancy/filtersets.py @@ -11,11 +11,11 @@ __all__ = ( 'ContactAssignmentFilterSet', 'ContactFilterSet', 'ContactGroupFilterSet', + 'ContactModelFilterSet', 'ContactRoleFilterSet', 'TenancyFilterSet', 'TenantFilterSet', 'TenantGroupFilterSet', - 'ContactModelFilterSet' ) @@ -165,7 +165,7 @@ class TenantFilterSet(PrimaryModelFilterSet, ContactModelFilterSet): class Meta: model = Tenant - fields = ['id', 'name', 'slug'] + fields = ['id', 'name', 'slug', 'description'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/tenancy/forms/forms.py b/netbox/tenancy/forms/forms.py index 36284b69c..5dcad1d43 100644 --- a/netbox/tenancy/forms/forms.py +++ b/netbox/tenancy/forms/forms.py @@ -5,9 +5,9 @@ from tenancy.models import * from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField __all__ = ( + 'ContactModelFilterForm', 'TenancyForm', 'TenancyFilterForm', - 'ContactModelFilterForm' )