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(),