From 538984c6d2a806f2de4e1af4e5d5f510840c6929 Mon Sep 17 00:00:00 2001 From: Johannes Erwerle Date: Mon, 21 Feb 2022 12:26:12 +0100 Subject: [PATCH 01/45] 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/45] 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 c515218760de802001b2a4ef8d6f887eae135e47 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 7 Mar 2022 10:07:07 -0500 Subject: [PATCH 03/45] PRVB --- docs/release-notes/version-3.1.md | 4 ++++ netbox/netbox/settings.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index 5decff1b3..4f00434d7 100644 --- a/docs/release-notes/version-3.1.md +++ b/docs/release-notes/version-3.1.md @@ -1,5 +1,9 @@ # NetBox v3.1 +## v3.1.10 (FUTURE) + +--- + ## v3.1.9 (2022-03-07) ### Enhancements diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 02d52664f..d16e00337 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -19,7 +19,7 @@ from netbox.config import PARAMS # Environment setup # -VERSION = '3.1.9' +VERSION = '3.1.10-dev' # Hostname HOSTNAME = platform.node() From 8cd24b1a6703791fac7dc605eec04115e19426ee Mon Sep 17 00:00:00 2001 From: thatmattlove Date: Tue, 8 Mar 2022 14:28:52 -0700 Subject: [PATCH 04/45] 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 05/45] 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 06/45] 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 07/45] 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 08/45] 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 09/45] 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 10/45] 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 11/45] 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 12/45] 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 13/45] 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 14/45] 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 15/45] 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 16/45] 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 17/45] 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 18/45] 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 19/45] 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 20/45] 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 21/45] 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 22/45] 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 23/45] 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 24/45] 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 From 0da04232f3fba9abee90fe83ff05e18eaa972038 Mon Sep 17 00:00:00 2001 From: PieterL75 <74899468+PieterL75@users.noreply.github.com> Date: Fri, 18 Mar 2022 18:23:39 +0100 Subject: [PATCH 25/45] Fixes #8813 Retain search value after submitting (#8907) * Fixes #8813 Retain search value after submitting * remove autofocus from searchbar Co-authored-by: Pieter Lambrecht --- netbox/templates/base/layout.html | 4 ++-- netbox/utilities/templates/search/searchbar.html | 2 +- netbox/utilities/templatetags/search.py | 7 +++++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/netbox/templates/base/layout.html b/netbox/templates/base/layout.html index da2d10c65..0def1c90e 100644 --- a/netbox/templates/base/layout.html +++ b/netbox/templates/base/layout.html @@ -33,7 +33,7 @@
- {% search_options %} + {% search_options request %}
@@ -45,7 +45,7 @@ {# Search bar #}
- {% search_options %} + {% search_options request %}
{# Proflie/login button #} diff --git a/netbox/utilities/templates/search/searchbar.html b/netbox/utilities/templates/search/searchbar.html index d71fd8e69..74d12e9b9 100644 --- a/netbox/utilities/templates/search/searchbar.html +++ b/netbox/utilities/templates/search/searchbar.html @@ -5,7 +5,7 @@ aria-label="Search" placeholder="Search" class="form-control" - value="{{ request.GET.q }}" + value="{{ request.GET.q|escape }}" /> diff --git a/netbox/utilities/templatetags/search.py b/netbox/utilities/templatetags/search.py index aad533e7e..5726ae5d5 100644 --- a/netbox/utilities/templatetags/search.py +++ b/netbox/utilities/templatetags/search.py @@ -8,6 +8,9 @@ search_form = SearchForm() @register.inclusion_tag("search/searchbar.html") -def search_options() -> Dict: +def search_options(request) -> Dict: """Provide search options to template.""" - return {"options": search_form.options} + return { + 'options': search_form.options, + 'request': request, + } From f64987d0c4ddef473f997bfa71c8bca3d6adbf09 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 18 Mar 2022 13:25:47 -0400 Subject: [PATCH 26/45] Changelog for #8813 --- docs/release-notes/version-3.1.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index 6279a109f..9e6d8ad96 100644 --- a/docs/release-notes/version-3.1.md +++ b/docs/release-notes/version-3.1.md @@ -11,6 +11,7 @@ ### Bug Fixes +* [#8813](https://github.com/netbox-community/netbox/issues/8813) - Retain global search bar query after submitting * [#8820](https://github.com/netbox-community/netbox/issues/8820) - Fix navbar background color in dark mode * [#8850](https://github.com/netbox-community/netbox/issues/8850) - Show airflow field on device REST API serializer when config context data is included From 22980cea7ba264b42f1a119235e6928e5c8f5f40 Mon Sep 17 00:00:00 2001 From: kkthxbye <> Date: Mon, 21 Mar 2022 10:46:51 +0100 Subject: [PATCH 27/45] Speed up rendering of the script list --- netbox/extras/views.py | 2 ++ netbox/templates/extras/script_list.html | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/netbox/extras/views.py b/netbox/extras/views.py index cc53c465d..ae43ac489 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -753,6 +753,8 @@ class ScriptListView(ContentTypePermissionRequiredMixin, View): for _scripts in scripts.values(): for script in _scripts.values(): + # Prevent django from instantiating the class on all accesses + script.do_not_call_in_templates = True script.result = results.get(script.full_name) return render(request, 'extras/script_list.html', { diff --git a/netbox/templates/extras/script_list.html b/netbox/templates/extras/script_list.html index ccbdca705..a6f6833c9 100644 --- a/netbox/templates/extras/script_list.html +++ b/netbox/templates/extras/script_list.html @@ -34,7 +34,7 @@ {% for class_name, script in module_scripts.items %} - {{ script }} + {{ script.Meta.name }} {% include 'extras/inc/job_label.html' with result=script.result %} From e6980626d87c4a9d3df6f862e8caf2e9fa683692 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 22 Mar 2022 09:37:57 -0400 Subject: [PATCH 28/45] Fixes #8932: Fix error when setting null value for interface rf_role via REST API --- docs/release-notes/version-3.1.md | 1 + netbox/dcim/api/serializers.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index 9e6d8ad96..1637a18a1 100644 --- a/docs/release-notes/version-3.1.md +++ b/docs/release-notes/version-3.1.md @@ -14,6 +14,7 @@ * [#8813](https://github.com/netbox-community/netbox/issues/8813) - Retain global search bar query after submitting * [#8820](https://github.com/netbox-community/netbox/issues/8820) - Fix navbar background color in dark mode * [#8850](https://github.com/netbox-community/netbox/issues/8850) - Show airflow field on device REST API serializer when config context data is included +* [#8932](https://github.com/netbox-community/netbox/issues/8932) - Fix error when setting null value for interface `rf_role` via REST API --- diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 2d555e756..6be27217c 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -619,8 +619,8 @@ class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, Con parent = NestedInterfaceSerializer(required=False, allow_null=True) bridge = NestedInterfaceSerializer(required=False, allow_null=True) lag = NestedInterfaceSerializer(required=False, allow_null=True) - mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False) - rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_null=True) + mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_blank=True) + rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_blank=True) rf_channel = ChoiceField(choices=WirelessChannelChoices, required=False, allow_blank=True) untagged_vlan = NestedVLANSerializer(required=False, allow_null=True) tagged_vlans = SerializedPKRelatedField( From 197dfca5b2d181369b90e40704ac9188d149a688 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 22 Mar 2022 09:50:38 -0400 Subject: [PATCH 29/45] Fixes #8935: Correct ordering of next/previous racks to use naturalized names --- docs/release-notes/version-3.1.md | 1 + netbox/dcim/views.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index 1637a18a1..6bfd28bca 100644 --- a/docs/release-notes/version-3.1.md +++ b/docs/release-notes/version-3.1.md @@ -15,6 +15,7 @@ * [#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 * [#8932](https://github.com/netbox-community/netbox/issues/8932) - Fix error when setting null value for interface `rf_role` via REST API +* [#8935](https://github.com/netbox-community/netbox/issues/8935) - Correct ordering of next/previous racks to use naturalized names --- diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 87c4828d5..6697a44cc 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -609,8 +609,8 @@ class RackView(generic.ObjectView): peer_racks = peer_racks.filter(location=instance.location) else: peer_racks = peer_racks.filter(location__isnull=True) - next_rack = peer_racks.filter(name__gt=instance.name).order_by('name').first() - prev_rack = peer_racks.filter(name__lt=instance.name).order_by('-name').first() + next_rack = peer_racks.filter(_name__gt=instance._name).first() + prev_rack = peer_racks.filter(_name__lt=instance._name).reverse().first() reservations = RackReservation.objects.restrict(request.user, 'view').filter(rack=instance) power_feeds = PowerFeed.objects.restrict(request.user, 'view').filter(rack=instance).prefetch_related( From 5f89226cd7ba8325cded61b1396e35dc009cb8c5 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 22 Mar 2022 10:59:43 -0400 Subject: [PATCH 30/45] Update testing instructions --- docs/development/getting-started.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/development/getting-started.md b/docs/development/getting-started.md index 0e892bd4a..3c02c01bf 100644 --- a/docs/development/getting-started.md +++ b/docs/development/getting-started.md @@ -124,8 +124,10 @@ The demo data is provided in JSON format and loaded into an empty database using Prior to committing any substantial changes to the code base, be sure to run NetBox's test suite to catch any potential errors. Tests are run using the `test` management command. Remember to ensure the Python virtual environment is active before running this command. Also keep in mind that these commands are executed in the `/netbox/` directory, not the root directory of the repository. +When running tests, it's advised to use the special testing configuration file that ships with NetBox. This ensures that tests are run with the same configuration parameters consistently. To override your local configuration when running tests, set the `NETBOX_CONFIGURATION` environment variable to `netbox.configuration_testing`. + ```no-highlight -$ python manage.py test +$ NETBOX_CONFIGURATION=netbox.configuration_testing python manage.py test ``` In cases where you haven't made any changes to the database (which is most of the time), you can append the `--keepdb` argument to this command to reuse the test database between runs. This cuts down on the time it takes to run the test suite since the database doesn't have to be rebuilt each time. (Note that this argument will cause errors if you've modified any model fields since the previous test run.) From 41efad4056b05255f8a85ffa956b3edbeb626783 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 22 Mar 2022 11:39:26 -0400 Subject: [PATCH 31/45] Fixes #8919: Fix filtering of VLAN groups by site under prefix edit form --- docs/release-notes/version-3.1.md | 1 + netbox/ipam/forms/models.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index 6bfd28bca..22c5b0da1 100644 --- a/docs/release-notes/version-3.1.md +++ b/docs/release-notes/version-3.1.md @@ -14,6 +14,7 @@ * [#8813](https://github.com/netbox-community/netbox/issues/8813) - Retain global search bar query after submitting * [#8820](https://github.com/netbox-community/netbox/issues/8820) - Fix navbar background color in dark mode * [#8850](https://github.com/netbox-community/netbox/issues/8850) - Show airflow field on device REST API serializer when config context data is included +* [#8919](https://github.com/netbox-community/netbox/issues/8919) - Fix filtering of VLAN groups by site under prefix edit form * [#8932](https://github.com/netbox-community/netbox/issues/8932) - Fix error when setting null value for interface `rf_role` via REST API * [#8935](https://github.com/netbox-community/netbox/issues/8935) - Correct ordering of next/previous racks to use naturalized names diff --git a/netbox/ipam/forms/models.py b/netbox/ipam/forms/models.py index 08138f4fe..6d51e9bb3 100644 --- a/netbox/ipam/forms/models.py +++ b/netbox/ipam/forms/models.py @@ -222,7 +222,7 @@ class PrefixForm(TenancyForm, CustomFieldModelForm): label='VLAN group', null_option='None', query_params={ - 'site_id': '$site' + 'site': '$site' }, initial_params={ 'vlans': '$vlan' From ae46cd33b633d9248e38a68ea11e896e35eb3400 Mon Sep 17 00:00:00 2001 From: kkthxbye <> Date: Wed, 23 Mar 2022 12:18:14 +0100 Subject: [PATCH 32/45] - Move do_not_call_in_templates to BaseScript - Fix the name classproperty --- netbox/extras/scripts.py | 6 +++++- netbox/extras/views.py | 2 -- netbox/templates/extras/script_list.html | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/netbox/extras/scripts.py b/netbox/extras/scripts.py index fb3c6558a..f80dfaefa 100644 --- a/netbox/extras/scripts.py +++ b/netbox/extras/scripts.py @@ -259,6 +259,10 @@ class BaseScript: Base model for custom scripts. User classes should inherit from this model if they want to extend Script functionality for use in other subclasses. """ + + # Prevent django from instantiating the class on all accesses + do_not_call_in_templates = True + class Meta: pass @@ -280,7 +284,7 @@ class BaseScript: @classproperty def name(self): - return getattr(self.Meta, 'name', self.__class__.__name__) + return getattr(self.Meta, 'name', self.__name__) @classproperty def full_name(self): diff --git a/netbox/extras/views.py b/netbox/extras/views.py index ae43ac489..cc53c465d 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -753,8 +753,6 @@ class ScriptListView(ContentTypePermissionRequiredMixin, View): for _scripts in scripts.values(): for script in _scripts.values(): - # Prevent django from instantiating the class on all accesses - script.do_not_call_in_templates = True script.result = results.get(script.full_name) return render(request, 'extras/script_list.html', { diff --git a/netbox/templates/extras/script_list.html b/netbox/templates/extras/script_list.html index a6f6833c9..1c502aacd 100644 --- a/netbox/templates/extras/script_list.html +++ b/netbox/templates/extras/script_list.html @@ -34,7 +34,7 @@ {% for class_name, script in module_scripts.items %} - {{ script.Meta.name }} + {{ script.name }} {% include 'extras/inc/job_label.html' with result=script.result %} From e09ab79a1ac508779b715f5c7fb00412d1b57308 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 23 Mar 2022 17:01:57 -0400 Subject: [PATCH 33/45] Changelog for #8924 --- docs/release-notes/version-3.1.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index 22c5b0da1..5c61ec3d2 100644 --- a/docs/release-notes/version-3.1.md +++ b/docs/release-notes/version-3.1.md @@ -15,6 +15,7 @@ * [#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 * [#8919](https://github.com/netbox-community/netbox/issues/8919) - Fix filtering of VLAN groups by site under prefix edit form +* [#8924](https://github.com/netbox-community/netbox/issues/8924) - Improve load time of custom script list * [#8932](https://github.com/netbox-community/netbox/issues/8932) - Fix error when setting null value for interface `rf_role` via REST API * [#8935](https://github.com/netbox-community/netbox/issues/8935) - Correct ordering of next/previous racks to use naturalized names From 6ceb78fd4ce16a3dc88fff18d7892e91c4a87611 Mon Sep 17 00:00:00 2001 From: Johannes Erwerle Date: Thu, 24 Mar 2022 09:28:17 +0100 Subject: [PATCH 34/45] Fixed #8952: rack rear faces link not clickable --- netbox/dcim/svg.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/netbox/dcim/svg.py b/netbox/dcim/svg.py index e333320b6..7cd0fa417 100644 --- a/netbox/dcim/svg.py +++ b/netbox/dcim/svg.py @@ -146,10 +146,10 @@ class RackElevationSVG: class_='device-image' ) image.fit(scale='slice') - drawing.add(image) - drawing.add(drawing.text(get_device_name(device), insert=text, stroke='black', - stroke_width='0.2em', stroke_linejoin='round', class_='device-image-label')) - drawing.add(drawing.text(get_device_name(device), insert=text, fill='white', class_='device-image-label')) + link.add(image) + link.add(drawing.text(get_device_name(device), insert=text, stroke='black', + stroke_width='0.2em', stroke_linejoin='round', class_='device-image-label')) + link.add(drawing.text(get_device_name(device), insert=text, fill='white', class_='device-image-label')) @staticmethod def _draw_empty(drawing, rack, start, end, text, id_, face_id, class_, reservation): From 30a6dc2f6418a3d951f060798ca91771371e5daa Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 24 Mar 2022 10:34:09 -0400 Subject: [PATCH 35/45] Fixes #8951: Allow changing device type & platform to different manufacturer simultaneously --- docs/release-notes/version-3.1.md | 1 + netbox/dcim/forms/models.py | 5 ----- netbox/dcim/models/devices.py | 4 ++-- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index 5c61ec3d2..95034cb12 100644 --- a/docs/release-notes/version-3.1.md +++ b/docs/release-notes/version-3.1.md @@ -18,6 +18,7 @@ * [#8924](https://github.com/netbox-community/netbox/issues/8924) - Improve load time of custom script list * [#8932](https://github.com/netbox-community/netbox/issues/8932) - Fix error when setting null value for interface `rf_role` via REST API * [#8935](https://github.com/netbox-community/netbox/issues/8935) - Correct ordering of next/previous racks to use naturalized names +* [#8951](https://github.com/netbox-community/netbox/issues/8951) - Allow changing device type & platform to different manufacturer simultaneously --- diff --git a/netbox/dcim/forms/models.py b/netbox/dcim/forms/models.py index ca9aa6d3a..a3d6fba3d 100644 --- a/netbox/dcim/forms/models.py +++ b/netbox/dcim/forms/models.py @@ -605,11 +605,6 @@ class DeviceForm(TenancyForm, CustomFieldModelForm): # can be flipped from one face to another. self.fields['position'].widget.add_query_param('exclude', self.instance.pk) - # Limit platform by manufacturer - self.fields['platform'].queryset = Platform.objects.filter( - Q(manufacturer__isnull=True) | Q(manufacturer=self.instance.device_type.manufacturer) - ) - # Disable rack assignment if this is a child device installed in a parent device if self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'): self.fields['site'].disabled = True diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 6b8ff043d..737685fd9 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -739,8 +739,8 @@ class Device(PrimaryModel, ConfigContextModel): if hasattr(self, 'device_type') and self.platform: if self.platform.manufacturer and self.platform.manufacturer != self.device_type.manufacturer: raise ValidationError({ - 'platform': "The assigned platform is limited to {} device types, but this device's type belongs " - "to {}.".format(self.platform.manufacturer, self.device_type.manufacturer) + 'platform': f"The assigned platform is limited to {self.platform.manufacturer} device types, but " + f"this device's type belongs to {self.device_type.manufacturer}." }) # A Device can only be assigned to a Cluster in the same Site (or no Site) From c78e7c14d3b787298e61540ee8202b83d9390f44 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 24 Mar 2022 10:47:39 -0400 Subject: [PATCH 36/45] Fixes #8947: Retain filter parameters when handling an export template exception --- docs/release-notes/version-3.1.md | 1 + netbox/netbox/views/generic.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index 95034cb12..43ac9eb1f 100644 --- a/docs/release-notes/version-3.1.md +++ b/docs/release-notes/version-3.1.md @@ -18,6 +18,7 @@ * [#8924](https://github.com/netbox-community/netbox/issues/8924) - Improve load time of custom script list * [#8932](https://github.com/netbox-community/netbox/issues/8932) - Fix error when setting null value for interface `rf_role` via REST API * [#8935](https://github.com/netbox-community/netbox/issues/8935) - Correct ordering of next/previous racks to use naturalized names +* [#8947](https://github.com/netbox-community/netbox/issues/8947) - Retain filter parameters when handling an export template exception * [#8951](https://github.com/netbox-community/netbox/issues/8951) - Allow changing device type & platform to different manufacturer simultaneously --- diff --git a/netbox/netbox/views/generic.py b/netbox/netbox/views/generic.py index 74f8f325b..46bc5b24e 100644 --- a/netbox/netbox/views/generic.py +++ b/netbox/netbox/views/generic.py @@ -212,7 +212,10 @@ class ObjectListView(ObjectPermissionRequiredMixin, View): return template.render_to_response(self.queryset) except Exception as e: messages.error(request, f"There was an error rendering the selected export template ({template.name}): {e}") - return redirect(request.path) + # Strip the `export` param and redirect user to the filtered objects list + query_params = request.GET.copy() + query_params.pop('export') + return redirect(f'{request.path}?{query_params.urlencode()}') def get(self, request): model = self.queryset.model From d312fe7c2bb6c9f1ed26fd02963eff7d21be5723 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 24 Mar 2022 11:14:24 -0400 Subject: [PATCH 37/45] Fixes #8696: Fix help link under FHRP group assigment creation view --- docs/core-functionality/ipam.md | 1 + docs/models/ipam/fhrpgroup.md | 6 ------ docs/models/ipam/fhrpgroupassignment.md | 5 +++++ docs/release-notes/version-3.1.md | 2 ++ 4 files changed, 8 insertions(+), 6 deletions(-) create mode 100644 docs/models/ipam/fhrpgroupassignment.md diff --git a/docs/core-functionality/ipam.md b/docs/core-functionality/ipam.md index 9fa5e0eb4..01bb3c76d 100644 --- a/docs/core-functionality/ipam.md +++ b/docs/core-functionality/ipam.md @@ -21,6 +21,7 @@ --- {!models/ipam/fhrpgroup.md!} +{!models/ipam/fhrpgroupassignment.md!} --- diff --git a/docs/models/ipam/fhrpgroup.md b/docs/models/ipam/fhrpgroup.md index 5efbc8428..c5baccd7b 100644 --- a/docs/models/ipam/fhrpgroup.md +++ b/docs/models/ipam/fhrpgroup.md @@ -8,9 +8,3 @@ A first-hop redundancy protocol (FHRP) enables multiple physical interfaces to p * Gateway Load Balancing Protocol (GLBP) NetBox models these redundancy groups by protocol and group ID. Each group may optionally be assigned an authentication type and key. (Note that the authentication key is stored as a plaintext value in NetBox.) Each group may be assigned or more virtual IPv4 and/or IPv6 addresses. - -## FHRP Group Assignments - -Member device and VM interfaces can be assigned to FHRP groups, along with a numeric priority value. For instance, three interfaces, each belonging to a different router, may each be assigned to the same FHRP group to serve a common virtual IP address. Each of these assignments would typically receive a different priority. - -Interfaces are assigned to FHRP groups under the interface detail view. diff --git a/docs/models/ipam/fhrpgroupassignment.md b/docs/models/ipam/fhrpgroupassignment.md new file mode 100644 index 000000000..c3e0bf200 --- /dev/null +++ b/docs/models/ipam/fhrpgroupassignment.md @@ -0,0 +1,5 @@ +# FHRP Group Assignments + +Member device and VM interfaces can be assigned to FHRP groups, along with a numeric priority value. For instance, three interfaces, each belonging to a different router, may each be assigned to the same FHRP group to serve a common virtual IP address. Each of these assignments would typically receive a different priority. + +Interfaces are assigned to FHRP groups under the interface detail view. diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index 43ac9eb1f..79635a228 100644 --- a/docs/release-notes/version-3.1.md +++ b/docs/release-notes/version-3.1.md @@ -11,6 +11,7 @@ ### Bug Fixes +* [#8696](https://github.com/netbox-community/netbox/issues/8696) - Fix help link under FHRP group assigment creation view * [#8813](https://github.com/netbox-community/netbox/issues/8813) - Retain global search bar query after submitting * [#8820](https://github.com/netbox-community/netbox/issues/8820) - Fix navbar background color in dark mode * [#8850](https://github.com/netbox-community/netbox/issues/8850) - Show airflow field on device REST API serializer when config context data is included @@ -20,6 +21,7 @@ * [#8935](https://github.com/netbox-community/netbox/issues/8935) - Correct ordering of next/previous racks to use naturalized names * [#8947](https://github.com/netbox-community/netbox/issues/8947) - Retain filter parameters when handling an export template exception * [#8951](https://github.com/netbox-community/netbox/issues/8951) - Allow changing device type & platform to different manufacturer simultaneously +* [#8952](https://github.com/netbox-community/netbox/issues/8952) - Device images in rear rack elevations should be hyperlinked --- From a5820e27a68fa842e9f931bc585a64e2766f771a Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 24 Mar 2022 11:56:18 -0400 Subject: [PATCH 38/45] Fixes #8905: Disable ordering by assigned tags to prevent erroneous results --- docs/release-notes/version-3.1.md | 1 + netbox/utilities/tables.py | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index 79635a228..ea18a0834 100644 --- a/docs/release-notes/version-3.1.md +++ b/docs/release-notes/version-3.1.md @@ -15,6 +15,7 @@ * [#8813](https://github.com/netbox-community/netbox/issues/8813) - Retain global search bar query after submitting * [#8820](https://github.com/netbox-community/netbox/issues/8820) - Fix navbar background color in dark mode * [#8850](https://github.com/netbox-community/netbox/issues/8850) - Show airflow field on device REST API serializer when config context data is included +* [#8905](https://github.com/netbox-community/netbox/issues/8905) - Disable ordering by assigned tags to prevent erroneous results * [#8919](https://github.com/netbox-community/netbox/issues/8919) - Fix filtering of VLAN groups by site under prefix edit form * [#8924](https://github.com/netbox-community/netbox/issues/8924) - Improve load time of custom script list * [#8932](https://github.com/netbox-community/netbox/issues/8932) - Fix error when setting null value for interface `rf_role` via REST API diff --git a/netbox/utilities/tables.py b/netbox/utilities/tables.py index 84be36da3..3aace8353 100644 --- a/netbox/utilities/tables.py +++ b/netbox/utilities/tables.py @@ -430,6 +430,7 @@ class TagColumn(tables.TemplateColumn): def __init__(self, url_name=None): super().__init__( + orderable=False, template_code=self.template_code, extra_context={'url_name': url_name} ) From a433d5d59d80755065a05870f6179fb320c15b63 Mon Sep 17 00:00:00 2001 From: Alex Gittings Date: Fri, 25 Mar 2022 09:25:55 +0000 Subject: [PATCH 39/45] Closes #8926: Implement type and roll to device bay table --- netbox/dcim/tables/devices.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index 3c2b3dace..3ff2a8b9f 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -677,6 +677,15 @@ class DeviceBayTable(DeviceComponentTable): 'args': [Accessor('device_id')], } ) + device_role = ColoredLabelColumn( + accessor=Accessor('installed_device__device_role'), + verbose_name='Role' + ) + device_type = tables.Column( + accessor=Accessor('installed_device__device_type'), + linkify=True, + verbose_name='Type' + ) status = tables.TemplateColumn( template_code=DEVICEBAY_STATUS, order_by=Accessor('installed_device__status') @@ -691,7 +700,7 @@ class DeviceBayTable(DeviceComponentTable): class Meta(DeviceComponentTable.Meta): model = DeviceBay fields = ( - 'pk', 'id', 'name', 'device', 'label', 'status', 'installed_device', 'description', 'tags', + 'pk', 'id', 'name', 'device', 'label', 'status', 'device_role', 'device_type', 'installed_device', 'description', 'tags', 'created', 'last_updated', ) From 12784295180e7e42ae44aefb02db641d57c13740 Mon Sep 17 00:00:00 2001 From: tranthang2404 <43313369+tranthang2404@users.noreply.github.com> Date: Fri, 25 Mar 2022 20:52:13 +0700 Subject: [PATCH 40/45] Closes #8232: Add color show full 100% utilization (#8816) * Closes #8232: Add color show full 100% utilization * change rounding * change rounding * fix hard code html * format --- netbox/dcim/models/racks.py | 2 +- netbox/ipam/models/ip.py | 6 +++--- netbox/utilities/templates/helpers/utilization_graph.html | 4 ++-- netbox/utilities/templatetags/helpers.py | 4 +++- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index 082ecfe57..9413d834e 100644 --- a/netbox/dcim/models/racks.py +++ b/netbox/dcim/models/racks.py @@ -412,7 +412,7 @@ class Rack(PrimaryModel): available_units.remove(u) occupied_unit_count = self.u_height - len(available_units) - percentage = int(float(occupied_unit_count) / self.u_height * 100) + percentage = float(occupied_unit_count) / self.u_height * 100 return percentage diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index 81c3ef34a..7d9937c1b 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -248,7 +248,7 @@ class Aggregate(GetAvailablePrefixesMixin, PrimaryModel): """ queryset = Prefix.objects.filter(prefix__net_contained_or_equal=str(self.prefix)) child_prefixes = netaddr.IPSet([p.prefix for p in queryset]) - utilization = int(float(child_prefixes.size) / self.prefix.size * 100) + utilization = float(child_prefixes.size) / self.prefix.size * 100 return min(utilization, 100) @@ -548,7 +548,7 @@ class Prefix(GetAvailablePrefixesMixin, PrimaryModel): vrf=self.vrf ) child_prefixes = netaddr.IPSet([p.prefix for p in queryset]) - utilization = int(float(child_prefixes.size) / self.prefix.size * 100) + utilization = float(child_prefixes.size) / self.prefix.size * 100 else: # Compile an IPSet to avoid counting duplicate IPs child_ips = netaddr.IPSet( @@ -558,7 +558,7 @@ class Prefix(GetAvailablePrefixesMixin, PrimaryModel): prefix_size = self.prefix.size if self.prefix.version == 4 and self.prefix.prefixlen < 31 and not self.is_pool: prefix_size -= 2 - utilization = int(float(child_ips.size) / prefix_size * 100) + utilization = float(child_ips.size) / prefix_size * 100 return min(utilization, 100) diff --git a/netbox/utilities/templates/helpers/utilization_graph.html b/netbox/utilities/templates/helpers/utilization_graph.html index fe1c0fc9a..afdd670fd 100644 --- a/netbox/utilities/templates/helpers/utilization_graph.html +++ b/netbox/utilities/templates/helpers/utilization_graph.html @@ -12,10 +12,10 @@ class="progress-bar {{ bar_class }}" style="width: {{ utilization }}%;" > - {% if utilization >= 25 %}{{ utilization }}%{% endif %} + {% if utilization >= 25 %}{{ utilization|floatformat:-2 }}%{% endif %} {% if utilization < 25 %} - {{ utilization }}% + {{ utilization|floatformat:-2 }}% {% endif %} {% endif %} diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index 0e45cb581..cff3428c0 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -389,7 +389,9 @@ def utilization_graph(utilization, warning_threshold=75, danger_threshold=90): """ Display a horizontal bar graph indicating a percentage of utilization. """ - if danger_threshold and utilization >= danger_threshold: + if utilization == 100: + bar_class = 'bg-secondary' + elif danger_threshold and utilization >= danger_threshold: bar_class = 'bg-danger' elif warning_threshold and utilization >= warning_threshold: bar_class = 'bg-warning' From dde4495e20e98c4c1d7f85889ca171c646fe5ae2 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 25 Mar 2022 09:59:58 -0400 Subject: [PATCH 41/45] #8232: Cleanup & test fix --- netbox/ipam/tests/test_models.py | 4 ++-- netbox/utilities/templates/helpers/utilization_graph.html | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/netbox/ipam/tests/test_models.py b/netbox/ipam/tests/test_models.py index f6130f1c1..f63e873b4 100644 --- a/netbox/ipam/tests/test_models.py +++ b/netbox/ipam/tests/test_models.py @@ -204,11 +204,11 @@ class TestPrefix(TestCase): IPAddress.objects.bulk_create([ IPAddress(address=IPNetwork(f'10.0.0.{i}/24')) for i in range(1, 33) ]) - self.assertEqual(prefix.get_utilization(), 12) # 12.5% utilization + self.assertEqual(prefix.get_utilization(), 32 / 254 * 100) # ~12.5% utilization # Create a child range with 32 additional IPs IPRange.objects.create(start_address=IPNetwork('10.0.0.33/24'), end_address=IPNetwork('10.0.0.64/24')) - self.assertEqual(prefix.get_utilization(), 25) # 25% utilization + self.assertEqual(prefix.get_utilization(), 64 / 254 * 100) # ~25% utilization # # Uniqueness enforcement tests diff --git a/netbox/utilities/templates/helpers/utilization_graph.html b/netbox/utilities/templates/helpers/utilization_graph.html index afdd670fd..e6829befc 100644 --- a/netbox/utilities/templates/helpers/utilization_graph.html +++ b/netbox/utilities/templates/helpers/utilization_graph.html @@ -12,10 +12,10 @@ class="progress-bar {{ bar_class }}" style="width: {{ utilization }}%;" > - {% if utilization >= 25 %}{{ utilization|floatformat:-2 }}%{% endif %} + {% if utilization >= 25 %}{{ utilization|floatformat:0 }}%{% endif %} {% if utilization < 25 %} - {{ utilization|floatformat:-2 }}% + {{ utilization|floatformat:0 }}% {% endif %} {% endif %} From 26637d934bcf60eccbf8f5306b76b2b91b323cc0 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 25 Mar 2022 10:02:21 -0400 Subject: [PATCH 42/45] Change log for #8232, #8926 --- docs/release-notes/version-3.1.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index ea18a0834..67bc89b75 100644 --- a/docs/release-notes/version-3.1.md +++ b/docs/release-notes/version-3.1.md @@ -30,6 +30,7 @@ ### Enhancements +* [#8232](https://github.com/netbox-community/netbox/issues/8232) - Use a different color for 100% utilization bars * [#8594](https://github.com/netbox-community/netbox/issues/8594) - Enable filtering by exact description match for all applicable models * [#8629](https://github.com/netbox-community/netbox/issues/8629) - Add description to tag table search function * [#8664](https://github.com/netbox-community/netbox/issues/8664) - Show assigned ASNs/sites under list views @@ -37,6 +38,7 @@ * [#8758](https://github.com/netbox-community/netbox/issues/8758) - Allow empty string substitution when renaming objects in bulk * [#8762](https://github.com/netbox-community/netbox/issues/8762) - Link to rack elevations list from site view * [#8766](https://github.com/netbox-community/netbox/issues/8766) - Add SCTP to service protocols list +* [#8926](https://github.com/netbox-community/netbox/issues/8926) - Add device type, role columns to device bay table ### Bug Fixes From 8924d5fa0554315f7886083bdda4c3f50c11d3d4 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 25 Mar 2022 10:04:48 -0400 Subject: [PATCH 43/45] Correct change log --- docs/release-notes/version-3.1.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index 67bc89b75..c120b93b9 100644 --- a/docs/release-notes/version-3.1.md +++ b/docs/release-notes/version-3.1.md @@ -4,6 +4,7 @@ ### Enhancements +* [#8232](https://github.com/netbox-community/netbox/issues/8232) - Use a different color for 100% utilization bars * [#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 @@ -18,6 +19,7 @@ * [#8905](https://github.com/netbox-community/netbox/issues/8905) - Disable ordering by assigned tags to prevent erroneous results * [#8919](https://github.com/netbox-community/netbox/issues/8919) - Fix filtering of VLAN groups by site under prefix edit form * [#8924](https://github.com/netbox-community/netbox/issues/8924) - Improve load time of custom script list +* [#8926](https://github.com/netbox-community/netbox/issues/8926) - Add device type, role columns to device bay table * [#8932](https://github.com/netbox-community/netbox/issues/8932) - Fix error when setting null value for interface `rf_role` via REST API * [#8935](https://github.com/netbox-community/netbox/issues/8935) - Correct ordering of next/previous racks to use naturalized names * [#8947](https://github.com/netbox-community/netbox/issues/8947) - Retain filter parameters when handling an export template exception @@ -30,7 +32,6 @@ ### Enhancements -* [#8232](https://github.com/netbox-community/netbox/issues/8232) - Use a different color for 100% utilization bars * [#8594](https://github.com/netbox-community/netbox/issues/8594) - Enable filtering by exact description match for all applicable models * [#8629](https://github.com/netbox-community/netbox/issues/8629) - Add description to tag table search function * [#8664](https://github.com/netbox-community/netbox/issues/8664) - Show assigned ASNs/sites under list views @@ -38,7 +39,6 @@ * [#8758](https://github.com/netbox-community/netbox/issues/8758) - Allow empty string substitution when renaming objects in bulk * [#8762](https://github.com/netbox-community/netbox/issues/8762) - Link to rack elevations list from site view * [#8766](https://github.com/netbox-community/netbox/issues/8766) - Add SCTP to service protocols list -* [#8926](https://github.com/netbox-community/netbox/issues/8926) - Add device type, role columns to device bay table ### Bug Fixes From 20a6f6ac79402446a2075e7ff93a7f0f076e3dcf Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 25 Mar 2022 10:14:37 -0400 Subject: [PATCH 44/45] Release v3.1.10 --- .github/ISSUE_TEMPLATE/bug_report.yaml | 2 +- .github/ISSUE_TEMPLATE/feature_request.yaml | 2 +- docs/release-notes/version-3.1.md | 2 +- netbox/netbox/settings.py | 2 +- requirements.txt | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 3af825d30..21dc72545 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.1.9 + placeholder: v3.1.10 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index f5bf198b8..f64f5ccba 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.1.9 + placeholder: v3.1.10 validations: required: true - type: dropdown diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index c120b93b9..ce7001204 100644 --- a/docs/release-notes/version-3.1.md +++ b/docs/release-notes/version-3.1.md @@ -1,6 +1,6 @@ # NetBox v3.1 -## v3.1.10 (FUTURE) +## v3.1.10 (2022-03-25) ### Enhancements diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index d16e00337..0ae386343 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -19,7 +19,7 @@ from netbox.config import PARAMS # Environment setup # -VERSION = '3.1.10-dev' +VERSION = '3.1.10' # Hostname HOSTNAME = platform.node() diff --git a/requirements.txt b/requirements.txt index 04d053180..b331fb17e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,14 +18,14 @@ gunicorn==20.1.0 Jinja2==3.0.3 Markdown==3.3.6 markdown-include==0.6.0 -mkdocs-material==8.2.5 +mkdocs-material==8.2.7 netaddr==0.8.0 Pillow==9.0.1 psycopg2-binary==2.9.3 PyYAML==6.0 social-auth-app-django==5.0.0 social-auth-core==4.2.0 -svgwrite==1.4.1 +svgwrite==1.4.2 tablib==3.2.0 tzdata==2021.5 From 271c2ea3e3eda5b80f0d14b9c320f7f79011e1ff Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 25 Mar 2022 10:16:40 -0400 Subject: [PATCH 45/45] Correct changelog --- docs/release-notes/version-3.1.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index ce7001204..1051d6bb2 100644 --- a/docs/release-notes/version-3.1.md +++ b/docs/release-notes/version-3.1.md @@ -9,6 +9,7 @@ * [#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 +* [#8926](https://github.com/netbox-community/netbox/issues/8926) - Add device type, role columns to device bay table ### Bug Fixes @@ -19,7 +20,6 @@ * [#8905](https://github.com/netbox-community/netbox/issues/8905) - Disable ordering by assigned tags to prevent erroneous results * [#8919](https://github.com/netbox-community/netbox/issues/8919) - Fix filtering of VLAN groups by site under prefix edit form * [#8924](https://github.com/netbox-community/netbox/issues/8924) - Improve load time of custom script list -* [#8926](https://github.com/netbox-community/netbox/issues/8926) - Add device type, role columns to device bay table * [#8932](https://github.com/netbox-community/netbox/issues/8932) - Fix error when setting null value for interface `rf_role` via REST API * [#8935](https://github.com/netbox-community/netbox/issues/8935) - Correct ordering of next/previous racks to use naturalized names * [#8947](https://github.com/netbox-community/netbox/issues/8947) - Retain filter parameters when handling an export template exception