From cabc077616cf997b03a3b6156e33fb5255f6f449 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 f2bbfcf74ab034836fd3c555bb98d7db6de934b6 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 4fbf72263fa107e2966e314cd63fd40303f24996 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 4dcd94157ea72e937d7d1e0fbe91ee4fc81e1162 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 2f787cb3eb12ffad7853210397007d09fc6b993a 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 fd1430b2d80257da8f0591d97f2e3912594430ba 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 d080f80e2921149f4807ac1966cb8fa593c1fa45 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 7b102f199b458e0730b074d81b55270f72b574ce 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 da990d5db76bf33a313afb7928f0a649dc925e1e 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 3abcf47b623e1d745e51a7f163c1df805338a098 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 e487c32d85dab238da51d2c078fc126455a25521 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 54a98c5062b21b7daab22b784dde60b3e7c5bac6 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 7a74319d2e811ea4c1bf2d5b7171764396e8de15 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 97097d5424c2e94c9a991ff9f595351cfe4b98b5 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 2f02cc147413061bab3de1f7c9d23c2dd3ea9eaa 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 39e8b38a1a2cb5b46189d9be741ef5f8d118ffa7 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 07ef06be0aed0f06dfbdee578924317a4656b542 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 8f8f401431c4f55bf132c4dc434f05208089bafe 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 7945d82bb1a708fe56711ccebc8f92549017fa36 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 af6182ce140b2189c4764c9f34861757c61e46d0 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 156badbb5407b78b484fe8cc28c5afc8c5ce750a 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 7b2ee7744eab8d888dae220faa48947250169e3f 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 93350b9326e2745f383735d882b157b1a83f84ed 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 1ebe3b6ffee135ede808fea9abb8d5f37e583d0e 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 9bcc2399c426e34b290747090c551b657e6bcd5e 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 a18ecfbb082d603f34d2e334cbde901109b01527 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 ba1d177541bdb537976d20e71df6314fa2edd379 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 f6d4d54d06cc0f07c50116040e0fd1f983c64483 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 3c0a04ebb5beb4f1ad38690d3368d72b3b28a411 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 b8f62f36f96b1fe5e3b5c9e49eede101f00e8133 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 5f946edb265d02a1ac5a9f232ae0ad7d397078e6 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 b69bf7c35faeb7c1fce42acbe30cbd364f6c3fd9 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 1ab57ee22416ea7572c1f8cf601fd066b6f369d4 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 83e78dd4a60819a11f54472c78cde6718963d857 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 29910165956cfa30abd7019c5dd875b2b120f353 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 774a58509a3eb432759e877f3e35714f5e7a8e44 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 b1894be1b359b8e14c1759c4d3ce74c53db11d09 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 4639435b24f344b6631f2bd0bca44cdd4ceea8da 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 5166b48010736b52b7dba9f238a2529bfffaae2d 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 be9fe2c36b377fa7d5f092f7bcce7f0808cd1311 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 cd1c32d979656fa04b9f3b92cb4ee6a97c33686f 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 05b80bed3bce10e58f39c6875739997a84e95c57 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 737cc5e5b2d71c6ec438f14974ddd41117796745 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 d1c7a8748acfbd2f936681e7f9e02b3c25dee6bd 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 71f49ffb4368e799cdbd1ce8dce5e0f2de0f3e08 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