From 538984c6d2a806f2de4e1af4e5d5f510840c6929 Mon Sep 17 00:00:00 2001 From: Johannes Erwerle Date: Mon, 21 Feb 2022 12:26:12 +0100 Subject: [PATCH 01/23] Fixes 8553: Fix Provider network, ASN, and contact options missings from global search selector --- netbox/netbox/constants.py | 374 +++++++++++++++++++++---------------- netbox/netbox/forms.py | 49 ++--- 2 files changed, 226 insertions(+), 197 deletions(-) diff --git a/netbox/netbox/constants.py b/netbox/netbox/constants.py index e29da6617..45de4d5b2 100644 --- a/netbox/netbox/constants.py +++ b/netbox/netbox/constants.py @@ -1,4 +1,5 @@ from collections import OrderedDict +from typing import Dict from circuits.filtersets import CircuitFilterSet, ProviderFilterSet, ProviderNetworkFilterSet from circuits.models import Circuit, ProviderNetwork, Provider @@ -26,169 +27,212 @@ from virtualization.models import Cluster, VirtualMachine from virtualization.tables import ClusterTable, VirtualMachineTable SEARCH_MAX_RESULTS = 15 -SEARCH_TYPES = OrderedDict(( - # Circuits - ('provider', { - 'queryset': Provider.objects.annotate( - count_circuits=count_related(Circuit, 'provider') - ), - 'filterset': ProviderFilterSet, - 'table': ProviderTable, - 'url': 'circuits:provider_list', - }), - ('circuit', { - 'queryset': Circuit.objects.prefetch_related( - 'type', 'provider', 'tenant', 'terminations__site' - ), - 'filterset': CircuitFilterSet, - 'table': CircuitTable, - 'url': 'circuits:circuit_list', - }), - ('providernetwork', { - 'queryset': ProviderNetwork.objects.prefetch_related('provider'), - 'filterset': ProviderNetworkFilterSet, - 'table': ProviderNetworkTable, - 'url': 'circuits:providernetwork_list', - }), - # DCIM - ('site', { - 'queryset': Site.objects.prefetch_related('region', 'tenant'), - 'filterset': SiteFilterSet, - 'table': SiteTable, - 'url': 'dcim:site_list', - }), - ('rack', { - 'queryset': Rack.objects.prefetch_related('site', 'location', 'tenant', 'role'), - 'filterset': RackFilterSet, - 'table': RackTable, - 'url': 'dcim:rack_list', - }), - ('rackreservation', { - 'queryset': RackReservation.objects.prefetch_related('site', 'rack', 'user'), - 'filterset': RackReservationFilterSet, - 'table': RackReservationTable, - 'url': 'dcim:rackreservation_list', - }), - ('location', { - 'queryset': Location.objects.add_related_count( - Location.objects.add_related_count( - Location.objects.all(), - Device, - 'location', - 'device_count', - cumulative=True + +CIRCUIT_TYPES = OrderedDict( + ( + ('provider', { + 'queryset': Provider.objects.annotate( + count_circuits=count_related(Circuit, 'provider') ), - Rack, - 'location', - 'rack_count', - cumulative=True - ).prefetch_related('site'), - 'filterset': LocationFilterSet, - 'table': LocationTable, - 'url': 'dcim:location_list', - }), - ('devicetype', { - 'queryset': DeviceType.objects.prefetch_related('manufacturer').annotate( - instance_count=count_related(Device, 'device_type') - ), - 'filterset': DeviceTypeFilterSet, - 'table': DeviceTypeTable, - 'url': 'dcim:devicetype_list', - }), - ('device', { - 'queryset': Device.objects.prefetch_related( - 'device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack', 'primary_ip4', 'primary_ip6', - ), - 'filterset': DeviceFilterSet, - 'table': DeviceTable, - 'url': 'dcim:device_list', - }), - ('virtualchassis', { - 'queryset': VirtualChassis.objects.prefetch_related('master').annotate( - member_count=count_related(Device, 'virtual_chassis') - ), - 'filterset': VirtualChassisFilterSet, - 'table': VirtualChassisTable, - 'url': 'dcim:virtualchassis_list', - }), - ('cable', { - 'queryset': Cable.objects.all(), - 'filterset': CableFilterSet, - 'table': CableTable, - 'url': 'dcim:cable_list', - }), - ('powerfeed', { - 'queryset': PowerFeed.objects.all(), - 'filterset': PowerFeedFilterSet, - 'table': PowerFeedTable, - 'url': 'dcim:powerfeed_list', - }), - # Virtualization - ('cluster', { - 'queryset': Cluster.objects.prefetch_related('type', 'group').annotate( - device_count=count_related(Device, 'cluster'), - vm_count=count_related(VirtualMachine, 'cluster') - ), - 'filterset': ClusterFilterSet, - 'table': ClusterTable, - 'url': 'virtualization:cluster_list', - }), - ('virtualmachine', { - 'queryset': VirtualMachine.objects.prefetch_related( - 'cluster', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', - ), - 'filterset': VirtualMachineFilterSet, - 'table': VirtualMachineTable, - 'url': 'virtualization:virtualmachine_list', - }), - # IPAM - ('vrf', { - 'queryset': VRF.objects.prefetch_related('tenant'), - 'filterset': VRFFilterSet, - 'table': VRFTable, - 'url': 'ipam:vrf_list', - }), - ('aggregate', { - 'queryset': Aggregate.objects.prefetch_related('rir'), - 'filterset': AggregateFilterSet, - 'table': AggregateTable, - 'url': 'ipam:aggregate_list', - }), - ('prefix', { - 'queryset': Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role'), - 'filterset': PrefixFilterSet, - 'table': PrefixTable, - 'url': 'ipam:prefix_list', - }), - ('ipaddress', { - 'queryset': IPAddress.objects.prefetch_related('vrf__tenant', 'tenant'), - 'filterset': IPAddressFilterSet, - 'table': IPAddressTable, - 'url': 'ipam:ipaddress_list', - }), - ('vlan', { - 'queryset': VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role'), - 'filterset': VLANFilterSet, - 'table': VLANTable, - 'url': 'ipam:vlan_list', - }), - ('asn', { - 'queryset': ASN.objects.prefetch_related('rir', 'tenant'), - 'filterset': ASNFilterSet, - 'table': ASNTable, - 'url': 'ipam:asn_list', - }), - # Tenancy - ('tenant', { - 'queryset': Tenant.objects.prefetch_related('group'), - 'filterset': TenantFilterSet, - 'table': TenantTable, - 'url': 'tenancy:tenant_list', - }), - ('contact', { - 'queryset': Contact.objects.prefetch_related('group', 'assignments').annotate(assignment_count=count_related(ContactAssignment, 'contact')), - 'filterset': ContactFilterSet, - 'table': ContactTable, - 'url': 'tenancy:contact_list', - }), -)) + 'filterset': ProviderFilterSet, + 'table': ProviderTable, + 'url': 'circuits:provider_list', + }), + ('circuit', { + 'queryset': Circuit.objects.prefetch_related( + 'type', 'provider', 'tenant', 'terminations__site' + ), + 'filterset': CircuitFilterSet, + 'table': CircuitTable, + 'url': 'circuits:circuit_list', + }), + ('providernetwork', { + 'queryset': ProviderNetwork.objects.prefetch_related('provider'), + 'filterset': ProviderNetworkFilterSet, + 'table': ProviderNetworkTable, + 'url': 'circuits:providernetwork_list', + }), + ) +) + + +DCIM_TYPES = OrderedDict( + ( + ('site', { + 'queryset': Site.objects.prefetch_related('region', 'tenant'), + 'filterset': SiteFilterSet, + 'table': SiteTable, + 'url': 'dcim:site_list', + }), + ('rack', { + 'queryset': Rack.objects.prefetch_related('site', 'location', 'tenant', 'role'), + 'filterset': RackFilterSet, + 'table': RackTable, + 'url': 'dcim:rack_list', + }), + ('rackreservation', { + 'queryset': RackReservation.objects.prefetch_related('site', 'rack', 'user'), + 'filterset': RackReservationFilterSet, + 'table': RackReservationTable, + 'url': 'dcim:rackreservation_list', + }), + ('location', { + 'queryset': Location.objects.add_related_count( + Location.objects.add_related_count( + Location.objects.all(), + Device, + 'location', + 'device_count', + cumulative=True + ), + Rack, + 'location', + 'rack_count', + cumulative=True + ).prefetch_related('site'), + 'filterset': LocationFilterSet, + 'table': LocationTable, + 'url': 'dcim:location_list', + }), + ('devicetype', { + 'queryset': DeviceType.objects.prefetch_related('manufacturer').annotate( + instance_count=count_related(Device, 'device_type') + ), + 'filterset': DeviceTypeFilterSet, + 'table': DeviceTypeTable, + 'url': 'dcim:devicetype_list', + }), + ('device', { + 'queryset': Device.objects.prefetch_related( + 'device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack', 'primary_ip4', 'primary_ip6', + ), + 'filterset': DeviceFilterSet, + 'table': DeviceTable, + 'url': 'dcim:device_list', + }), + ('virtualchassis', { + 'queryset': VirtualChassis.objects.prefetch_related('master').annotate( + member_count=count_related(Device, 'virtual_chassis') + ), + 'filterset': VirtualChassisFilterSet, + 'table': VirtualChassisTable, + 'url': 'dcim:virtualchassis_list', + }), + ('cable', { + 'queryset': Cable.objects.all(), + 'filterset': CableFilterSet, + 'table': CableTable, + 'url': 'dcim:cable_list', + }), + ('powerfeed', { + 'queryset': PowerFeed.objects.all(), + 'filterset': PowerFeedFilterSet, + 'table': PowerFeedTable, + 'url': 'dcim:powerfeed_list', + }), + ) +) + +IPAM_TYPES = OrderedDict( + ( + ('vrf', { + 'queryset': VRF.objects.prefetch_related('tenant'), + 'filterset': VRFFilterSet, + 'table': VRFTable, + 'url': 'ipam:vrf_list', + }), + ('aggregate', { + 'queryset': Aggregate.objects.prefetch_related('rir'), + 'filterset': AggregateFilterSet, + 'table': AggregateTable, + 'url': 'ipam:aggregate_list', + }), + ('prefix', { + 'queryset': Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role'), + 'filterset': PrefixFilterSet, + 'table': PrefixTable, + 'url': 'ipam:prefix_list', + }), + ('ipaddress', { + 'queryset': IPAddress.objects.prefetch_related('vrf__tenant', 'tenant'), + 'filterset': IPAddressFilterSet, + 'table': IPAddressTable, + 'url': 'ipam:ipaddress_list', + }), + ('vlan', { + 'queryset': VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role'), + 'filterset': VLANFilterSet, + 'table': VLANTable, + 'url': 'ipam:vlan_list', + }), + ('asn', { + 'queryset': ASN.objects.prefetch_related('rir', 'tenant'), + 'filterset': ASNFilterSet, + 'table': ASNTable, + 'url': 'ipam:asn_list', + }), + ) +) + +TENANCY_TYPES = OrderedDict( + ( + ('tenant', { + 'queryset': Tenant.objects.prefetch_related('group'), + 'filterset': TenantFilterSet, + 'table': TenantTable, + 'url': 'tenancy:tenant_list', + }), + ('contact', { + 'queryset': Contact.objects.prefetch_related('group', 'assignments').annotate( + assignment_count=count_related(ContactAssignment, 'contact')), + 'filterset': ContactFilterSet, + 'table': ContactTable, + 'url': 'tenancy:contact_list', + }), + ) +) + +VIRTUALIZATION_TYPES = OrderedDict( + ( + ('cluster', { + 'queryset': Cluster.objects.prefetch_related('type', 'group').annotate( + device_count=count_related(Device, 'cluster'), + vm_count=count_related(VirtualMachine, 'cluster') + ), + 'filterset': ClusterFilterSet, + 'table': ClusterTable, + 'url': 'virtualization:cluster_list', + }), + ('virtualmachine', { + 'queryset': VirtualMachine.objects.prefetch_related( + 'cluster', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', + ), + 'filterset': VirtualMachineFilterSet, + 'table': VirtualMachineTable, + 'url': 'virtualization:virtualmachine_list', + }), + ) +) + +SEARCH_TYPE_HIERARCHY = OrderedDict( + ( + ("Circuits", CIRCUIT_TYPES), + ("DCIM", DCIM_TYPES), + ("IPAM", IPAM_TYPES), + ("Tenancy", TENANCY_TYPES), + ("Virtualization", VIRTUALIZATION_TYPES), + ) +) + + +def build_search_types() -> Dict[str, Dict]: + result = dict() + + for app_types in SEARCH_TYPE_HIERARCHY.values(): + for name, items in app_types.items(): + result[name] = items + + return result + + +SEARCH_TYPES = build_search_types() diff --git a/netbox/netbox/forms.py b/netbox/netbox/forms.py index b5d68c1fc..d220527fa 100644 --- a/netbox/netbox/forms.py +++ b/netbox/netbox/forms.py @@ -1,39 +1,24 @@ from django import forms from utilities.forms import BootstrapMixin +from netbox.constants import SEARCH_TYPE_HIERARCHY -OBJ_TYPE_CHOICES = ( - ('', 'All Objects'), - ('Circuits', ( - ('provider', 'Providers'), - ('circuit', 'Circuits'), - )), - ('DCIM', ( - ('site', 'Sites'), - ('rack', 'Racks'), - ('rackreservation', 'Rack reservations'), - ('location', 'Locations'), - ('devicetype', 'Device Types'), - ('device', 'Devices'), - ('virtualchassis', 'Virtual chassis'), - ('cable', 'Cables'), - ('powerfeed', 'Power feeds'), - )), - ('IPAM', ( - ('vrf', 'VRFs'), - ('aggregate', 'Aggregates'), - ('prefix', 'Prefixes'), - ('ipaddress', 'IP Addresses'), - ('vlan', 'VLANs'), - )), - ('Tenancy', ( - ('tenant', 'Tenants'), - )), - ('Virtualization', ( - ('cluster', 'Clusters'), - ('virtualmachine', 'Virtual Machines'), - )), -) + +def build_search_choices(): + result = list() + result.append(('', 'All Objects')) + for category, items in SEARCH_TYPE_HIERARCHY.items(): + subcategories = list() + for slug, obj in items.items(): + name = obj['queryset'].model._meta.verbose_name_plural + name = name[0].upper() + name[1:] + subcategories.append((slug, name)) + result.append((category, tuple(subcategories))) + + return tuple(result) + + +OBJ_TYPE_CHOICES = build_search_choices() def build_options(): From 36d6dd1ca93a531b29b6849c2ad8d05f29dc72f2 Mon Sep 17 00:00:00 2001 From: Alex Gittings Date: Thu, 24 Feb 2022 17:08:38 +0000 Subject: [PATCH 02/23] 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 8cd24b1a6703791fac7dc605eec04115e19426ee Mon Sep 17 00:00:00 2001 From: thatmattlove Date: Tue, 8 Mar 2022 14:28:52 -0700 Subject: [PATCH 03/23] Fixes #8820: correct navbar color in dark mode --- docs/release-notes/version-3.1.md | 2 ++ netbox/project-static/dist/netbox-dark.css | Bin 374438 -> 374410 bytes netbox/project-static/styles/theme-dark.scss | 4 ++-- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index 4f00434d7..3b02b3c42 100644 --- a/docs/release-notes/version-3.1.md +++ b/docs/release-notes/version-3.1.md @@ -2,6 +2,8 @@ ## v3.1.10 (FUTURE) +* [#8820](https://github.com/netbox-community/netbox/issues/8820) - Fix navbar background color in dark mode + --- ## v3.1.9 (2022-03-07) diff --git a/netbox/project-static/dist/netbox-dark.css b/netbox/project-static/dist/netbox-dark.css index 2c8931027c1902b0279ac476ad92f3463e3efd90..b929f176a6c67c2067c4f29044f724d50ccbd60b 100644 GIT binary patch delta 135 zcmZ4XR;=r-SVIfr7N)QI(;L^Z>P{~(U{aXAQ-DclJCN1N$ZnWqm}X>TJzbHN)na;r z925KW3kFPl)4h$D+Bj8>j7=;}4NT1^>u*z-&S%Ut5pIT1`vYSpAZ7+)mhBIWS-GvJ e_cbuHO>bYvsytnwn3ZSxw0u^{?bi9M+3o<$`75CS delta 119 zcmeBrE4J*dSVIfr7N)QI(|b&rjHjP7V3MAG)0D{&%-Y^-z?8-~z0sUWe7e6ZtJd@n z#!O<{Z<;e%GET44XX2Y)ZOp_oUE30<5yWjjX3PY{%s|Yt{g^Q;w>8L6?dbuftUS}J O@>wOf%jdIZy8{5qNhf9i diff --git a/netbox/project-static/styles/theme-dark.scss b/netbox/project-static/styles/theme-dark.scss index b86acc17a..c0933e991 100644 --- a/netbox/project-static/styles/theme-dark.scss +++ b/netbox/project-static/styles/theme-dark.scss @@ -145,9 +145,9 @@ $nav-tabs-link-active-border-color: $gray-800 $gray-800 $nav-tabs-link-active-bg $nav-pills-link-active-color: $component-active-color; $nav-pills-link-active-bg: $component-active-bg; -$navbar-light-color: $navbar-dark-color; -$navbar-light-toggler-icon-bg: url("data:image/svg+xml,"); +$navbar-light-color: $darker; $navbar-light-toggler-border-color: $gray-700; +$navbar-light-toggler-icon-bg: url("data:image/svg+xml,"); // Dropdowns $dropdown-color: $body-color; From bf22b820bf9e4c70b7cb420c9ef7c1660da09c29 Mon Sep 17 00:00:00 2001 From: Alex Gittings Date: Wed, 9 Mar 2022 16:35:47 +0000 Subject: [PATCH 04/23] 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 05/23] 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 06/23] 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 07/23] 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 08/23] 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 09/23] 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 4eb7cd06b4697061e51927dfa86ce2921192ab30 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 14 Mar 2022 09:46:40 -0400 Subject: [PATCH 10/23] Adjust font size for serial number under device status view --- netbox/templates/dcim/device/status.html | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/netbox/templates/dcim/device/status.html b/netbox/templates/dcim/device/status.html index 8cbc0979d..a668ebf1e 100644 --- a/netbox/templates/dcim/device/status.html +++ b/netbox/templates/dcim/device/status.html @@ -37,9 +37,7 @@ Serial Number - - - + OS Version From faba6c9bdcaa26e7d29f1c461e8023f8ad013e5e Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 14 Mar 2022 09:54:11 -0400 Subject: [PATCH 11/23] Fixes #8850: Show airflow field on device REST API serializer when config context data is included --- docs/release-notes/version-3.1.md | 3 +++ netbox/dcim/api/serializers.py | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index 3b02b3c42..ed713102d 100644 --- a/docs/release-notes/version-3.1.md +++ b/docs/release-notes/version-3.1.md @@ -2,7 +2,10 @@ ## v3.1.10 (FUTURE) +### Bug Fixes + * [#8820](https://github.com/netbox-community/netbox/issues/8820) - Fix navbar background color in dark mode +* [#8850](https://github.com/netbox-community/netbox/issues/8850) - Show airflow field on device REST API serializer when config context data is included --- diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 6549e51a1..2d555e756 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -497,9 +497,9 @@ class DeviceWithConfigContextSerializer(DeviceSerializer): class Meta(DeviceSerializer.Meta): 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', - 'tags', 'custom_fields', 'config_context', 'created', 'last_updated', + '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', 'config_context', 'created', 'last_updated', ] @swagger_serializer_method(serializer_or_field=serializers.DictField) From 1add5accf2790d880b3ca98c75619103630c701f Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 14 Mar 2022 09:57:51 -0400 Subject: [PATCH 12/23] Fixes #8844: Correct VLAN ID max value --- netbox/ipam/filtersets.py | 2 +- netbox/ipam/forms/bulk_import.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py index 5342d223d..07514f38b 100644 --- a/netbox/ipam/filtersets.py +++ b/netbox/ipam/filtersets.py @@ -334,7 +334,7 @@ class PrefixFilterSet(PrimaryModelFilterSet, TenancyFilterSet): ) vlan_vid = django_filters.NumberFilter( field_name='vlan__vid', - label='VLAN number (1-4095)', + label='VLAN number (1-4094)', ) role_id = django_filters.ModelMultipleChoiceFilter( queryset=Role.objects.all(), diff --git a/netbox/ipam/forms/bulk_import.py b/netbox/ipam/forms/bulk_import.py index 65fc35c34..c7778817f 100644 --- a/netbox/ipam/forms/bulk_import.py +++ b/netbox/ipam/forms/bulk_import.py @@ -375,7 +375,7 @@ class VLANCSVForm(CustomFieldModelCSVForm): model = VLAN fields = ('site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description') help_texts = { - 'vid': 'Numeric VLAN ID (1-4095)', + 'vid': 'Numeric VLAN ID (1-4094)', 'name': 'VLAN name', } From a143eca57d756a5b2786f96fd6ec09c3d2ff272b Mon Sep 17 00:00:00 2001 From: Alex Gittings Date: Tue, 15 Mar 2022 00:02:16 +0000 Subject: [PATCH 13/23] Closes: #8575 Implement rack_a and rack_b for cable table --- netbox/dcim/tables/cables.py | 16 ++++++++++++++-- .../templates/dcim/inc/cable_termination.html | 18 +++++++++++++++++- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/netbox/dcim/tables/cables.py b/netbox/dcim/tables/cables.py index 9f2c08342..ebd0b8ebc 100644 --- a/netbox/dcim/tables/cables.py +++ b/netbox/dcim/tables/cables.py @@ -23,6 +23,12 @@ class CableTable(BaseTable): orderable=False, verbose_name='Side A' ) + rack_a = tables.Column( + accessor=Accessor('termination_a__device__rack'), + orderable=False, + linkify=True, + verbose_name='Rack A' + ) termination_a = tables.Column( accessor=Accessor('termination_a'), orderable=False, @@ -35,6 +41,12 @@ class CableTable(BaseTable): orderable=False, verbose_name='Side B' ) + rack_b = tables.Column( + accessor=Accessor('termination_b__device__rack'), + orderable=False, + linkify=True, + verbose_name='Rack B' + ) termination_b = tables.Column( accessor=Accessor('termination_b'), orderable=False, @@ -55,10 +67,10 @@ class CableTable(BaseTable): class Meta(BaseTable.Meta): model = Cable fields = ( - 'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b', + 'pk', 'id', 'label', 'termination_a_parent', 'rack_a', 'termination_a', 'termination_b_parent', 'rack_b', 'termination_b', 'status', 'type', 'tenant', 'color', 'length', 'tags', 'created', 'last_updated', ) default_columns = ( 'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b', 'status', 'type', - ) + ) \ No newline at end of file diff --git a/netbox/templates/dcim/inc/cable_termination.html b/netbox/templates/dcim/inc/cable_termination.html index 1ba3d05c9..967d733de 100644 --- a/netbox/templates/dcim/inc/cable_termination.html +++ b/netbox/templates/dcim/inc/cable_termination.html @@ -8,6 +8,22 @@ {{ termination.device }} + {% if termination.device.site %} + + Site + + {{ termination.device.site }} + + + {% endif %} + {% if termination.device.rack %} + + Rack + + {{ termination.device.rack }} + + + {% endif %} Type @@ -35,4 +51,4 @@ {% endif %} - + \ No newline at end of file From bdbfff911bfbe85caf2c4c7509b63f847a82dd2c Mon Sep 17 00:00:00 2001 From: Alex Gittings Date: Tue, 15 Mar 2022 00:04:22 +0000 Subject: [PATCH 14/23] add new line --- netbox/templates/dcim/inc/cable_termination.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/templates/dcim/inc/cable_termination.html b/netbox/templates/dcim/inc/cable_termination.html index 967d733de..c9f3f0d4a 100644 --- a/netbox/templates/dcim/inc/cable_termination.html +++ b/netbox/templates/dcim/inc/cable_termination.html @@ -51,4 +51,4 @@ {% endif %} - \ No newline at end of file + From 9548cf32ffc370af2bc6b65af0ae470fb31960b3 Mon Sep 17 00:00:00 2001 From: Alex Gittings Date: Tue, 15 Mar 2022 00:05:10 +0000 Subject: [PATCH 15/23] add new line --- netbox/dcim/tables/cables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/tables/cables.py b/netbox/dcim/tables/cables.py index ebd0b8ebc..97b54bf41 100644 --- a/netbox/dcim/tables/cables.py +++ b/netbox/dcim/tables/cables.py @@ -73,4 +73,4 @@ class CableTable(BaseTable): default_columns = ( 'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b', 'status', 'type', - ) \ No newline at end of file + ) From fd6d3205d0fe2d17931da1fd40489a7f15036083 Mon Sep 17 00:00:00 2001 From: "Sean M. Collins" Date: Wed, 16 Mar 2022 11:45:14 -0400 Subject: [PATCH 16/23] Update GitHub link for Netaddr The project was renamed/moved to a new location in GitHub and we should update the link in case the redirect stops functioning --- base_requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base_requirements.txt b/base_requirements.txt index 0b8365e0e..247a37a41 100644 --- a/base_requirements.txt +++ b/base_requirements.txt @@ -83,7 +83,7 @@ markdown-include mkdocs-material # Library for manipulating IP prefixes and addresses -# https://github.com/drkjam/netaddr +# https://github.com/netaddr/netaddr netaddr # Fork of PIL (Python Imaging Library) for image processing From 585b5a221d9ce423122b7e2dfff8823351732cb8 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 17 Mar 2022 17:06:21 -0400 Subject: [PATCH 17/23] Changelog for #8553 --- docs/release-notes/version-3.1.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index ed713102d..ed283ed0a 100644 --- a/docs/release-notes/version-3.1.md +++ b/docs/release-notes/version-3.1.md @@ -2,6 +2,10 @@ ## v3.1.10 (FUTURE) +### Enhancements + +* [#8553](https://github.com/netbox-community/netbox/issues/8553) - Add missing object types to global search form + ### Bug Fixes * [#8820](https://github.com/netbox-community/netbox/issues/8820) - Fix navbar background color in dark mode From 5abde866f10de182704fe85900eaa01ee0cdb666 Mon Sep 17 00:00:00 2001 From: Alex Gittings Date: Mon, 14 Mar 2022 23:24:29 +0000 Subject: [PATCH 18/23] Closes: #8457 - implement nonracked devices on locations and sites --- netbox/dcim/views.py | 12 ++++ .../templates/dcim/inc/nonracked_devices.html | 62 +++++++++++++++++++ netbox/templates/dcim/location.html | 1 + netbox/templates/dcim/rack.html | 45 +------------- netbox/templates/dcim/site.html | 1 + 5 files changed, 77 insertions(+), 44 deletions(-) create mode 100644 netbox/templates/dcim/inc/nonracked_devices.html diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index cee516f5c..87c4828d5 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -328,6 +328,11 @@ class SiteView(generic.ObjectView): 'device_count', cumulative=True ).restrict(request.user, 'view').filter(site=instance) + nonracked_devices = Device.objects.filter( + site=instance, + position__isnull=True, + parent_bay__isnull=True + ).prefetch_related('device_type__manufacturer') asns = ASN.objects.restrict(request.user, 'view').filter(sites=instance) asn_count = asns.count() @@ -338,6 +343,7 @@ class SiteView(generic.ObjectView): 'stats': stats, 'locations': locations, 'asns': asns, + 'nonracked_devices': nonracked_devices, } @@ -415,11 +421,17 @@ class LocationView(generic.ObjectView): ).filter(pk__in=location_ids).exclude(pk=instance.pk) child_locations_table = tables.LocationTable(child_locations) paginate_table(child_locations_table, request) + nonracked_devices = Device.objects.filter( + location=instance, + position__isnull=True, + parent_bay__isnull=True + ).prefetch_related('device_type__manufacturer') return { 'rack_count': rack_count, 'device_count': device_count, 'child_locations_table': child_locations_table, + 'nonracked_devices': nonracked_devices, } diff --git a/netbox/templates/dcim/inc/nonracked_devices.html b/netbox/templates/dcim/inc/nonracked_devices.html new file mode 100644 index 000000000..f1b669eb9 --- /dev/null +++ b/netbox/templates/dcim/inc/nonracked_devices.html @@ -0,0 +1,62 @@ +{% load helpers %} + +
+
+ Non-Racked Devices +
+
+{% if nonracked_devices %} + + + + + + + + {% for device in nonracked_devices %} + + + + + {% if device.parent_bay %} + + + {% else %} + + {% endif %} + + {% endfor %} +
NameRoleTypeParent Device
+ {{ device }} + {{ device.device_role }}{{ device.device_type }}{{ device.parent_bay.device }}{{ device.parent_bay }}
+ {% else %} +
+ None +
+ {% endif %} +
+ {% if perms.dcim.add_device %} + {% if object|meta:'verbose_name' == 'rack' %} + + {% elif object|meta:'verbose_name' == 'site' %} + + {% elif object|meta:'verbose_name' == 'location' %} + + {% endif %} + {% endif %} +
\ No newline at end of file diff --git a/netbox/templates/dcim/location.html b/netbox/templates/dcim/location.html index b684385a7..43bbfd114 100644 --- a/netbox/templates/dcim/location.html +++ b/netbox/templates/dcim/location.html @@ -90,6 +90,7 @@
{% include 'inc/panels/custom_fields.html' %} {% include 'inc/panels/contacts.html' %} + {% include 'dcim/inc/nonracked_devices.html' %} {% include 'inc/panels/image_attachments.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index 93bd21fd9..4eb94a0ce 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -288,50 +288,7 @@ -
-
- Non-Racked Devices -
-
- {% if nonracked_devices %} - - - - - - - - {% for device in nonracked_devices %} - - - - - {% if device.parent_bay %} - - - {% else %} - - {% endif %} - - {% endfor %} -
NameRoleTypeParent Device
- {{ device }} - {{ device.device_role }}{{ device.device_type }}{{ device.parent_bay.device }}{{ device.parent_bay }}
- {% else %} -
- None -
- {% endif %} -
- {% if perms.dcim.add_device %} - - {% endif %} -
+ {% include 'dcim/inc/nonracked_devices.html' %} {% include 'inc/panels/contacts.html' %} {% plugin_right_page object %} diff --git a/netbox/templates/dcim/site.html b/netbox/templates/dcim/site.html index f71105d1b..aa17fd57f 100644 --- a/netbox/templates/dcim/site.html +++ b/netbox/templates/dcim/site.html @@ -277,6 +277,7 @@ + {% include 'dcim/inc/nonracked_devices.html' %} {% include 'inc/panels/contacts.html' %}
Locations
From da37db1ea9411cd8dcf34767a39dcc02177b225b Mon Sep 17 00:00:00 2001 From: minitriga Date: Fri, 18 Mar 2022 14:39:22 +0000 Subject: [PATCH 19/23] 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 20/23] 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 21/23] 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' ) From 900825a2affdff1e51cc23853ecf3f5d16360543 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 18 Mar 2022 11:46:49 -0400 Subject: [PATCH 22/23] Changelog for #8457, #8575, #8645 --- docs/release-notes/version-3.1.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index ed283ed0a..6279a109f 100644 --- a/docs/release-notes/version-3.1.md +++ b/docs/release-notes/version-3.1.md @@ -4,7 +4,10 @@ ### Enhancements +* [#8457](https://github.com/netbox-community/netbox/issues/8457) - Enable adding non-racked devices from site & location views * [#8553](https://github.com/netbox-community/netbox/issues/8553) - Add missing object types to global search form +* [#8575](https://github.com/netbox-community/netbox/issues/8575) - Add rack columns to cables list +* [#8645](https://github.com/netbox-community/netbox/issues/8645) - Enable filtering objects by assigned contacts & contact roles ### Bug Fixes From 9a0bb14e762fe20daf0aa12e4410fb05cd8d74ca Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 18 Mar 2022 11:47:43 -0400 Subject: [PATCH 23/23] Install tblib to fix tracebacks during parallel test runs --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9ba75118b..b12c80eac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,7 +56,7 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt - pip install pycodestyle coverage + pip install pycodestyle coverage tblib ln -s configuration.testing.py netbox/netbox/configuration.py - name: Build documentation