From b0df24e6d1fac3bf568bc29a16c5d7584032a6d3 Mon Sep 17 00:00:00 2001 From: kkthxbye-code Date: Fri, 15 Jul 2022 08:51:05 +0200 Subject: [PATCH 01/17] UI: Only set focus on select field search boxes if the select is open --- netbox/project-static/dist/netbox.js | Bin 376180 -> 376234 bytes netbox/project-static/dist/netbox.js.map | Bin 345595 -> 345631 bytes .../src/select/api/apiSelect.ts | 7 ++++++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index b611079e17d534b7e0aea1ed07b5e33c124129f5..c766b8a72357ae532a9e3a1e3c988ac73050d0fc 100644 GIT binary patch delta 48 zcmezJOKjC|v4$4L7N!>F7M3lnkAtR9zs4do-M^iglSiqzSU0~QHBU)XO>O&?Al3z0 E0OZpWQ2+n{ delta 25 hcmZ4WTkOj(v4$4L7N!>F7M3lnkAt>L2D47g0sxkh3TprW diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index 494ba0b56e2fc3ad948e546f77283e156a6b89a8..55474df17cba29ff6f6346049b65baf3886a7ca2 100644 GIT binary patch delta 98 zcmey}EjqtPw4sHug{g&k3(MheYVIyN-j0smNJOBcW1vo`qhqK}u%lzJPNbu=sq^#) tw^*cD0v+8gw?F>I63oYyQR!6WtmE&PS-IVTmDQJ>DNlQQKM!k;B>+lVA4dQH delta 62 zcmbQ=Bl^2rw4sHug{g&k3(MheOwK;jAKYS*VhMC~Hr>wtoh6u$%eca+%2~(X(WGK~ S3M;EGJCmj5_RT!3IhFv Date: Fri, 15 Jul 2022 08:34:30 -0400 Subject: [PATCH 02/17] Fixes #9715: Fix SOCIAL_AUTH_PIPELINE config parameter not taking effect --- docs/administration/authentication/overview.md | 2 +- docs/release-notes/version-3.2.md | 5 +++++ netbox/netbox/settings.py | 15 ++++++++------- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/docs/administration/authentication/overview.md b/docs/administration/authentication/overview.md index b405ed09a..fca9eab5e 100644 --- a/docs/administration/authentication/overview.md +++ b/docs/administration/authentication/overview.md @@ -34,4 +34,4 @@ REMOTE_AUTH_BACKEND = 'social_core.backends.google.GoogleOAuth2' NetBox supports single sign-on authentication via the [python-social-auth](https://github.com/python-social-auth) library. To enable SSO, specify the path to the desired authentication backend within the `social_core` Python package. Please see the complete list of [supported authentication backends](https://github.com/python-social-auth/social-core/tree/master/social_core/backends) for the available options. -Most remote authentication backends require some additional configuration through settings prefixed with `SOCIAL_AUTH_`. These will be automatically imported from NetBox's `configuration.py` file. Additionally, the [authentication pipeline](https://python-social-auth.readthedocs.io/en/latest/pipeline.html) can be customized via the `SOCIAL_AUTH_PIPELINE` parameter. +Most remote authentication backends require some additional configuration through settings prefixed with `SOCIAL_AUTH_`. These will be automatically imported from NetBox's `configuration.py` file. Additionally, the [authentication pipeline](https://python-social-auth.readthedocs.io/en/latest/pipeline.html) can be customized via the `SOCIAL_AUTH_PIPELINE` parameter. (NetBox's default pipeline is defined in `netbox/settings.py` for your reference.) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index 1ba6235b8..250082061 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -2,6 +2,11 @@ ## v3.2.7 (FUTURE) +### Bug Fixes + +* [#9715](https://github.com/netbox-community/netbox/issues/9715) - Fix `SOCIAL_AUTH_PIPELINE` config parameter not taking effect +* [#9734](https://github.com/netbox-community/netbox/issues/9734) - Fix stealing focus by select fields in forms + --- ## v3.2.6 (2022-07-11) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 736290380..b17d70e31 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -476,13 +476,6 @@ if SENTRY_ENABLED: # Django social auth # -# Load all SOCIAL_AUTH_* settings from the user configuration -for param in dir(configuration): - if param.startswith('SOCIAL_AUTH_'): - globals()[param] = getattr(configuration, param) - -SOCIAL_AUTH_JSONFIELD_ENABLED = True - SOCIAL_AUTH_PIPELINE = ( 'social_core.pipeline.social_auth.social_details', 'social_core.pipeline.social_auth.social_uid', @@ -496,6 +489,14 @@ SOCIAL_AUTH_PIPELINE = ( 'social_core.pipeline.user.user_details', ) +# Load all SOCIAL_AUTH_* settings from the user configuration +for param in dir(configuration): + if param.startswith('SOCIAL_AUTH_'): + globals()[param] = getattr(configuration, param) + +# Force usage of PostgreSQL's JSONB field for extra data +SOCIAL_AUTH_JSONFIELD_ENABLED = True + # # Django Prometheus From fe2fae5b86d554a9a242a376084b246f3e78c6bf Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 15 Jul 2022 09:42:12 -0400 Subject: [PATCH 03/17] Closes #9741: Check for UserConfig instance during user login --- docs/release-notes/version-3.2.md | 4 ++++ netbox/users/views.py | 10 ++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index 250082061..6ec077b8d 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -2,6 +2,10 @@ ## v3.2.7 (FUTURE) +### Enhancements + +* [#9741](https://github.com/netbox-community/netbox/issues/9741) - Check for UserConfig instance during user login + ### Bug Fixes * [#9715](https://github.com/netbox-community/netbox/issues/9715) - Fix `SOCIAL_AUTH_PIPELINE` config parameter not taking effect diff --git a/netbox/users/views.py b/netbox/users/views.py index 6a923e77e..344f375fc 100644 --- a/netbox/users/views.py +++ b/netbox/users/views.py @@ -20,7 +20,7 @@ from netbox.authentication import get_auth_backend_display from netbox.config import get_config from utilities.forms import ConfirmationForm from .forms import LoginForm, PasswordChangeForm, TokenForm, UserConfigForm -from .models import Token +from .models import Token, UserConfig # @@ -69,7 +69,13 @@ class LoginView(View): # Authenticate user auth_login(request, form.get_user()) logger.info(f"User {request.user} successfully authenticated") - messages.info(request, "Logged in as {}.".format(request.user)) + messages.info(request, f"Logged in as {request.user}.") + + # Ensure the user has a UserConfig defined. (This should normally be handled by + # create_userconfig() on user creation.) + if not hasattr(request.user, 'config'): + config = get_config() + UserConfig(user=request.user, data=config.DEFAULT_USER_PREFERENCES).save() return self.redirect_to_next(request, logger) From 6da171a6997c931fd26faa444d36c97d6ac97243 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 15 Jul 2022 10:52:37 -0400 Subject: [PATCH 04/17] Corrected typo for description of 'snapshots' --- docs/models/extras/webhook.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/models/extras/webhook.md b/docs/models/extras/webhook.md index d0137938d..9f64401ae 100644 --- a/docs/models/extras/webhook.md +++ b/docs/models/extras/webhook.md @@ -43,7 +43,7 @@ The following data is available as context for Jinja2 templates: * `username` - The name of the user account associated with the change. * `request_id` - The unique request ID. This may be used to correlate multiple changes associated with a single request. * `data` - A detailed representation of the object in its current state. This is typically equivalent to the model's representation in NetBox's REST API. -* `snapshots` - Minimal "snapshots" of the object state both before and after the change was made; provided ass a dictionary with keys named `prechange` and `postchange`. These are not as extensive as the fully serialized representation, but contain enough information to convey what has changed. +* `snapshots` - Minimal "snapshots" of the object state both before and after the change was made; provided as a dictionary with keys named `prechange` and `postchange`. These are not as extensive as the fully serialized representation, but contain enough information to convey what has changed. ### Default Request Body From 68f53aaa87137d3b22e4b6275b565a363e36b253 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 15 Jul 2022 15:24:47 -0400 Subject: [PATCH 05/17] Closes #9745: Add wireless LANs and links to global search --- docs/release-notes/version-3.2.md | 1 + netbox/netbox/constants.py | 256 +---------------------------- netbox/netbox/forms/__init__.py | 2 +- netbox/netbox/search.py | 261 ++++++++++++++++++++++++++++++ netbox/netbox/views/__init__.py | 3 +- 5 files changed, 266 insertions(+), 257 deletions(-) create mode 100644 netbox/netbox/search.py diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index 6ec077b8d..3eb6a7a15 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -5,6 +5,7 @@ ### Enhancements * [#9741](https://github.com/netbox-community/netbox/issues/9741) - Check for UserConfig instance during user login +* [#9745](https://github.com/netbox-community/netbox/issues/9745) - Add wireless LANs and links to global search ### Bug Fixes diff --git a/netbox/netbox/constants.py b/netbox/netbox/constants.py index cc04e9aa8..61588d331 100644 --- a/netbox/netbox/constants.py +++ b/netbox/netbox/constants.py @@ -1,256 +1,2 @@ -from collections import OrderedDict -from typing import Dict - -import circuits.filtersets -import circuits.tables -import dcim.filtersets -import dcim.tables -import ipam.filtersets -import ipam.tables -import tenancy.filtersets -import tenancy.tables -import virtualization.filtersets -import virtualization.tables -from circuits.models import Circuit, ProviderNetwork, Provider -from dcim.models import ( - Cable, Device, DeviceType, Location, Module, ModuleType, PowerFeed, Rack, RackReservation, Site, VirtualChassis, -) -from ipam.models import Aggregate, ASN, IPAddress, Prefix, Service, VLAN, VRF -from tenancy.models import Contact, Tenant, ContactAssignment -from utilities.utils import count_related -from virtualization.models import Cluster, VirtualMachine - +# Max results per object type SEARCH_MAX_RESULTS = 15 - -CIRCUIT_TYPES = OrderedDict( - ( - ('provider', { - 'queryset': Provider.objects.annotate( - count_circuits=count_related(Circuit, 'provider') - ), - 'filterset': circuits.filtersets.ProviderFilterSet, - 'table': circuits.tables.ProviderTable, - 'url': 'circuits:provider_list', - }), - ('circuit', { - 'queryset': Circuit.objects.prefetch_related( - 'type', 'provider', 'tenant', 'tenant__group', 'terminations__site' - ), - 'filterset': circuits.filtersets.CircuitFilterSet, - 'table': circuits.tables.CircuitTable, - 'url': 'circuits:circuit_list', - }), - ('providernetwork', { - 'queryset': ProviderNetwork.objects.prefetch_related('provider'), - 'filterset': circuits.filtersets.ProviderNetworkFilterSet, - 'table': circuits.tables.ProviderNetworkTable, - 'url': 'circuits:providernetwork_list', - }), - ) -) - - -DCIM_TYPES = OrderedDict( - ( - ('site', { - 'queryset': Site.objects.prefetch_related('region', 'tenant', 'tenant__group'), - 'filterset': dcim.filtersets.SiteFilterSet, - 'table': dcim.tables.SiteTable, - 'url': 'dcim:site_list', - }), - ('rack', { - 'queryset': Rack.objects.prefetch_related('site', 'location', 'tenant', 'tenant__group', 'role').annotate( - device_count=count_related(Device, 'rack') - ), - 'filterset': dcim.filtersets.RackFilterSet, - 'table': dcim.tables.RackTable, - 'url': 'dcim:rack_list', - }), - ('rackreservation', { - 'queryset': RackReservation.objects.prefetch_related('site', 'rack', 'user'), - 'filterset': dcim.filtersets.RackReservationFilterSet, - 'table': dcim.tables.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': dcim.filtersets.LocationFilterSet, - 'table': dcim.tables.LocationTable, - 'url': 'dcim:location_list', - }), - ('devicetype', { - 'queryset': DeviceType.objects.prefetch_related('manufacturer').annotate( - instance_count=count_related(Device, 'device_type') - ), - 'filterset': dcim.filtersets.DeviceTypeFilterSet, - 'table': dcim.tables.DeviceTypeTable, - 'url': 'dcim:devicetype_list', - }), - ('device', { - 'queryset': Device.objects.prefetch_related( - 'device_type__manufacturer', 'device_role', 'tenant', 'tenant__group', 'site', 'rack', 'primary_ip4', 'primary_ip6', - ), - 'filterset': dcim.filtersets.DeviceFilterSet, - 'table': dcim.tables.DeviceTable, - 'url': 'dcim:device_list', - }), - ('moduletype', { - 'queryset': ModuleType.objects.prefetch_related('manufacturer').annotate( - instance_count=count_related(Module, 'module_type') - ), - 'filterset': dcim.filtersets.ModuleTypeFilterSet, - 'table': dcim.tables.ModuleTypeTable, - 'url': 'dcim:moduletype_list', - }), - ('module', { - 'queryset': Module.objects.prefetch_related( - 'module_type__manufacturer', 'device', 'module_bay', - ), - 'filterset': dcim.filtersets.ModuleFilterSet, - 'table': dcim.tables.ModuleTable, - 'url': 'dcim:module_list', - }), - ('virtualchassis', { - 'queryset': VirtualChassis.objects.prefetch_related('master').annotate( - member_count=count_related(Device, 'virtual_chassis') - ), - 'filterset': dcim.filtersets.VirtualChassisFilterSet, - 'table': dcim.tables.VirtualChassisTable, - 'url': 'dcim:virtualchassis_list', - }), - ('cable', { - 'queryset': Cable.objects.all(), - 'filterset': dcim.filtersets.CableFilterSet, - 'table': dcim.tables.CableTable, - 'url': 'dcim:cable_list', - }), - ('powerfeed', { - 'queryset': PowerFeed.objects.all(), - 'filterset': dcim.filtersets.PowerFeedFilterSet, - 'table': dcim.tables.PowerFeedTable, - 'url': 'dcim:powerfeed_list', - }), - ) -) - -IPAM_TYPES = OrderedDict( - ( - ('vrf', { - 'queryset': VRF.objects.prefetch_related('tenant', 'tenant__group'), - 'filterset': ipam.filtersets.VRFFilterSet, - 'table': ipam.tables.VRFTable, - 'url': 'ipam:vrf_list', - }), - ('aggregate', { - 'queryset': Aggregate.objects.prefetch_related('rir'), - 'filterset': ipam.filtersets.AggregateFilterSet, - 'table': ipam.tables.AggregateTable, - 'url': 'ipam:aggregate_list', - }), - ('prefix', { - 'queryset': Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'tenant__group', 'vlan', 'role'), - 'filterset': ipam.filtersets.PrefixFilterSet, - 'table': ipam.tables.PrefixTable, - 'url': 'ipam:prefix_list', - }), - ('ipaddress', { - 'queryset': IPAddress.objects.prefetch_related('vrf__tenant', 'tenant', 'tenant__group'), - 'filterset': ipam.filtersets.IPAddressFilterSet, - 'table': ipam.tables.IPAddressTable, - 'url': 'ipam:ipaddress_list', - }), - ('vlan', { - 'queryset': VLAN.objects.prefetch_related('site', 'group', 'tenant', 'tenant__group', 'role'), - 'filterset': ipam.filtersets.VLANFilterSet, - 'table': ipam.tables.VLANTable, - 'url': 'ipam:vlan_list', - }), - ('asn', { - 'queryset': ASN.objects.prefetch_related('rir', 'tenant', 'tenant__group'), - 'filterset': ipam.filtersets.ASNFilterSet, - 'table': ipam.tables.ASNTable, - 'url': 'ipam:asn_list', - }), - ('service', { - 'queryset': Service.objects.prefetch_related('device', 'virtual_machine'), - 'filterset': ipam.filtersets.ServiceFilterSet, - 'table': ipam.tables.ServiceTable, - 'url': 'ipam:service_list', - }), - ) -) - -TENANCY_TYPES = OrderedDict( - ( - ('tenant', { - 'queryset': Tenant.objects.prefetch_related('group'), - 'filterset': tenancy.filtersets.TenantFilterSet, - 'table': tenancy.tables.TenantTable, - 'url': 'tenancy:tenant_list', - }), - ('contact', { - 'queryset': Contact.objects.prefetch_related('group', 'assignments').annotate( - assignment_count=count_related(ContactAssignment, 'contact')), - 'filterset': tenancy.filtersets.ContactFilterSet, - 'table': tenancy.tables.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': virtualization.filtersets.ClusterFilterSet, - 'table': virtualization.tables.ClusterTable, - 'url': 'virtualization:cluster_list', - }), - ('virtualmachine', { - 'queryset': VirtualMachine.objects.prefetch_related( - 'cluster', 'tenant', 'tenant__group', 'platform', 'primary_ip4', 'primary_ip6', - ), - 'filterset': virtualization.filtersets.VirtualMachineFilterSet, - 'table': virtualization.tables.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/__init__.py b/netbox/netbox/forms/__init__.py index 23848724d..d1451e003 100644 --- a/netbox/netbox/forms/__init__.py +++ b/netbox/netbox/forms/__init__.py @@ -1,6 +1,6 @@ from django import forms -from netbox.constants import SEARCH_TYPE_HIERARCHY +from netbox.search import SEARCH_TYPE_HIERARCHY from utilities.forms import BootstrapMixin from .base import * diff --git a/netbox/netbox/search.py b/netbox/netbox/search.py new file mode 100644 index 000000000..ef0c4fd87 --- /dev/null +++ b/netbox/netbox/search.py @@ -0,0 +1,261 @@ +import circuits.filtersets +import circuits.tables +import dcim.filtersets +import dcim.tables +import ipam.filtersets +import ipam.tables +import tenancy.filtersets +import tenancy.tables +import virtualization.filtersets +import wireless.tables +import wireless.filtersets +import virtualization.tables +from circuits.models import Circuit, ProviderNetwork, Provider +from dcim.models import ( + Cable, Device, DeviceType, Interface, Location, Module, ModuleType, PowerFeed, Rack, RackReservation, Site, + VirtualChassis, +) +from ipam.models import Aggregate, ASN, IPAddress, Prefix, Service, VLAN, VRF +from tenancy.models import Contact, Tenant, ContactAssignment +from utilities.utils import count_related +from wireless.models import WirelessLAN, WirelessLink +from virtualization.models import Cluster, VirtualMachine + +CIRCUIT_TYPES = { + 'provider': { + 'queryset': Provider.objects.annotate( + count_circuits=count_related(Circuit, 'provider') + ), + 'filterset': circuits.filtersets.ProviderFilterSet, + 'table': circuits.tables.ProviderTable, + 'url': 'circuits:provider_list', + }, + 'circuit': { + 'queryset': Circuit.objects.prefetch_related( + 'type', 'provider', 'tenant', 'tenant__group', 'terminations__site' + ), + 'filterset': circuits.filtersets.CircuitFilterSet, + 'table': circuits.tables.CircuitTable, + 'url': 'circuits:circuit_list', + }, + 'providernetwork': { + 'queryset': ProviderNetwork.objects.prefetch_related('provider'), + 'filterset': circuits.filtersets.ProviderNetworkFilterSet, + 'table': circuits.tables.ProviderNetworkTable, + 'url': 'circuits:providernetwork_list', + }, +} + +DCIM_TYPES = { + 'site': { + 'queryset': Site.objects.prefetch_related('region', 'tenant', 'tenant__group'), + 'filterset': dcim.filtersets.SiteFilterSet, + 'table': dcim.tables.SiteTable, + 'url': 'dcim:site_list', + }, + 'rack': { + 'queryset': Rack.objects.prefetch_related('site', 'location', 'tenant', 'tenant__group', 'role').annotate( + device_count=count_related(Device, 'rack') + ), + 'filterset': dcim.filtersets.RackFilterSet, + 'table': dcim.tables.RackTable, + 'url': 'dcim:rack_list', + }, + 'rackreservation': { + 'queryset': RackReservation.objects.prefetch_related('site', 'rack', 'user'), + 'filterset': dcim.filtersets.RackReservationFilterSet, + 'table': dcim.tables.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': dcim.filtersets.LocationFilterSet, + 'table': dcim.tables.LocationTable, + 'url': 'dcim:location_list', + }, + 'devicetype': { + 'queryset': DeviceType.objects.prefetch_related('manufacturer').annotate( + instance_count=count_related(Device, 'device_type') + ), + 'filterset': dcim.filtersets.DeviceTypeFilterSet, + 'table': dcim.tables.DeviceTypeTable, + 'url': 'dcim:devicetype_list', + }, + 'device': { + 'queryset': Device.objects.prefetch_related( + 'device_type__manufacturer', 'device_role', 'tenant', 'tenant__group', 'site', 'rack', 'primary_ip4', + 'primary_ip6', + ), + 'filterset': dcim.filtersets.DeviceFilterSet, + 'table': dcim.tables.DeviceTable, + 'url': 'dcim:device_list', + }, + 'moduletype': { + 'queryset': ModuleType.objects.prefetch_related('manufacturer').annotate( + instance_count=count_related(Module, 'module_type') + ), + 'filterset': dcim.filtersets.ModuleTypeFilterSet, + 'table': dcim.tables.ModuleTypeTable, + 'url': 'dcim:moduletype_list', + }, + 'module': { + 'queryset': Module.objects.prefetch_related( + 'module_type__manufacturer', 'device', 'module_bay', + ), + 'filterset': dcim.filtersets.ModuleFilterSet, + 'table': dcim.tables.ModuleTable, + 'url': 'dcim:module_list', + }, + 'virtualchassis': { + 'queryset': VirtualChassis.objects.prefetch_related('master').annotate( + member_count=count_related(Device, 'virtual_chassis') + ), + 'filterset': dcim.filtersets.VirtualChassisFilterSet, + 'table': dcim.tables.VirtualChassisTable, + 'url': 'dcim:virtualchassis_list', + }, + 'cable': { + 'queryset': Cable.objects.all(), + 'filterset': dcim.filtersets.CableFilterSet, + 'table': dcim.tables.CableTable, + 'url': 'dcim:cable_list', + }, + 'powerfeed': { + 'queryset': PowerFeed.objects.all(), + 'filterset': dcim.filtersets.PowerFeedFilterSet, + 'table': dcim.tables.PowerFeedTable, + 'url': 'dcim:powerfeed_list', + }, +} + +IPAM_TYPES = { + 'vrf': { + 'queryset': VRF.objects.prefetch_related('tenant', 'tenant__group'), + 'filterset': ipam.filtersets.VRFFilterSet, + 'table': ipam.tables.VRFTable, + 'url': 'ipam:vrf_list', + }, + 'aggregate': { + 'queryset': Aggregate.objects.prefetch_related('rir'), + 'filterset': ipam.filtersets.AggregateFilterSet, + 'table': ipam.tables.AggregateTable, + 'url': 'ipam:aggregate_list', + }, + 'prefix': { + 'queryset': Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'tenant__group', 'vlan', 'role'), + 'filterset': ipam.filtersets.PrefixFilterSet, + 'table': ipam.tables.PrefixTable, + 'url': 'ipam:prefix_list', + }, + 'ipaddress': { + 'queryset': IPAddress.objects.prefetch_related('vrf__tenant', 'tenant', 'tenant__group'), + 'filterset': ipam.filtersets.IPAddressFilterSet, + 'table': ipam.tables.IPAddressTable, + 'url': 'ipam:ipaddress_list', + }, + 'vlan': { + 'queryset': VLAN.objects.prefetch_related('site', 'group', 'tenant', 'tenant__group', 'role'), + 'filterset': ipam.filtersets.VLANFilterSet, + 'table': ipam.tables.VLANTable, + 'url': 'ipam:vlan_list', + }, + 'asn': { + 'queryset': ASN.objects.prefetch_related('rir', 'tenant', 'tenant__group'), + 'filterset': ipam.filtersets.ASNFilterSet, + 'table': ipam.tables.ASNTable, + 'url': 'ipam:asn_list', + }, + 'service': { + 'queryset': Service.objects.prefetch_related('device', 'virtual_machine'), + 'filterset': ipam.filtersets.ServiceFilterSet, + 'table': ipam.tables.ServiceTable, + 'url': 'ipam:service_list', + }, +} + +TENANCY_TYPES = { + 'tenant': { + 'queryset': Tenant.objects.prefetch_related('group'), + 'filterset': tenancy.filtersets.TenantFilterSet, + 'table': tenancy.tables.TenantTable, + 'url': 'tenancy:tenant_list', + }, + 'contact': { + 'queryset': Contact.objects.prefetch_related('group', 'assignments').annotate( + assignment_count=count_related(ContactAssignment, 'contact')), + 'filterset': tenancy.filtersets.ContactFilterSet, + 'table': tenancy.tables.ContactTable, + 'url': 'tenancy:contact_list', + }, +} + +VIRTUALIZATION_TYPES = { + 'cluster': { + 'queryset': Cluster.objects.prefetch_related('type', 'group').annotate( + device_count=count_related(Device, 'cluster'), + vm_count=count_related(VirtualMachine, 'cluster') + ), + 'filterset': virtualization.filtersets.ClusterFilterSet, + 'table': virtualization.tables.ClusterTable, + 'url': 'virtualization:cluster_list', + }, + 'virtualmachine': { + 'queryset': VirtualMachine.objects.prefetch_related( + 'cluster', 'tenant', 'tenant__group', 'platform', 'primary_ip4', 'primary_ip6', + ), + 'filterset': virtualization.filtersets.VirtualMachineFilterSet, + 'table': virtualization.tables.VirtualMachineTable, + 'url': 'virtualization:virtualmachine_list', + }, +} + +WIRELESS_TYPES = { + 'wirelesslan': { + 'queryset': WirelessLAN.objects.prefetch_related('group', 'vlan').annotate( + interface_count=count_related(Interface, 'wireless_lans') + ), + 'filterset': wireless.filtersets.WirelessLANFilterSet, + 'table': wireless.tables.WirelessLANTable, + 'url': 'wireless:wirelesslan_list', + }, + 'wirelesslink': { + 'queryset': WirelessLink.objects.prefetch_related('interface_a__device', 'interface_b__device'), + 'filterset': wireless.filtersets.WirelessLinkFilterSet, + 'table': wireless.tables.WirelessLinkTable, + 'url': 'wireless:wirelesslink_list', + }, +} + +SEARCH_TYPE_HIERARCHY = { + 'Circuits': CIRCUIT_TYPES, + 'DCIM': DCIM_TYPES, + 'IPAM': IPAM_TYPES, + 'Tenancy': TENANCY_TYPES, + 'Virtualization': VIRTUALIZATION_TYPES, + 'Wireless': WIRELESS_TYPES, +} + + +def build_search_types(): + 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/views/__init__.py b/netbox/netbox/views/__init__.py index f159ee637..204fce469 100644 --- a/netbox/netbox/views/__init__.py +++ b/netbox/netbox/views/__init__.py @@ -22,8 +22,9 @@ from dcim.models import ( from extras.models import ObjectChange from extras.tables import ObjectChangeTable from ipam.models import Aggregate, IPAddress, IPRange, Prefix, VLAN, VRF -from netbox.constants import SEARCH_MAX_RESULTS, SEARCH_TYPES +from netbox.constants import SEARCH_MAX_RESULTS from netbox.forms import SearchForm +from netbox.search import SEARCH_TYPES from tenancy.models import Tenant from virtualization.models import Cluster, VirtualMachine from wireless.models import WirelessLAN, WirelessLink From e07dd3ddcb2b8453d5a72ccb984e456297fb8296 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 15 Jul 2022 15:31:42 -0400 Subject: [PATCH 06/17] Define NESTED_SERIALIZER_PREFIX constant --- netbox/dcim/api/serializers.py | 17 +++++++++-------- netbox/dcim/api/views.py | 5 +++-- netbox/extras/api/customfields.py | 5 +++-- netbox/extras/api/serializers.py | 7 ++++--- netbox/ipam/api/serializers.py | 7 ++++--- netbox/netbox/api/viewsets/__init__.py | 3 ++- netbox/netbox/constants.py | 3 +++ netbox/tenancy/api/serializers.py | 3 ++- 8 files changed, 30 insertions(+), 20 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index c2cb846a9..d95480aa7 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -15,6 +15,7 @@ from netbox.api.serializers import ( NestedGroupModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer, WritableNestedSerializer, ) from netbox.config import ConfigItem +from netbox.constants import NESTED_SERIALIZER_PREFIX from tenancy.api.nested_serializers import NestedTenantSerializer from users.api.nested_serializers import NestedUserSerializer from utilities.api import get_serializer_for_model @@ -41,7 +42,7 @@ class LinkTerminationSerializer(serializers.ModelSerializer): Return the appropriate serializer for the link termination model. """ if obj._link_peer is not None: - serializer = get_serializer_for_model(obj._link_peer, prefix='Nested') + serializer = get_serializer_for_model(obj._link_peer, prefix=NESTED_SERIALIZER_PREFIX) context = {'request': self.context['request']} return serializer(obj._link_peer, context=context).data return None @@ -67,7 +68,7 @@ class ConnectedEndpointSerializer(serializers.ModelSerializer): Return the appropriate serializer for the type of connected object. """ if obj._path is not None and obj._path.destination is not None: - serializer = get_serializer_for_model(obj._path.destination, prefix='Nested') + serializer = get_serializer_for_model(obj._path.destination, prefix=NESTED_SERIALIZER_PREFIX) context = {'request': self.context['request']} return serializer(obj._path.destination, context=context).data return None @@ -543,7 +544,7 @@ class InventoryItemTemplateSerializer(ValidatedModelSerializer): def get_component(self, obj): if obj.component is None: return None - serializer = get_serializer_for_model(obj.component, prefix='Nested') + serializer = get_serializer_for_model(obj.component, prefix=NESTED_SERIALIZER_PREFIX) context = {'request': self.context['request']} return serializer(obj.component, context=context).data @@ -935,7 +936,7 @@ class InventoryItemSerializer(NetBoxModelSerializer): def get_component(self, obj): if obj.component is None: return None - serializer = get_serializer_for_model(obj.component, prefix='Nested') + serializer = get_serializer_for_model(obj.component, prefix=NESTED_SERIALIZER_PREFIX) context = {'request': self.context['request']} return serializer(obj.component, context=context).data @@ -991,7 +992,7 @@ class CableSerializer(NetBoxModelSerializer): termination = getattr(obj, 'termination_{}'.format(side.lower())) if termination is None: return None - serializer = get_serializer_for_model(termination, prefix='Nested') + serializer = get_serializer_for_model(termination, prefix=NESTED_SERIALIZER_PREFIX) context = {'request': self.context['request']} data = serializer(termination, context=context).data @@ -1037,7 +1038,7 @@ class CablePathSerializer(serializers.ModelSerializer): """ Return the appropriate serializer for the origin. """ - serializer = get_serializer_for_model(obj.origin, prefix='Nested') + serializer = get_serializer_for_model(obj.origin, prefix=NESTED_SERIALIZER_PREFIX) context = {'request': self.context['request']} return serializer(obj.origin, context=context).data @@ -1047,7 +1048,7 @@ class CablePathSerializer(serializers.ModelSerializer): Return the appropriate serializer for the destination, if any. """ if obj.destination_id is not None: - serializer = get_serializer_for_model(obj.destination, prefix='Nested') + serializer = get_serializer_for_model(obj.destination, prefix=NESTED_SERIALIZER_PREFIX) context = {'request': self.context['request']} return serializer(obj.destination, context=context).data return None @@ -1056,7 +1057,7 @@ class CablePathSerializer(serializers.ModelSerializer): def get_path(self, obj): ret = [] for node in obj.get_path(): - serializer = get_serializer_for_model(node, prefix='Nested') + serializer = get_serializer_for_model(node, prefix=NESTED_SERIALIZER_PREFIX) context = {'request': self.context['request']} ret.append(serializer(node, context=context).data) return ret diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 3fa652a9b..6387e26f8 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -22,6 +22,7 @@ from netbox.api.metadata import ContentTypeMetadata from netbox.api.pagination import StripCountAnnotationsPaginator from netbox.api.viewsets import NetBoxModelViewSet from netbox.config import get_config +from netbox.constants import NESTED_SERIALIZER_PREFIX from utilities.api import get_serializer_for_model from utilities.utils import count_related from virtualization.models import VirtualMachine @@ -69,14 +70,14 @@ class PathEndpointMixin(object): break # Serialize each object - serializer_a = get_serializer_for_model(near_end, prefix='Nested') + serializer_a = get_serializer_for_model(near_end, prefix=NESTED_SERIALIZER_PREFIX) x = serializer_a(near_end, context={'request': request}).data if cable is not None: y = serializers.TracedCableSerializer(cable, context={'request': request}).data else: y = None if far_end is not None: - serializer_b = get_serializer_for_model(far_end, prefix='Nested') + serializer_b = get_serializer_for_model(far_end, prefix=NESTED_SERIALIZER_PREFIX) z = serializer_b(far_end, context={'request': request}).data else: z = None diff --git a/netbox/extras/api/customfields.py b/netbox/extras/api/customfields.py index fd6e1f550..b7fd1e129 100644 --- a/netbox/extras/api/customfields.py +++ b/netbox/extras/api/customfields.py @@ -3,6 +3,7 @@ from rest_framework.fields import Field from extras.choices import CustomFieldTypeChoices from extras.models import CustomField +from netbox.constants import NESTED_SERIALIZER_PREFIX # @@ -51,10 +52,10 @@ class CustomFieldsDataField(Field): for cf in self._get_custom_fields(): value = cf.deserialize(obj.get(cf.name)) if value is not None and cf.type == CustomFieldTypeChoices.TYPE_OBJECT: - serializer = get_serializer_for_model(cf.object_type.model_class(), prefix='Nested') + serializer = get_serializer_for_model(cf.object_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX) value = serializer(value, context=self.parent.context).data elif value is not None and cf.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT: - serializer = get_serializer_for_model(cf.object_type.model_class(), prefix='Nested') + serializer = get_serializer_for_model(cf.object_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX) value = serializer(value, many=True, context=self.parent.context).data data[cf.name] = value diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index bdb54067a..a4e3c6609 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -15,6 +15,7 @@ from extras.utils import FeatureQuery from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField from netbox.api.exceptions import SerializerNotFound from netbox.api.serializers import BaseModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer +from netbox.constants import NESTED_SERIALIZER_PREFIX from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer from tenancy.models import Tenant, TenantGroup from users.api.nested_serializers import NestedUserSerializer @@ -192,7 +193,7 @@ class ImageAttachmentSerializer(ValidatedModelSerializer): @swagger_serializer_method(serializer_or_field=serializers.DictField) def get_parent(self, obj): - serializer = get_serializer_for_model(obj.parent, prefix='Nested') + serializer = get_serializer_for_model(obj.parent, prefix=NESTED_SERIALIZER_PREFIX) return serializer(obj.parent, context={'request': self.context['request']}).data @@ -242,7 +243,7 @@ class JournalEntrySerializer(NetBoxModelSerializer): @swagger_serializer_method(serializer_or_field=serializers.DictField) def get_assigned_object(self, instance): - serializer = get_serializer_for_model(instance.assigned_object_type.model_class(), prefix='Nested') + serializer = get_serializer_for_model(instance.assigned_object_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX) context = {'request': self.context['request']} return serializer(instance.assigned_object, context=context).data @@ -462,7 +463,7 @@ class ObjectChangeSerializer(BaseModelSerializer): return None try: - serializer = get_serializer_for_model(obj.changed_object, prefix='Nested') + serializer = get_serializer_for_model(obj.changed_object, prefix=NESTED_SERIALIZER_PREFIX) except SerializerNotFound: return obj.object_repr context = { diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 3fa1bcc7e..4462ed697 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -10,6 +10,7 @@ from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, VLANGROUP_SCOPE_TYPES from ipam.models import * from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField from netbox.api.serializers import NetBoxModelSerializer +from netbox.constants import NESTED_SERIALIZER_PREFIX from tenancy.api.nested_serializers import NestedTenantSerializer from utilities.api import get_serializer_for_model from virtualization.api.nested_serializers import NestedVirtualMachineSerializer @@ -145,7 +146,7 @@ class FHRPGroupAssignmentSerializer(NetBoxModelSerializer): def get_interface(self, obj): if obj.interface is None: return None - serializer = get_serializer_for_model(obj.interface, prefix='Nested') + serializer = get_serializer_for_model(obj.interface, prefix=NESTED_SERIALIZER_PREFIX) context = {'request': self.context['request']} return serializer(obj.interface, context=context).data @@ -191,7 +192,7 @@ class VLANGroupSerializer(NetBoxModelSerializer): def get_scope(self, obj): if obj.scope_id is None: return None - serializer = get_serializer_for_model(obj.scope, prefix='Nested') + serializer = get_serializer_for_model(obj.scope, prefix=NESTED_SERIALIZER_PREFIX) context = {'request': self.context['request']} return serializer(obj.scope, context=context).data @@ -375,7 +376,7 @@ class IPAddressSerializer(NetBoxModelSerializer): def get_assigned_object(self, obj): if obj.assigned_object is None: return None - serializer = get_serializer_for_model(obj.assigned_object, prefix='Nested') + serializer = get_serializer_for_model(obj.assigned_object, prefix=NESTED_SERIALIZER_PREFIX) context = {'request': self.context['request']} return serializer(obj.assigned_object, context=context).data diff --git a/netbox/netbox/api/viewsets/__init__.py b/netbox/netbox/api/viewsets/__init__.py index 462c07c6f..0f149240d 100644 --- a/netbox/netbox/api/viewsets/__init__.py +++ b/netbox/netbox/api/viewsets/__init__.py @@ -10,6 +10,7 @@ from rest_framework.viewsets import ModelViewSet from extras.models import ExportTemplate from netbox.api.exceptions import SerializerNotFound +from netbox.constants import NESTED_SERIALIZER_PREFIX from utilities.api import get_serializer_for_model from .mixins import * @@ -60,7 +61,7 @@ class NetBoxModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ObjectVali if self.brief: logger.debug("Request is for 'brief' format; initializing nested serializer") try: - serializer = get_serializer_for_model(self.queryset.model, prefix='Nested') + serializer = get_serializer_for_model(self.queryset.model, prefix=NESTED_SERIALIZER_PREFIX) logger.debug(f"Using serializer {serializer}") return serializer except SerializerNotFound: diff --git a/netbox/netbox/constants.py b/netbox/netbox/constants.py index 61588d331..776938a97 100644 --- a/netbox/netbox/constants.py +++ b/netbox/netbox/constants.py @@ -1,2 +1,5 @@ +# Prefix for nested serializers +NESTED_SERIALIZER_PREFIX = 'Nested' + # Max results per object type SEARCH_MAX_RESULTS = 15 diff --git a/netbox/tenancy/api/serializers.py b/netbox/tenancy/api/serializers.py index a2286efed..626554077 100644 --- a/netbox/tenancy/api/serializers.py +++ b/netbox/tenancy/api/serializers.py @@ -4,6 +4,7 @@ from rest_framework import serializers from netbox.api import ChoiceField, ContentTypeField from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer +from netbox.constants import NESTED_SERIALIZER_PREFIX from tenancy.choices import ContactPriorityChoices from tenancy.models import * from utilities.api import get_serializer_for_model @@ -108,6 +109,6 @@ class ContactAssignmentSerializer(NetBoxModelSerializer): @swagger_serializer_method(serializer_or_field=serializers.DictField) def get_object(self, instance): - serializer = get_serializer_for_model(instance.content_type.model_class(), prefix='Nested') + serializer = get_serializer_for_model(instance.content_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX) context = {'request': self.context['request']} return serializer(instance.object, context=context).data From 250265c3d951e588a19a233fb4893fd98f68f771 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 15 Jul 2022 15:40:55 -0400 Subject: [PATCH 07/17] Fixes #9746: Permit filtering interfaces by arbitrary speed value in UI --- docs/release-notes/version-3.2.md | 1 + netbox/dcim/forms/filtersets.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index 3eb6a7a15..ce491e0db 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -11,6 +11,7 @@ * [#9715](https://github.com/netbox-community/netbox/issues/9715) - Fix `SOCIAL_AUTH_PIPELINE` config parameter not taking effect * [#9734](https://github.com/netbox-community/netbox/issues/9734) - Fix stealing focus by select fields in forms +* [#9746](https://github.com/netbox-community/netbox/issues/9746) - Permit filtering interfaces by arbitrary speed value in UI --- diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 1535e5718..38221b371 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -982,8 +982,8 @@ class InterfaceFilterForm(DeviceComponentFilterForm): ) speed = forms.IntegerField( required=False, - label='Select Speed', - widget=SelectSpeedWidget(attrs={'readonly': None}) + label='Speed', + widget=SelectSpeedWidget() ) duplex = MultipleChoiceField( choices=InterfaceDuplexChoices, From 3d475e5afa2bbee182d33d6f9e2b08b42e6fa866 Mon Sep 17 00:00:00 2001 From: Stephen Muth Date: Wed, 13 Jul 2022 19:22:47 -0400 Subject: [PATCH 08/17] Fixes #9634: Respect image URLs which are already fully formed For local storage, URLs will always be relative, but some custom storage backends such as S3 may return absolute ones. --- netbox/dcim/svg.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/netbox/dcim/svg.py b/netbox/dcim/svg.py index 1de68ec36..4c093a312 100644 --- a/netbox/dcim/svg.py +++ b/netbox/dcim/svg.py @@ -113,8 +113,12 @@ class RackElevationSVG: # Embed front device type image if one exists if self.include_images and device.device_type.front_image: + url = device.device_type.front_image.url + # Convert any relative URLs to absolute + if url.startswith('/'): + url = '{}{}'.format(self.base_url, url) image = drawing.image( - href='{}{}'.format(self.base_url, device.device_type.front_image.url), + href=url, insert=start, size=end, class_='device-image' @@ -139,8 +143,12 @@ class RackElevationSVG: # Embed rear device type image if one exists if self.include_images and device.device_type.rear_image: + url = device.device_type.rear_image.url + # Convert any relative URLs to absolute + if url.startswith('/'): + url = '{}{}'.format(self.base_url, url) image = drawing.image( - href='{}{}'.format(self.base_url, device.device_type.rear_image.url), + href=url, insert=start, size=end, class_='device-image' From b854cefb57ed3b5073498e3660119d79c6119b97 Mon Sep 17 00:00:00 2001 From: kkthxbye-code Date: Sun, 17 Jul 2022 17:33:47 +0200 Subject: [PATCH 09/17] Revert #9735 & #9696 --- netbox/project-static/dist/netbox.js | Bin 376234 -> 376144 bytes netbox/project-static/dist/netbox.js.map | Bin 345631 -> 345564 bytes .../src/select/api/apiSelect.ts | 6 ------ 3 files changed, 6 deletions(-) diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index c766b8a72357ae532a9e3a1e3c988ac73050d0fc..bc0cabef08f2cab679e475fe18741c4e97ee187e 100644 GIT binary patch delta 25 hcmZ4WTkOIwv4$4L7N!>F7M3lnH-okt2eX!E0RWSD3OxV- delta 70 zcmcccOKjC|v4$4L7N!>F7M3lnH-lJ;b24+MPrSw=G2OqNnUhDUxL7y8AT>`(Q%!BU aV=k*0cV=EeX^CE1esXE?_KQKR^;rP0AsL|n diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index 55474df17cba29ff6f6346049b65baf3886a7ca2..26bb1c514297c61a2e7d50a8863067ed8656e455 100644 GIT binary patch delta 52 zcmbQ=BYLM>w4sHug{g&k3(MheOwK;rIli+5@o^beI8{08_&XX^Y)@fj^<`%=x7@y& Ihc(9%0Px=t4gdfE delta 99 zcmcc9EjqtPw4sHug{g&k3(MheYVIyN-j0smNJOBcW1vo`qhqK}u%lzJPNbu=sq^#) uw^*cD10CHhowgtS#uCKGl~L(b<*eiHm|3}9k(Je#oheUydjSt?jwJw9*dGo6 diff --git a/netbox/project-static/src/select/api/apiSelect.ts b/netbox/project-static/src/select/api/apiSelect.ts index 4ae58d8f4..f5b605d58 100644 --- a/netbox/project-static/src/select/api/apiSelect.ts +++ b/netbox/project-static/src/select/api/apiSelect.ts @@ -411,12 +411,6 @@ export class APISelect { } finally { this.setOptionStyles(); this.enable(); - - // Set the focus to the search field if the select is open - if (this.slim.slim.content.classList.contains("ss-open")) { - this.slim.slim.search.input.focus(); - } - this.base.dispatchEvent(this.loadEvent); } } From 1a028f77d47e522f00e23c49721ba5e13c9b92f5 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 18 Jul 2022 09:16:43 -0400 Subject: [PATCH 10/17] Changelog for #9754 --- docs/release-notes/version-3.2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index ce491e0db..8884a3c91 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -10,7 +10,7 @@ ### Bug Fixes * [#9715](https://github.com/netbox-community/netbox/issues/9715) - Fix `SOCIAL_AUTH_PIPELINE` config parameter not taking effect -* [#9734](https://github.com/netbox-community/netbox/issues/9734) - Fix stealing focus by select fields in forms +* [#9754](https://github.com/netbox-community/netbox/issues/9754) - Fix regression introduced by #9632 * [#9746](https://github.com/netbox-community/netbox/issues/9746) - Permit filtering interfaces by arbitrary speed value in UI --- From a7a20ad2eacec59652933dfe0ecf6c80b761114c Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 19 Jul 2022 12:53:57 -0400 Subject: [PATCH 11/17] Fixes #9775: Fix exception when viewing a report with no descripton --- docs/release-notes/version-3.2.md | 2 ++ netbox/utilities/templatetags/builtins/filters.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index 8884a3c91..40aee82ad 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -9,9 +9,11 @@ ### Bug Fixes +* [#9634](https://github.com/netbox-community/netbox/issues/9634) - Fix image URLs in rack elevations when using external storage * [#9715](https://github.com/netbox-community/netbox/issues/9715) - Fix `SOCIAL_AUTH_PIPELINE` config parameter not taking effect * [#9754](https://github.com/netbox-community/netbox/issues/9754) - Fix regression introduced by #9632 * [#9746](https://github.com/netbox-community/netbox/issues/9746) - Permit filtering interfaces by arbitrary speed value in UI +* [#9775](https://github.com/netbox-community/netbox/issues/9775) - Fix exception when viewing a report with no description --- diff --git a/netbox/utilities/templatetags/builtins/filters.py b/netbox/utilities/templatetags/builtins/filters.py index 738dc0e00..5a6841286 100644 --- a/netbox/utilities/templatetags/builtins/filters.py +++ b/netbox/utilities/templatetags/builtins/filters.py @@ -144,6 +144,8 @@ def render_markdown(value): {{ md_source_text|markdown }} """ + if not value: + return '' # Render Markdown html = markdown(value, extensions=['def_list', 'fenced_code', 'tables', StrikethroughExtension()]) From 802d9d2b6ec178fbae7bfee1947af3af0589e538 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 19 Jul 2022 13:01:51 -0400 Subject: [PATCH 12/17] Fixes #9749: Retain original slug values when modifying object names --- docs/release-notes/version-3.2.md | 1 + netbox/project-static/dist/netbox.js | Bin 376144 -> 376156 bytes netbox/project-static/dist/netbox.js.map | Bin 345564 -> 345579 bytes netbox/project-static/src/buttons/reslug.ts | 4 +++- 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index 40aee82ad..6718fc247 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -13,6 +13,7 @@ * [#9715](https://github.com/netbox-community/netbox/issues/9715) - Fix `SOCIAL_AUTH_PIPELINE` config parameter not taking effect * [#9754](https://github.com/netbox-community/netbox/issues/9754) - Fix regression introduced by #9632 * [#9746](https://github.com/netbox-community/netbox/issues/9746) - Permit filtering interfaces by arbitrary speed value in UI +* [#9749](https://github.com/netbox-community/netbox/issues/9749) - Retain original slug values when modifying object names * [#9775](https://github.com/netbox-community/netbox/issues/9775) - Fix exception when viewing a report with no description --- diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index bc0cabef08f2cab679e475fe18741c4e97ee187e..3b9d253c910687cab913fa4410ebf02d56259832 100644 GIT binary patch delta 53 zcmcccOYF`sv4$4L7N!>F7M3lnagEY7H5w&(dS!_@rKz@Mt{O#9woYQ6=Jwo1*2mTW DLZ}p; delta 37 tcmccfOYFigv4$4L7N!>F7M3lnagEb68(9SyYp2JrW|iAMvyt_%H2^BW4yynF diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index 26bb1c514297c61a2e7d50a8863067ed8656e455..5a78b64f1000be47875ef11fdecfa3e13886291a 100644 GIT binary patch delta 60 zcmcc9E&94!w4sHug{g(Pg=GtCwyc1oqocQuyQ5>Mj=!UGl8)!}99dQgR)0tLESKq> QvaA-2McWU_vPx+H07u{vvH$=8 delta 52 zcmaFeEqbS0w4sHug{g(Pg=GtCw(Rt+vaC{^j;{L7I_{2+;nM|OSVgCAl4X@+%-enu HD5U`a$}|t6 diff --git a/netbox/project-static/src/buttons/reslug.ts b/netbox/project-static/src/buttons/reslug.ts index 2549bf112..f445854c1 100644 --- a/netbox/project-static/src/buttons/reslug.ts +++ b/netbox/project-static/src/buttons/reslug.ts @@ -38,7 +38,9 @@ export function initReslug(): void { slugLength = Number(slugLengthAttr); } sourceField.addEventListener('blur', () => { - slugField.value = slugify(sourceField.value, slugLength); + if (!slugField.value) { + slugField.value = slugify(sourceField.value, slugLength); + } }); slugButton.addEventListener('click', () => { slugField.value = slugify(sourceField.value, slugLength); From 44586743ea410e9df11d88ca3580ccc08a71bd17 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 19 Jul 2022 14:21:20 -0400 Subject: [PATCH 13/17] Fixes #9437: Standardize form submission buttons and behavior when using enter key --- docs/release-notes/version-3.2.md | 1 + netbox/project-static/dist/netbox.js | Bin 376156 -> 375907 bytes netbox/project-static/dist/netbox.js.map | Bin 345579 -> 345348 bytes netbox/project-static/src/forms/elements.ts | 40 +--------------- .../circuits/circuittermination_edit.html | 14 ------ netbox/templates/dcim/interface_edit.html | 11 ----- netbox/templates/generic/bulk_delete.html | 2 +- netbox/templates/generic/bulk_edit.html | 2 +- netbox/templates/generic/bulk_import.html | 12 ++--- netbox/templates/generic/bulk_remove.html | 2 +- netbox/templates/generic/bulk_rename.html | 2 +- .../templates/generic/confirmation_form.html | 45 +++++++----------- netbox/templates/generic/object_edit.html | 8 ++-- netbox/templates/generic/object_import.html | 30 ++++++------ .../virtualization/vminterface_edit.html | 11 ----- 15 files changed, 49 insertions(+), 131 deletions(-) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index 6718fc247..7b9adb374 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -9,6 +9,7 @@ ### Bug Fixes +* [#9437](https://github.com/netbox-community/netbox/issues/9437) - Standardize form submission buttons and behavior when using enter key * [#9634](https://github.com/netbox-community/netbox/issues/9634) - Fix image URLs in rack elevations when using external storage * [#9715](https://github.com/netbox-community/netbox/issues/9715) - Fix `SOCIAL_AUTH_PIPELINE` config parameter not taking effect * [#9754](https://github.com/netbox-community/netbox/issues/9754) - Fix regression introduced by #9632 diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index 3b9d253c910687cab913fa4410ebf02d56259832..9ea2e5c7c51459ff2a2b51720f271929a0119eb8 100644 GIT binary patch delta 3383 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 U%d0n*FF8abAyVCbi1rWrFTc>I(f|Me delta 3660 zcmb7GdsI}_8eiX;y$y9~88}iZ!Kg5!M-(!zNn`R5W|Wsa6eW@YhSxBAoZ$>Aqa{89 z1w}pJ1|-b4>GeSeI+W#uZtM2AnXT^9@|};arCC2G1?S;kZ*#xau^&&sw0`>*)oA_U?Hr^v`Ou#Q>W&aI zoP2!{43+~9FQ=9WG3D;E{V<1k#d<_9Aji4K7Qt-!^s&>(XJ>aV%#ro)eT;NodM^nQ zl<|Mb(;=VoUk|}_Ipu>9NN4#6Z7^4P{6iB^yIW*MJuZ~Yk5+;px#hDT@2AWyy6$Yt z+y8a}8D8;8Ba|pl9-j%+3uZ~n(m5Ut49;&n*>>Ff1Kf9Z{-NWXmjyKb;5 z`){UG;&6-6a{EoaGW6$HC`omTj-J(_e?3beP5I~Vn+34&etlsA!t;G$&J)-Macpb> z(|Q0u3YnP6%yu@`!k+^ek5W#6QGG3J3T`f5Y~hdfgHh^H00G;9PwEe02)X@Xo4Qs9 zX9Or!OZ&k8;iSE9FHTf+f^~JzzKZs@3gA|C1EGE(B&hN**aMKorvyPV%vIL}!8yFr z7+w|(4Zt{rz&`M(2SVU;3gznZFgPoKjpvSr3Mf-gjRre_S2c@}J%G%P5v}~IXwdW4 zXJC^0?X%$Q0~R%FswQONhoj*sn5CYNh93bk)z4y~-H*(P5z~5f)Ye3(dj!Qy8IXrs zbs1nBL}tW@rP^^S|1c3mbwVM$`xvz_lh305QUP=jNr(~McYP+pI5n;Y&i2Q!LmI$; zz;tU=p_k=LB`y{`U5##lS^y8ns@wo2YVJmO8s`LW;IIki^6^cu4rZ#ynqV#9GPMN; zgG()JfdHHg+_wpSnQV8MlNK3GqU12AX&ArA?ADMu%rP2z9A=4`r{JHNdCSV~`z}%U zw!%mq+B>ls_Ui<@gD>3%v4Z5_?{7mg%GIvzFj2ts?{~ry9meR~1K}8B>mK+)z~eRh zU;qG9H|+=2Kh(mEX-rIHrX_!5_s5pcW0$-h7Hnbi)Z>ACT{nzDF5l?}F$(dU-(jrA z?T}odD+j4Q@vbl_z`{g33(vPR$?i!hEG)N6W{W!Puux7=VqLe8h;Xi3=tMpFJz-S< zE{A_1m~ff!g`n3i_{H-=y>`JjF9=!M_C=wqFIe9ERv4r0ir0k@gyic&r{2P{Q&^5T zw+BpOafvyWJ#XPp+z_zz)#w|-cl{B^-4?EDnuC86%!9F~%!Yf38bU*jUWV}%{U%)h z(XXV^oaQm66?4~3-C))Aovs_esYZUUOT-DRzI;pf|rPzUhd6vmPkgEY03S2PqFgo#l&BYe3?uG zQ>@i!@}WVa7O=#EU4_AJW;CF0$;GnoJI~=u8)+a-U!MKWi$>VEDJ}+2Jp5s?* z$xcY(TUL`&-d;;4fK~h6!E5V~`#9cSM~2`r!eD;Bj`Z=jqW`EokKN{o%D0u<4Ti~y z>aTUAF9bMx$&&}HB`@exdpTi79&$$K&zskh$bjTt1k?*_i9{idXEcy}%%ZD-qzk21 zKBbcet799900Hja)M7Uh3Wijxm~0gtRx!;gx~*cVHOC~e2br^|rJTeH3Gr6Gwi!bC=oT^y=J2W(G8snlcUnjrRPyXiWCP6RH#d=Ln8hnv zNfl)B+pS~^%-}7rl2jO}UVoJY^+C~o+sPypMR5e{>pZej+Q6wAV!#> z+IEna0227con$q{bH^@Xel*K!#2UCaOcJ=dix?nFJ-v&pFCK{cVHB+sicoW zK5wd|&A1imqj9)e;G+}4%lG@}Hg2s#YlnQ)z^fKh1!bODLT4kaSwh7jUWbuso%B#k z@)JwwoVzkmsLpSfu2vFhCP30mYDAaHiEzX3Qa;2e${mDpbCfCkXTYwECnaI3hV|kkC#`| zsj!H5RMYV=jbE*%kuZ-3*3e80nbpt<5qXZu+6!qm6xcitZJv8rcnM>6PlkP=WUhE1 zA$-QP3C2IKp&h7Qvx;^FPIHL!962UC)9Um7bV}#9R?`vOx0-rkfr{UK0E4()OWOh# zI{tTX@jZ2PGNkjXb#%P{f(Kmo49)C?+_(m5a;rbBp;-`+(VK;N^)yPK;^=9Uhi-r$ ztfw)LXp?tDNde!tj%E&v?N#uu#L;>r$*pMx^3HWwY0+xRdYYn@=H>?aG@so_ANT7q zXDcY+;~Qz1f6Bu#BTb==G{Qf&mxIzqtk-CDOC$ZcKPI(t3tg=J0QBEZYp~wc*4OF3 zHEcRSR|$~F4R25rx=wt9&O~T?11U}90Sexyn@>|{4#GACJ1dTl>7aias0Ef?ym0KD zr;)cSbU0trK}Tv4+p+qYT$K_azc!^`-*meqm0^*@@rxbw1)TNfPU?gtb#Eu_>hoW! C?*+2} diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index 5a78b64f1000be47875ef11fdecfa3e13886291a..60656bc6d6f3450acca6ca6d4369da29b4749527 100644 GIT binary patch delta 49 zcmaFeE!xs0+R(z-!qmdNg(cOOr64uKdApz=iy|Xau*dW~f0k-CU*|GsbJy)({8_$# F1pt|^5fA_X delta 281 zcmXw!F^j@L5QS0G$(8;9Yez&l5y9ro=t)>|i^Lc$#Rvu>h+ZQWTBN$O_m2qvC~LXa zOoGkwcHev7&ZNJ!$6xK7meO)sxj8HEw2Rh4b6_Jh0uD?9X%AF;>hhgz0|i;<$g*&t7tG(M4-I})1DzozJjHgftYEmEEj%{ua|G - * Save & Continue - * - * ``` - * - * @param event Click event. - */ -function handleSubmitWithReturnUrl(event: MouseEvent): void { - const element = event.target as HTMLElement; - if (element.tagName === 'BUTTON') { - const button = element as HTMLButtonElement; - const action = button.getAttribute('return-url'); - const form = button.form; - if (form !== null && isTruthy(action)) { - form.action = action; - form.submit(); - } - } -} +import { getElements, scrollTo } from '../util'; function handleFormSubmit(event: Event, form: HTMLFormElement): void { // Track the names of each invalid field. @@ -57,15 +29,6 @@ function handleFormSubmit(event: Event, form: HTMLFormElement): void { } } -/** - * Attach event listeners to form buttons with the `return-url` attribute present. - */ -function initReturnUrlSubmitButtons(): void { - for (const button of getElements('button[return-url]')) { - button.addEventListener('click', handleSubmitWithReturnUrl); - } -} - /** * Attach an event listener to each form's submitter (button[type=submit]). When called, the * callback checks the validity of each form field and adds the appropriate Bootstrap CSS class @@ -82,5 +45,4 @@ export function initFormElements(): void { submitter.addEventListener('click', (event: Event) => handleFormSubmit(event, form)); } } - initReturnUrlSubmitButtons(); } diff --git a/netbox/templates/circuits/circuittermination_edit.html b/netbox/templates/circuits/circuittermination_edit.html index f8393f945..8919bbd66 100644 --- a/netbox/templates/circuits/circuittermination_edit.html +++ b/netbox/templates/circuits/circuittermination_edit.html @@ -48,17 +48,3 @@ {% render_field form.description %} {% endblock %} - -{# Override buttons block, 'Create & Add Another'/'_addanother' is not needed on a circuit. #} -{% block buttons %} - Cancel - {% if object.pk %} - - {% else %} - - {% endif %} -{% endblock buttons %} diff --git a/netbox/templates/dcim/interface_edit.html b/netbox/templates/dcim/interface_edit.html index ddda1ae31..62aa54ef3 100644 --- a/netbox/templates/dcim/interface_edit.html +++ b/netbox/templates/dcim/interface_edit.html @@ -91,14 +91,3 @@ {% endif %} {% endblock %} - -{% block buttons %} - Cancel - {% if object.pk %} - - - {% else %} - - - {% endif %} -{% endblock %} diff --git a/netbox/templates/generic/bulk_delete.html b/netbox/templates/generic/bulk_delete.html index 1bcc2db1d..edc5132ce 100644 --- a/netbox/templates/generic/bulk_delete.html +++ b/netbox/templates/generic/bulk_delete.html @@ -36,8 +36,8 @@ Context: {{ field }} {% endfor %}
- Cancel + Cancel
diff --git a/netbox/templates/generic/bulk_edit.html b/netbox/templates/generic/bulk_edit.html index 362644135..cea50eaff 100644 --- a/netbox/templates/generic/bulk_edit.html +++ b/netbox/templates/generic/bulk_edit.html @@ -118,8 +118,8 @@ Context:
- Cancel + Cancel
diff --git a/netbox/templates/generic/bulk_import.html b/netbox/templates/generic/bulk_import.html index 1a85c3a21..1d638cb2c 100644 --- a/netbox/templates/generic/bulk_import.html +++ b/netbox/templates/generic/bulk_import.html @@ -44,12 +44,12 @@ Context:
-
- {% if return_url %} - Cancel - {% endif %} - -
+
+ + {% if return_url %} + Cancel + {% endif %} +
{% if fields %} diff --git a/netbox/templates/generic/bulk_remove.html b/netbox/templates/generic/bulk_remove.html index 0bda6adbc..6dc102b5a 100644 --- a/netbox/templates/generic/bulk_remove.html +++ b/netbox/templates/generic/bulk_remove.html @@ -23,8 +23,8 @@ {{ field }} {% endfor %}
- Cancel + Cancel
diff --git a/netbox/templates/generic/bulk_rename.html b/netbox/templates/generic/bulk_rename.html index 134d3df5a..ef6b18cae 100644 --- a/netbox/templates/generic/bulk_rename.html +++ b/netbox/templates/generic/bulk_rename.html @@ -34,11 +34,11 @@
- Cancel {% if '_preview' in request.POST and not form.errors %} {% endif %} + Cancel
diff --git a/netbox/templates/generic/confirmation_form.html b/netbox/templates/generic/confirmation_form.html index c4c15f7e7..e9d3d01aa 100644 --- a/netbox/templates/generic/confirmation_form.html +++ b/netbox/templates/generic/confirmation_form.html @@ -2,33 +2,24 @@ {% load form_helpers %} {% block content %} -
- -
- -
- {% csrf_token %} - {% for field in form.hidden_fields %} - {{ field }} - {% endfor %} - -
-
{% block confirmation_title %}{% endblock %}
- -
- {% block message %}

Are you sure?

{% endblock %} -
- - - -
- -
- +
+
+
+ {% csrf_token %} + {% for field in form.hidden_fields %} + {{ field }} + {% endfor %} +
+
{% block confirmation_title %}{% endblock %}
+
+ {% block message %}

Are you sure?

{% endblock %} +
+
- +
+
{% endblock %} diff --git a/netbox/templates/generic/object_edit.html b/netbox/templates/generic/object_edit.html index 06308e9ef..892c7d2b1 100644 --- a/netbox/templates/generic/object_edit.html +++ b/netbox/templates/generic/object_edit.html @@ -94,19 +94,19 @@ Context:
{% block buttons %} - Cancel {% if object.pk %} {% else %} - + {% endif %} + Cancel {% endblock buttons %}
diff --git a/netbox/templates/generic/object_import.html b/netbox/templates/generic/object_import.html index ffa16b4c2..4d54fde61 100644 --- a/netbox/templates/generic/object_import.html +++ b/netbox/templates/generic/object_import.html @@ -5,19 +5,19 @@ {% block title %}{{ obj_type|bettertitle }} Import{% endblock %} {% block content %} -
-
-
- {% csrf_token %} - {% render_form form %} -
- {% if return_url %} - Cancel - {% endif %} - - -
-
-
-
+
+
+
+ {% csrf_token %} + {% render_form form %} +
+ + + {% if return_url %} + Cancel + {% endif %} +
+
+
+
{% endblock content %} diff --git a/netbox/templates/virtualization/vminterface_edit.html b/netbox/templates/virtualization/vminterface_edit.html index 496960a64..316900865 100644 --- a/netbox/templates/virtualization/vminterface_edit.html +++ b/netbox/templates/virtualization/vminterface_edit.html @@ -55,14 +55,3 @@
{% endif %} {% endblock %} - -{% block buttons %} - Cancel - {% if object.pk %} - - - {% else %} - - - {% endif %} -{% endblock %} From 1c9db2d9f8dffd3ac1f2f28564e054ca73d54e78 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 19 Jul 2022 16:21:32 -0400 Subject: [PATCH 14/17] Fixes #9499: Fix filtered bulk deletion of VM Interfaces --- docs/release-notes/version-3.2.md | 1 + netbox/virtualization/views.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index 7b9adb374..806b0fa91 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -10,6 +10,7 @@ ### Bug Fixes * [#9437](https://github.com/netbox-community/netbox/issues/9437) - Standardize form submission buttons and behavior when using enter key +* [#9499](https://github.com/netbox-community/netbox/issues/9499) - Fix filtered bulk deletion of VM Interfaces * [#9634](https://github.com/netbox-community/netbox/issues/9634) - Fix image URLs in rack elevations when using external storage * [#9715](https://github.com/netbox-community/netbox/issues/9715) - Fix `SOCIAL_AUTH_PIPELINE` config parameter not taking effect * [#9754](https://github.com/netbox-community/netbox/issues/9754) - Fix regression introduced by #9632 diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index 0b593289b..4cd7da30d 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -471,6 +471,7 @@ class VMInterfaceBulkImportView(generic.BulkImportView): class VMInterfaceBulkEditView(generic.BulkEditView): queryset = VMInterface.objects.all() + filterset = filtersets.VMInterfaceFilterSet table = tables.VMInterfaceTable form = forms.VMInterfaceBulkEditForm @@ -482,6 +483,7 @@ class VMInterfaceBulkRenameView(generic.BulkRenameView): class VMInterfaceBulkDeleteView(generic.BulkDeleteView): queryset = VMInterface.objects.all() + filterset = filtersets.VMInterfaceFilterSet table = tables.VMInterfaceTable From 17e00ac0402a10fd823dc7f223b4db6b1c4ef9e1 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 20 Jul 2022 10:39:36 -0400 Subject: [PATCH 15/17] Fixes #9705: Support filter expressions for the serial field on racks, devices, and inventory items --- docs/release-notes/version-3.2.md | 1 + netbox/dcim/filtersets.py | 9 ++++++--- netbox/dcim/tests/test_filtersets.py | 20 +++++++++++--------- netbox/netbox/filtersets.py | 2 +- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index 806b0fa91..1e422547e 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -4,6 +4,7 @@ ### Enhancements +* [#9705](https://github.com/netbox-community/netbox/issues/9705) - Support filter expressions for the `serial` field on racks, devices, and inventory items * [#9741](https://github.com/netbox-community/netbox/issues/9741) - Check for UserConfig instance during user login * [#9745](https://github.com/netbox-community/netbox/issues/9745) - Add wireless LANs and links to global search diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 4b4201578..f5342106e 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -307,7 +307,7 @@ class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe to_field_name='slug', label='Role (slug)', ) - serial = django_filters.CharFilter( + serial = MultiValueCharFilter( lookup_expr='iexact' ) @@ -1002,10 +1002,13 @@ class ModuleFilterSet(NetBoxModelFilterSet): queryset=Device.objects.all(), label='Device (ID)', ) + serial = MultiValueCharFilter( + lookup_expr='iexact' + ) class Meta: model = Module - fields = ['id', 'serial', 'asset_tag'] + fields = ['id', 'asset_tag'] def search(self, queryset, name, value): if not value.strip(): @@ -1400,7 +1403,7 @@ class InventoryItemFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet): ) component_type = ContentTypeFilter() component_id = MultiValueNumberFilter() - serial = django_filters.CharFilter( + serial = MultiValueCharFilter( lookup_expr='iexact' ) diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index 47aa9368c..cf0f397df 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -494,10 +494,10 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_serial(self): - params = {'serial': 'ABC'} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - params = {'serial': 'abc'} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + params = {'serial': ['ABC', 'DEF']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'serial': ['abc', 'def']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_tenant(self): tenants = Tenant.objects.all()[:2] @@ -1860,7 +1860,9 @@ class ModuleTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) def test_serial(self): - params = {'asset_tag': ['A', 'B']} + params = {'serial': ['A', 'B']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'serial': ['a', 'b']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_asset_tag(self): @@ -3413,10 +3415,10 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_serial(self): - params = {'serial': 'ABC'} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - params = {'serial': 'abc'} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + params = {'serial': ['ABC', 'DEF']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'serial': ['abc', 'def']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_component_type(self): params = {'component_type': 'dcim.interface'} diff --git a/netbox/netbox/filtersets.py b/netbox/netbox/filtersets.py index 1a72d8159..f509afa5b 100644 --- a/netbox/netbox/filtersets.py +++ b/netbox/netbox/filtersets.py @@ -125,7 +125,7 @@ class BaseFilterSet(django_filters.FilterSet): return {} # Skip nonstandard lookup expressions - if existing_filter.method is not None or existing_filter.lookup_expr not in ['exact', 'in']: + if existing_filter.method is not None or existing_filter.lookup_expr not in ['exact', 'iexact', 'in']: return {} # Choose the lookup expression map based on the filter type From 9835d6b2dfd05d4ba1eb4d39dd6e6828948239db Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 20 Jul 2022 10:57:11 -0400 Subject: [PATCH 16/17] Release NetBox v3.2.7 --- .github/ISSUE_TEMPLATE/bug_report.yaml | 2 +- .github/ISSUE_TEMPLATE/feature_request.yaml | 2 +- docs/release-notes/version-3.2.md | 2 +- netbox/netbox/settings.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 78231890b..332a0ad75 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.2.6 + placeholder: v3.2.7 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 71d45092c..ff9b5e358 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.2.6 + placeholder: v3.2.7 validations: required: true - type: dropdown diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index 1e422547e..35e9b9a22 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -1,6 +1,6 @@ # NetBox v3.2 -## v3.2.7 (FUTURE) +## v3.2.7 (2022-07-20) ### Enhancements diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index b17d70e31..094771581 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -29,7 +29,7 @@ django.utils.encoding.force_text = force_str # Environment setup # -VERSION = '3.2.7-dev' +VERSION = '3.2.7' # Hostname HOSTNAME = platform.node() From 383918d83ba039e0e1217afa353312d972c181c3 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 20 Jul 2022 11:15:02 -0400 Subject: [PATCH 17/17] PRVB --- docs/release-notes/version-3.2.md | 4 ++++ netbox/netbox/settings.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index 35e9b9a22..c36344912 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -1,5 +1,9 @@ # NetBox v3.2 +## v3.2.8 (FUTURE) + +--- + ## v3.2.7 (2022-07-20) ### Enhancements diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 094771581..c7e49c1be 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -29,7 +29,7 @@ django.utils.encoding.force_text = force_str # Environment setup # -VERSION = '3.2.7' +VERSION = '3.2.8-dev' # Hostname HOSTNAME = platform.node()