diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index f13168f3c3..23d5b8182d 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.2 + placeholder: v3.1.3 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 277c9724fd..00b464515b 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.2 + placeholder: v3.1.3 validations: required: true - type: dropdown diff --git a/docs/models/extras/customlink.md b/docs/models/extras/customlink.md index 3b502cab24..7fd510841a 100644 --- a/docs/models/extras/customlink.md +++ b/docs/models/extras/customlink.md @@ -55,3 +55,7 @@ The link will only appear when viewing a device with a manufacturer name of "Cis ## Link Groups Group names can be specified to organize links into groups. Links with the same group name will render as a dropdown menu beneath a single button bearing the name of the group. + +## Table Columns + +Custom links can also be included in object tables by selecting the desired links from the table configuration form. When displayed, each link will render as a hyperlink for its corresponding object. When exported (e.g. as CSV data), each link render only its URL. diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index ce6a673271..d27c3f76ff 100644 --- a/docs/release-notes/version-3.1.md +++ b/docs/release-notes/version-3.1.md @@ -1,5 +1,31 @@ # NetBox v3.1 +## v3.1.3 (2021-12-29) + +### Enhancements + +* [#6782](https://github.com/netbox-community/netbox/issues/6782) - Enable the inclusion of custom links in tables +* [#7600](https://github.com/netbox-community/netbox/issues/7600) - Include count of available IPs on prefix view +* [#8034](https://github.com/netbox-community/netbox/issues/8034) - Enable specifying custom field validators during CSV import +* [#8100](https://github.com/netbox-community/netbox/issues/8100) - Add "other" choice for FHRP group protocol +* [#8175](https://github.com/netbox-community/netbox/issues/8175) - Display parent object when attaching an image + +### Bug Fixes + +* [#7246](https://github.com/netbox-community/netbox/issues/7246) - Don't attempt to URL-decode NAPALM response payloads +* [#7290](https://github.com/netbox-community/netbox/issues/7290) - Defer loading API-backed form fields +* [#7887](https://github.com/netbox-community/netbox/issues/7887) - Forward `HTTP_X_FORWARDED_FOR` to custom scripts +* [#7962](https://github.com/netbox-community/netbox/issues/7962) - Fix user menu under report/script result view +* [#7972](https://github.com/netbox-community/netbox/issues/7972) - Standardize name of `RemoteUserBackend` logger +* [#8097](https://github.com/netbox-community/netbox/issues/8097) - Fix styling of Markdown tables +* [#8127](https://github.com/netbox-community/netbox/issues/8127) - Fix disassociation of interface under IP address edit view +* [#8131](https://github.com/netbox-community/netbox/issues/8131) - Restore annotation of available IPs under prefix IPs view +* [#8134](https://github.com/netbox-community/netbox/issues/8134) - Fix bulk editing of objects within dynamic tables +* [#8139](https://github.com/netbox-community/netbox/issues/8139) - Fix rendering of table configuration form under VM interfaces view +* [#8140](https://github.com/netbox-community/netbox/issues/8140) - Restore missing fields on wireless LAN & link REST API serializers + +--- + ## v3.1.2 (2021-12-20) ### Enhancements diff --git a/netbox/circuits/forms/filtersets.py b/netbox/circuits/forms/filtersets.py index 0822ff206a..a668f9b161 100644 --- a/netbox/circuits/forms/filtersets.py +++ b/netbox/circuits/forms/filtersets.py @@ -26,14 +26,12 @@ class ProviderFilterForm(CustomFieldModelFilterForm): region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, - label=_('Region'), - fetch_trigger='open' + label=_('Region') ) site_group_id = DynamicModelMultipleChoiceField( queryset=SiteGroup.objects.all(), required=False, - label=_('Site group'), - fetch_trigger='open' + label=_('Site group') ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), @@ -42,8 +40,7 @@ class ProviderFilterForm(CustomFieldModelFilterForm): 'region_id': '$region_id', 'site_group_id': '$site_group_id', }, - label=_('Site'), - fetch_trigger='open' + label=_('Site') ) asn = forms.IntegerField( required=False, @@ -61,8 +58,7 @@ class ProviderNetworkFilterForm(CustomFieldModelFilterForm): provider_id = DynamicModelMultipleChoiceField( queryset=Provider.objects.all(), required=False, - label=_('Provider'), - fetch_trigger='open' + label=_('Provider') ) tag = TagFilterField(model) @@ -84,14 +80,12 @@ class CircuitFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): type_id = DynamicModelMultipleChoiceField( queryset=CircuitType.objects.all(), required=False, - label=_('Type'), - fetch_trigger='open' + label=_('Type') ) provider_id = DynamicModelMultipleChoiceField( queryset=Provider.objects.all(), required=False, - label=_('Provider'), - fetch_trigger='open' + label=_('Provider') ) provider_network_id = DynamicModelMultipleChoiceField( queryset=ProviderNetwork.objects.all(), @@ -99,8 +93,7 @@ class CircuitFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): query_params={ 'provider_id': '$provider_id' }, - label=_('Provider network'), - fetch_trigger='open' + label=_('Provider network') ) status = forms.MultipleChoiceField( choices=CircuitStatusChoices, @@ -110,14 +103,12 @@ class CircuitFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, - label=_('Region'), - fetch_trigger='open' + label=_('Region') ) site_group_id = DynamicModelMultipleChoiceField( queryset=SiteGroup.objects.all(), required=False, - label=_('Site group'), - fetch_trigger='open' + label=_('Site group') ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), @@ -126,8 +117,7 @@ class CircuitFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): 'region_id': '$region_id', 'site_group_id': '$site_group_id', }, - label=_('Site'), - fetch_trigger='open' + label=_('Site') ) commit_rate = forms.IntegerField( required=False, diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index f359f0f248..5830396ce9 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -15,14 +15,14 @@ from dcim import filtersets from dcim.models import * from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet -from ipam.models import Prefix, VLAN, ASN +from ipam.models import Prefix, VLAN from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired from netbox.api.exceptions import ServiceUnavailable from netbox.api.metadata import ContentTypeMetadata from netbox.api.views import ModelViewSet from netbox.config import get_config from utilities.api import get_serializer_for_model -from utilities.utils import count_related, decode_dict +from utilities.utils import count_related from virtualization.models import VirtualMachine from . import serializers from .exceptions import MissingFilterException @@ -501,7 +501,7 @@ def napalm(self, request, pk): response[method] = {'error': 'Only get_* NAPALM methods are supported'} continue try: - response[method] = decode_dict(getattr(d, method)()) + response[method] = getattr(d, method)() except NotImplementedError: response[method] = {'error': 'Method {} not implemented for NAPALM driver {}'.format(method, driver)} except Exception as e: diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index a1d996b2c8..002f12916e 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -57,14 +57,12 @@ class DeviceComponentFilterForm(CustomFieldModelFilterForm): region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, - label=_('Region'), - fetch_trigger='open' + label=_('Region') ) site_group_id = DynamicModelMultipleChoiceField( queryset=SiteGroup.objects.all(), required=False, - label=_('Site group'), - fetch_trigger='open' + label=_('Site group') ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), @@ -73,8 +71,7 @@ class DeviceComponentFilterForm(CustomFieldModelFilterForm): 'region_id': '$region_id', 'group_id': '$site_group_id', }, - label=_('Site'), - fetch_trigger='open' + label=_('Site') ) location_id = DynamicModelMultipleChoiceField( queryset=Location.objects.all(), @@ -82,14 +79,12 @@ class DeviceComponentFilterForm(CustomFieldModelFilterForm): query_params={ 'site_id': '$site_id', }, - label=_('Location'), - fetch_trigger='open' + label=_('Location') ) virtual_chassis_id = DynamicModelMultipleChoiceField( queryset=VirtualChassis.objects.all(), required=False, - label=_('Virtual Chassis'), - fetch_trigger='open' + label=_('Virtual Chassis') ) device_id = DynamicModelMultipleChoiceField( queryset=Device.objects.all(), @@ -99,8 +94,7 @@ class DeviceComponentFilterForm(CustomFieldModelFilterForm): 'location_id': '$location_id', 'virtual_chassis_id': '$virtual_chassis_id' }, - label=_('Device'), - fetch_trigger='open' + label=_('Device') ) @@ -109,8 +103,7 @@ class RegionFilterForm(CustomFieldModelFilterForm): parent_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, - label=_('Parent region'), - fetch_trigger='open' + label=_('Parent region') ) tag = TagFilterField(model) @@ -120,8 +113,7 @@ class SiteGroupFilterForm(CustomFieldModelFilterForm): parent_id = DynamicModelMultipleChoiceField( queryset=SiteGroup.objects.all(), required=False, - label=_('Parent group'), - fetch_trigger='open' + label=_('Parent group') ) tag = TagFilterField(model) @@ -142,20 +134,17 @@ class SiteFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, - label=_('Region'), - fetch_trigger='open' + label=_('Region') ) group_id = DynamicModelMultipleChoiceField( queryset=SiteGroup.objects.all(), required=False, - label=_('Site group'), - fetch_trigger='open' + label=_('Site group') ) asn_id = DynamicModelMultipleChoiceField( queryset=ASN.objects.all(), required=False, - label=_('ASNs'), - fetch_trigger='open' + label=_('ASNs') ) tag = TagFilterField(model) @@ -170,14 +159,12 @@ class LocationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, - label=_('Region'), - fetch_trigger='open' + label=_('Region') ) site_group_id = DynamicModelMultipleChoiceField( queryset=SiteGroup.objects.all(), required=False, - label=_('Site group'), - fetch_trigger='open' + label=_('Site group') ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), @@ -186,8 +173,7 @@ class LocationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): 'region_id': '$region_id', 'group_id': '$site_group_id', }, - label=_('Site'), - fetch_trigger='open' + label=_('Site') ) parent_id = DynamicModelMultipleChoiceField( queryset=Location.objects.all(), @@ -196,8 +182,7 @@ class LocationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): 'region_id': '$region_id', 'site_id': '$site_id', }, - label=_('Parent'), - fetch_trigger='open' + label=_('Parent') ) tag = TagFilterField(model) @@ -219,8 +204,7 @@ class RackFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, - label=_('Region'), - fetch_trigger='open' + label=_('Region') ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), @@ -228,8 +212,7 @@ class RackFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): query_params={ 'region_id': '$region_id' }, - label=_('Site'), - fetch_trigger='open' + label=_('Site') ) location_id = DynamicModelMultipleChoiceField( queryset=Location.objects.all(), @@ -238,8 +221,7 @@ class RackFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): query_params={ 'site_id': '$site_id' }, - label=_('Location'), - fetch_trigger='open' + label=_('Location') ) status = forms.MultipleChoiceField( choices=RackStatusChoices, @@ -260,8 +242,7 @@ class RackFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): queryset=RackRole.objects.all(), required=False, null_option='None', - label=_('Role'), - fetch_trigger='open' + label=_('Role') ) serial = forms.CharField( required=False @@ -280,8 +261,7 @@ class RackElevationFilterForm(RackFilterForm): query_params={ 'site_id': '$site_id', 'location_id': '$location_id', - }, - fetch_trigger='open' + } ) @@ -296,8 +276,7 @@ class RackReservationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, - label=_('Region'), - fetch_trigger='open' + label=_('Region') ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), @@ -305,15 +284,13 @@ class RackReservationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): query_params={ 'region_id': '$region_id' }, - label=_('Site'), - fetch_trigger='open' + label=_('Site') ) location_id = DynamicModelMultipleChoiceField( queryset=Location.objects.prefetch_related('site'), required=False, label=_('Location'), - null_option='None', - fetch_trigger='open' + null_option='None' ) user_id = DynamicModelMultipleChoiceField( queryset=User.objects.all(), @@ -321,8 +298,7 @@ class RackReservationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): label=_('User'), widget=APISelectMultiple( api_url='/api/users/users/', - ), - fetch_trigger='open' + ) ) tag = TagFilterField(model) @@ -342,8 +318,7 @@ class DeviceTypeFilterForm(CustomFieldModelFilterForm): manufacturer_id = DynamicModelMultipleChoiceField( queryset=Manufacturer.objects.all(), required=False, - label=_('Manufacturer'), - fetch_trigger='open' + label=_('Manufacturer') ) subdevice_role = forms.MultipleChoiceField( choices=add_blank_choice(SubdeviceRoleChoices), @@ -410,8 +385,7 @@ class PlatformFilterForm(CustomFieldModelFilterForm): manufacturer_id = DynamicModelMultipleChoiceField( queryset=Manufacturer.objects.all(), required=False, - label=_('Manufacturer'), - fetch_trigger='open' + label=_('Manufacturer') ) tag = TagFilterField(model) @@ -432,14 +406,12 @@ class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, CustomFi region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, - label=_('Region'), - fetch_trigger='open' + label=_('Region') ) site_group_id = DynamicModelMultipleChoiceField( queryset=SiteGroup.objects.all(), required=False, - label=_('Site group'), - fetch_trigger='open' + label=_('Site group') ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), @@ -448,8 +420,7 @@ class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, CustomFi 'region_id': '$region_id', 'group_id': '$site_group_id', }, - label=_('Site'), - fetch_trigger='open' + label=_('Site') ) location_id = DynamicModelMultipleChoiceField( queryset=Location.objects.all(), @@ -458,8 +429,7 @@ class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, CustomFi query_params={ 'site_id': '$site_id' }, - label=_('Location'), - fetch_trigger='open' + label=_('Location') ) rack_id = DynamicModelMultipleChoiceField( queryset=Rack.objects.all(), @@ -469,20 +439,17 @@ class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, CustomFi 'site_id': '$site_id', 'location_id': '$location_id', }, - label=_('Rack'), - fetch_trigger='open' + label=_('Rack') ) role_id = DynamicModelMultipleChoiceField( queryset=DeviceRole.objects.all(), required=False, - label=_('Role'), - fetch_trigger='open' + label=_('Role') ) manufacturer_id = DynamicModelMultipleChoiceField( queryset=Manufacturer.objects.all(), required=False, - label=_('Manufacturer'), - fetch_trigger='open' + label=_('Manufacturer') ) device_type_id = DynamicModelMultipleChoiceField( queryset=DeviceType.objects.all(), @@ -490,15 +457,13 @@ class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, CustomFi query_params={ 'manufacturer_id': '$manufacturer_id' }, - label=_('Model'), - fetch_trigger='open' + label=_('Model') ) platform_id = DynamicModelMultipleChoiceField( queryset=Platform.objects.all(), required=False, null_option='None', - label=_('Platform'), - fetch_trigger='open' + label=_('Platform') ) status = forms.MultipleChoiceField( choices=DeviceStatusChoices, @@ -589,14 +554,12 @@ class VirtualChassisFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, - label=_('Region'), - fetch_trigger='open' + label=_('Region') ) site_group_id = DynamicModelMultipleChoiceField( queryset=SiteGroup.objects.all(), required=False, - label=_('Site group'), - fetch_trigger='open' + label=_('Site group') ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), @@ -605,8 +568,7 @@ class VirtualChassisFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): 'region_id': '$region_id', 'group_id': '$site_group_id', }, - label=_('Site'), - fetch_trigger='open' + label=_('Site') ) tag = TagFilterField(model) @@ -622,8 +584,7 @@ class CableFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, - label=_('Region'), - fetch_trigger='open' + label=_('Region') ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), @@ -631,8 +592,7 @@ class CableFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): query_params={ 'region_id': '$region_id' }, - label=_('Site'), - fetch_trigger='open' + label=_('Site') ) rack_id = DynamicModelMultipleChoiceField( queryset=Rack.objects.all(), @@ -641,8 +601,7 @@ class CableFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): null_option='None', query_params={ 'site_id': '$site_id' - }, - fetch_trigger='open' + } ) type = forms.MultipleChoiceField( choices=add_blank_choice(CableTypeChoices), @@ -665,8 +624,7 @@ class CableFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): 'tenant_id': '$tenant_id', 'rack_id': '$rack_id', }, - label=_('Device'), - fetch_trigger='open' + label=_('Device') ) tag = TagFilterField(model) @@ -680,14 +638,12 @@ class PowerPanelFilterForm(CustomFieldModelFilterForm): region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, - label=_('Region'), - fetch_trigger='open' + label=_('Region') ) site_group_id = DynamicModelMultipleChoiceField( queryset=SiteGroup.objects.all(), required=False, - label=_('Site group'), - fetch_trigger='open' + label=_('Site group') ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), @@ -696,8 +652,7 @@ class PowerPanelFilterForm(CustomFieldModelFilterForm): 'region_id': '$region_id', 'group_id': '$site_group_id', }, - label=_('Site'), - fetch_trigger='open' + label=_('Site') ) location_id = DynamicModelMultipleChoiceField( queryset=Location.objects.all(), @@ -706,8 +661,7 @@ class PowerPanelFilterForm(CustomFieldModelFilterForm): query_params={ 'site_id': '$site_id' }, - label=_('Location'), - fetch_trigger='open' + label=_('Location') ) tag = TagFilterField(model) @@ -723,14 +677,12 @@ class PowerFeedFilterForm(CustomFieldModelFilterForm): region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, - label=_('Region'), - fetch_trigger='open' + label=_('Region') ) site_group_id = DynamicModelMultipleChoiceField( queryset=SiteGroup.objects.all(), required=False, - label=_('Site group'), - fetch_trigger='open' + label=_('Site group') ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), @@ -738,8 +690,7 @@ class PowerFeedFilterForm(CustomFieldModelFilterForm): query_params={ 'region_id': '$region_id' }, - label=_('Site'), - fetch_trigger='open' + label=_('Site') ) power_panel_id = DynamicModelMultipleChoiceField( queryset=PowerPanel.objects.all(), @@ -748,8 +699,7 @@ class PowerFeedFilterForm(CustomFieldModelFilterForm): query_params={ 'site_id': '$site_id' }, - label=_('Power panel'), - fetch_trigger='open' + label=_('Power panel') ) rack_id = DynamicModelMultipleChoiceField( queryset=Rack.objects.all(), @@ -758,8 +708,7 @@ class PowerFeedFilterForm(CustomFieldModelFilterForm): query_params={ 'site_id': '$site_id' }, - label=_('Rack'), - fetch_trigger='open' + label=_('Rack') ) status = forms.MultipleChoiceField( choices=PowerFeedStatusChoices, @@ -990,8 +939,7 @@ class InventoryItemFilterForm(DeviceComponentFilterForm): manufacturer_id = DynamicModelMultipleChoiceField( queryset=Manufacturer.objects.all(), required=False, - label=_('Manufacturer'), - fetch_trigger='open' + label=_('Manufacturer') ) serial = forms.CharField( required=False @@ -1016,8 +964,7 @@ class ConsoleConnectionFilterForm(FilterForm): region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, - label=_('Region'), - fetch_trigger='open' + label=_('Region') ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), @@ -1025,8 +972,7 @@ class ConsoleConnectionFilterForm(FilterForm): query_params={ 'region_id': '$region_id' }, - label=_('Site'), - fetch_trigger='open' + label=_('Site') ) device_id = DynamicModelMultipleChoiceField( queryset=Device.objects.all(), @@ -1034,8 +980,7 @@ class ConsoleConnectionFilterForm(FilterForm): query_params={ 'site_id': '$site_id' }, - label=_('Device'), - fetch_trigger='open' + label=_('Device') ) @@ -1043,8 +988,7 @@ class PowerConnectionFilterForm(FilterForm): region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, - label=_('Region'), - fetch_trigger='open' + label=_('Region') ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), @@ -1052,8 +996,7 @@ class PowerConnectionFilterForm(FilterForm): query_params={ 'region_id': '$region_id' }, - label=_('Site'), - fetch_trigger='open' + label=_('Site') ) device_id = DynamicModelMultipleChoiceField( queryset=Device.objects.all(), @@ -1061,8 +1004,7 @@ class PowerConnectionFilterForm(FilterForm): query_params={ 'site_id': '$site_id' }, - label=_('Device'), - fetch_trigger='open' + label=_('Device') ) @@ -1070,8 +1012,7 @@ class InterfaceConnectionFilterForm(FilterForm): region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, - label=_('Region'), - fetch_trigger='open' + label=_('Region') ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), @@ -1079,8 +1020,7 @@ class InterfaceConnectionFilterForm(FilterForm): query_params={ 'region_id': '$region_id' }, - label=_('Site'), - fetch_trigger='open' + label=_('Site') ) device_id = DynamicModelMultipleChoiceField( queryset=Device.objects.all(), @@ -1088,6 +1028,5 @@ class InterfaceConnectionFilterForm(FilterForm): query_params={ 'site_id': '$site_id' }, - label=_('Device'), - fetch_trigger='open' + label=_('Device') ) diff --git a/netbox/dcim/forms/models.py b/netbox/dcim/forms/models.py index db2f58a636..ca9aa6d3a4 100644 --- a/netbox/dcim/forms/models.py +++ b/netbox/dcim/forms/models.py @@ -301,16 +301,14 @@ class RackReservationForm(TenancyForm, CustomFieldModelForm): required=False, initial_params={ 'sites': '$site' - }, - fetch_trigger='open' + } ) site_group = DynamicModelChoiceField( queryset=SiteGroup.objects.all(), required=False, initial_params={ 'sites': '$site' - }, - fetch_trigger='open' + } ) site = DynamicModelChoiceField( queryset=Site.objects.all(), @@ -318,24 +316,21 @@ class RackReservationForm(TenancyForm, CustomFieldModelForm): query_params={ 'region_id': '$region', 'group_id': '$site_group', - }, - fetch_trigger='open' + } ) location = DynamicModelChoiceField( queryset=Location.objects.all(), required=False, query_params={ 'site_id': '$site' - }, - fetch_trigger='open' + } ) rack = DynamicModelChoiceField( queryset=Rack.objects.all(), query_params={ 'site_id': '$site', 'location_id': '$location', - }, - fetch_trigger='open' + } ) units = NumericArrayField( base_field=forms.IntegerField(), @@ -349,8 +344,7 @@ class RackReservationForm(TenancyForm, CustomFieldModelForm): ) tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), - required=False, - fetch_trigger='open' + required=False ) class Meta: diff --git a/netbox/dcim/models/__init__.py b/netbox/dcim/models/__init__.py index 58a3e1de5d..d74f348288 100644 --- a/netbox/dcim/models/__init__.py +++ b/netbox/dcim/models/__init__.py @@ -5,42 +5,3 @@ from .power import * from .racks import * from .sites import * - -__all__ = ( - 'BaseInterface', - 'Cable', - 'CablePath', - 'LinkTermination', - 'ConsolePort', - 'ConsolePortTemplate', - 'ConsoleServerPort', - 'ConsoleServerPortTemplate', - 'Device', - 'DeviceBay', - 'DeviceBayTemplate', - 'DeviceRole', - 'DeviceType', - 'FrontPort', - 'FrontPortTemplate', - 'Interface', - 'InterfaceTemplate', - 'InventoryItem', - 'Location', - 'Manufacturer', - 'Platform', - 'PowerFeed', - 'PowerOutlet', - 'PowerOutletTemplate', - 'PowerPanel', - 'PowerPort', - 'PowerPortTemplate', - 'Rack', - 'RackReservation', - 'RackRole', - 'RearPort', - 'RearPortTemplate', - 'Region', - 'Site', - 'SiteGroup', - 'VirtualChassis', -) diff --git a/netbox/dcim/tables/devicetypes.py b/netbox/dcim/tables/devicetypes.py index f932b7994d..0aa8ac2bf9 100644 --- a/netbox/dcim/tables/devicetypes.py +++ b/netbox/dcim/tables/devicetypes.py @@ -111,8 +111,7 @@ class Meta(BaseTable.Meta): class ConsolePortTemplateTable(ComponentTemplateTable): actions = ButtonsColumn( model=ConsolePortTemplate, - buttons=('edit', 'delete'), - return_url_extra='%23tab_consoleports' + buttons=('edit', 'delete') ) class Meta(ComponentTemplateTable.Meta): @@ -124,8 +123,7 @@ class Meta(ComponentTemplateTable.Meta): class ConsoleServerPortTemplateTable(ComponentTemplateTable): actions = ButtonsColumn( model=ConsoleServerPortTemplate, - buttons=('edit', 'delete'), - return_url_extra='%23tab_consoleserverports' + buttons=('edit', 'delete') ) class Meta(ComponentTemplateTable.Meta): @@ -137,8 +135,7 @@ class Meta(ComponentTemplateTable.Meta): class PowerPortTemplateTable(ComponentTemplateTable): actions = ButtonsColumn( model=PowerPortTemplate, - buttons=('edit', 'delete'), - return_url_extra='%23tab_powerports' + buttons=('edit', 'delete') ) class Meta(ComponentTemplateTable.Meta): @@ -150,8 +147,7 @@ class Meta(ComponentTemplateTable.Meta): class PowerOutletTemplateTable(ComponentTemplateTable): actions = ButtonsColumn( model=PowerOutletTemplate, - buttons=('edit', 'delete'), - return_url_extra='%23tab_poweroutlets' + buttons=('edit', 'delete') ) class Meta(ComponentTemplateTable.Meta): @@ -166,8 +162,7 @@ class InterfaceTemplateTable(ComponentTemplateTable): ) actions = ButtonsColumn( model=InterfaceTemplate, - buttons=('edit', 'delete'), - return_url_extra='%23tab_interfaces' + buttons=('edit', 'delete') ) class Meta(ComponentTemplateTable.Meta): @@ -183,8 +178,7 @@ class FrontPortTemplateTable(ComponentTemplateTable): color = ColorColumn() actions = ButtonsColumn( model=FrontPortTemplate, - buttons=('edit', 'delete'), - return_url_extra='%23tab_frontports' + buttons=('edit', 'delete') ) class Meta(ComponentTemplateTable.Meta): @@ -197,8 +191,7 @@ class RearPortTemplateTable(ComponentTemplateTable): color = ColorColumn() actions = ButtonsColumn( model=RearPortTemplate, - buttons=('edit', 'delete'), - return_url_extra='%23tab_rearports' + buttons=('edit', 'delete') ) class Meta(ComponentTemplateTable.Meta): @@ -210,8 +203,7 @@ class Meta(ComponentTemplateTable.Meta): class DeviceBayTemplateTable(ComponentTemplateTable): actions = ButtonsColumn( model=DeviceBayTemplate, - buttons=('edit', 'delete'), - return_url_extra='%23tab_devicebays' + buttons=('edit', 'delete') ) class Meta(ComponentTemplateTable.Meta): diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 3180d47b10..7048ae63e0 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -27,13 +27,7 @@ from . import filtersets, forms, tables from .choices import DeviceFaceChoices from .constants import NONCONNECTABLE_IFACE_TYPES -from .models import ( - Cable, CablePath, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, - DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, - InventoryItem, Manufacturer, PathEndpoint, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, - PowerPort, PowerPortTemplate, Rack, Location, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, - SiteGroup, VirtualChassis, -) +from .models import * class DeviceComponentsView(generic.ObjectChildrenView): @@ -51,10 +45,21 @@ def get_extra_context(self, request, instance): class DeviceTypeComponentsView(DeviceComponentsView): queryset = DeviceType.objects.all() template_name = 'dcim/devicetype/component_templates.html' + viewname = None # Used for return_url resolution def get_children(self, request, parent): return self.child_model.objects.restrict(request.user, 'view').filter(device_type=parent) + def get_extra_context(self, request, instance): + if self.viewname: + return_url = reverse(self.viewname, kwargs={'pk': instance.pk}) + else: + return_url = instance.get_absolute_url() + return { + 'active_tab': f"{self.child_model._meta.verbose_name_plural.replace(' ', '-')}", + 'return_url': return_url, + } + class BulkDisconnectView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View): """ @@ -798,48 +803,56 @@ class DeviceTypeConsolePortsView(DeviceTypeComponentsView): child_model = ConsolePortTemplate table = tables.ConsolePortTemplateTable filterset = filtersets.ConsolePortTemplateFilterSet + viewname = 'dcim:devicetype_consoleports' class DeviceTypeConsoleServerPortsView(DeviceTypeComponentsView): child_model = ConsoleServerPortTemplate table = tables.ConsoleServerPortTemplateTable filterset = filtersets.ConsoleServerPortTemplateFilterSet + viewname = 'dcim:devicetype_consoleserverports' class DeviceTypePowerPortsView(DeviceTypeComponentsView): child_model = PowerPortTemplate table = tables.PowerPortTemplateTable filterset = filtersets.PowerPortTemplateFilterSet + viewname = 'dcim:devicetype_powerports' class DeviceTypePowerOutletsView(DeviceTypeComponentsView): child_model = PowerOutletTemplate table = tables.PowerOutletTemplateTable filterset = filtersets.PowerOutletTemplateFilterSet + viewname = 'dcim:devicetype_poweroutlets' class DeviceTypeInterfacesView(DeviceTypeComponentsView): child_model = InterfaceTemplate table = tables.InterfaceTemplateTable filterset = filtersets.InterfaceTemplateFilterSet + viewname = 'dcim:devicetype_interfaces' class DeviceTypeFrontPortsView(DeviceTypeComponentsView): child_model = FrontPortTemplate table = tables.FrontPortTemplateTable filterset = filtersets.FrontPortTemplateFilterSet + viewname = 'dcim:devicetype_frontports' class DeviceTypeRearPortsView(DeviceTypeComponentsView): child_model = RearPortTemplate table = tables.RearPortTemplateTable filterset = filtersets.RearPortTemplateFilterSet + viewname = 'dcim:devicetype_rearports' class DeviceTypeDeviceBaysView(DeviceTypeComponentsView): child_model = DeviceBayTemplate table = tables.DeviceBayTemplateTable filterset = filtersets.DeviceBayTemplateFilterSet + viewname = 'dcim:devicetype_devicebays' class DeviceTypeEditView(generic.ObjectEditView): diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 1be1875969..9e4665cc29 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -5,10 +5,10 @@ from rest_framework import serializers from dcim.api.nested_serializers import ( - NestedDeviceSerializer, NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedPlatformSerializer, - NestedRackSerializer, NestedRegionSerializer, NestedSiteSerializer, NestedSiteGroupSerializer, + NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedPlatformSerializer, NestedRegionSerializer, + NestedSiteSerializer, NestedSiteGroupSerializer, ) -from dcim.models import Device, DeviceRole, DeviceType, Platform, Rack, Region, Site, SiteGroup +from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup from extras.choices import * from extras.models import * from extras.utils import FeatureQuery diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py index fb8cf53e81..9f44494e03 100644 --- a/netbox/extras/forms/bulk_import.py +++ b/netbox/extras/forms/bulk_import.py @@ -3,9 +3,10 @@ from django.contrib.postgres.forms import SimpleArrayField from django.utils.safestring import mark_safe +from extras.choices import CustomFieldTypeChoices from extras.models import * from extras.utils import FeatureQuery -from utilities.forms import CSVContentTypeField, CSVModelForm, CSVMultipleContentTypeField, SlugField +from utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelForm, CSVMultipleContentTypeField, SlugField __all__ = ( 'CustomFieldCSVForm', @@ -22,6 +23,10 @@ class CustomFieldCSVForm(CSVModelForm): limit_choices_to=FeatureQuery('custom_fields'), help_text="One or more assigned object types" ) + type = CSVChoiceField( + choices=CustomFieldTypeChoices, + help_text='Field data type (e.g. text, integer, etc.)' + ) choices = SimpleArrayField( base_field=forms.CharField(), required=False, @@ -32,7 +37,7 @@ class Meta: model = CustomField fields = ( 'name', 'label', 'type', 'content_types', 'required', 'description', 'weight', 'filter_logic', 'default', - 'choices', 'weight', + 'choices', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', ) diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index 07375a2035..03cd170b80 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -164,69 +164,58 @@ class ConfigContextFilterForm(FilterForm): region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, - label=_('Regions'), - fetch_trigger='open' + label=_('Regions') ) site_group_id = DynamicModelMultipleChoiceField( queryset=SiteGroup.objects.all(), required=False, - label=_('Site groups'), - fetch_trigger='open' + label=_('Site groups') ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), required=False, - label=_('Sites'), - fetch_trigger='open' + label=_('Sites') ) device_type_id = DynamicModelMultipleChoiceField( queryset=DeviceType.objects.all(), required=False, - label=_('Device types'), - fetch_trigger='open' + label=_('Device types') ) role_id = DynamicModelMultipleChoiceField( queryset=DeviceRole.objects.all(), required=False, - label=_('Roles'), - fetch_trigger='open' + label=_('Roles') ) platform_id = DynamicModelMultipleChoiceField( queryset=Platform.objects.all(), required=False, - label=_('Platforms'), - fetch_trigger='open' + label=_('Platforms') ) cluster_group_id = DynamicModelMultipleChoiceField( queryset=ClusterGroup.objects.all(), required=False, - label=_('Cluster groups'), - fetch_trigger='open' + label=_('Cluster groups') ) cluster_id = DynamicModelMultipleChoiceField( queryset=Cluster.objects.all(), required=False, - label=_('Clusters'), - fetch_trigger='open' + label=_('Clusters') ) tenant_group_id = DynamicModelMultipleChoiceField( queryset=TenantGroup.objects.all(), required=False, - label=_('Tenant groups'), - fetch_trigger='open' + label=_('Tenant groups') ) tenant_id = DynamicModelMultipleChoiceField( queryset=Tenant.objects.all(), required=False, - label=_('Tenant'), - fetch_trigger='open' + label=_('Tenant') ) tag = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), to_field_name='slug', required=False, - label=_('Tags'), - fetch_trigger='open' + label=_('Tags') ) @@ -263,8 +252,7 @@ class JournalEntryFilterForm(FilterForm): label=_('User'), widget=APISelectMultiple( api_url='/api/users/users/', - ), - fetch_trigger='open' + ) ) assigned_object_type_id = DynamicModelMultipleChoiceField( queryset=ContentType.objects.all(), @@ -272,8 +260,7 @@ class JournalEntryFilterForm(FilterForm): label=_('Object Type'), widget=APISelectMultiple( api_url='/api/extras/content-types/', - ), - fetch_trigger='open' + ) ) kind = forms.ChoiceField( choices=add_blank_choice(JournalEntryKindChoices), @@ -310,8 +297,7 @@ class ObjectChangeFilterForm(FilterForm): label=_('User'), widget=APISelectMultiple( api_url='/api/users/users/', - ), - fetch_trigger='open' + ) ) changed_object_type_id = DynamicModelMultipleChoiceField( queryset=ContentType.objects.all(), @@ -319,6 +305,5 @@ class ObjectChangeFilterForm(FilterForm): label=_('Object Type'), widget=APISelectMultiple( api_url='/api/extras/content-types/', - ), - fetch_trigger='open' + ) ) diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 47da21e197..36457efae6 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -229,6 +229,24 @@ def __str__(self): def get_absolute_url(self): return reverse('extras:customlink', args=[self.pk]) + def render(self, context): + """ + Render the CustomLink given the provided context, and return the text, link, and link_target. + + :param context: The context passed to Jinja2 + """ + text = render_jinja2(self.link_text, context) + if not text: + return {} + link = render_jinja2(self.link_url, context) + link_target = ' target="_blank"' if self.new_window else '' + + return { + 'text': text, + 'link': link, + 'link_target': link_target, + } + @extras_features('webhooks', 'export_templates') class ExportTemplate(ChangeLoggedModel): diff --git a/netbox/extras/templatetags/custom_links.py b/netbox/extras/templatetags/custom_links.py index fec5cf65ac..32ec966b35 100644 --- a/netbox/extras/templatetags/custom_links.py +++ b/netbox/extras/templatetags/custom_links.py @@ -62,16 +62,14 @@ def custom_links(context, obj): # Add non-grouped links else: try: - text_rendered = render_jinja2(cl.link_text, link_context) - if text_rendered: - link_rendered = render_jinja2(cl.link_url, link_context) - link_target = ' target="_blank"' if cl.new_window else '' + rendered = cl.render(link_context) + if rendered: template_code += LINK_BUTTON.format( - link_rendered, link_target, cl.button_class, text_rendered + rendered['link'], rendered['link_target'], cl.button_class, rendered['text'] ) except Exception as e: - template_code += '' \ - ' {}\n'.format(e, cl.name) + template_code += f'' \ + f' {cl.name}\n' # Add grouped links to template for group, links in group_names.items(): @@ -80,17 +78,15 @@ def custom_links(context, obj): for cl in links: try: - text_rendered = render_jinja2(cl.link_text, link_context) - if text_rendered: - link_target = ' target="_blank"' if cl.new_window else '' - link_rendered = render_jinja2(cl.link_url, link_context) + rendered = cl.render(link_context) + if rendered: links_rendered.append( - GROUP_LINK.format(link_rendered, link_target, text_rendered) + GROUP_LINK.format(rendered['link'], rendered['link_target'], rendered['text']) ) except Exception as e: links_rendered.append( - '
  • ' - ' {}
  • '.format(e, cl.name) + f'
  • ' + f' {cl.name}
  • ' ) if links_rendered: diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py index 9ce324a5c0..67abcf5430 100644 --- a/netbox/extras/tests/test_views.py +++ b/netbox/extras/tests/test_views.py @@ -39,10 +39,10 @@ def setUpTestData(cls): } cls.csv_data = ( - 'name,label,type,content_types,weight,filter_logic,choices', - 'field4,Field 4,text,dcim.site,100,exact,', - 'field5,Field 5,integer,dcim.site,100,exact,', - 'field6,Field 6,select,dcim.site,100,exact,"A,B,C"', + 'name,label,type,content_types,weight,filter_logic,choices,validation_minimum,validation_maximum,validation_regex', + 'field4,Field 4,text,dcim.site,100,exact,,,,[a-z]{3}', + 'field5,Field 5,integer,dcim.site,100,exact,,1,100,', + 'field6,Field 6,select,dcim.site,100,exact,"A,B,C",,,', ) cls.bulk_edit_data = { diff --git a/netbox/extras/views.py b/netbox/extras/views.py index ab9e3ba521..15f3ca48a1 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -10,6 +10,7 @@ from netbox.views import generic from utilities.forms import ConfirmationForm +from utilities.htmx import is_htmx from utilities.tables import paginate_table from utilities.utils import copy_safe_request, count_related, normalize_querydict, shallow_compare_dict from utilities.views import ContentTypePermissionRequiredMixin @@ -471,6 +472,7 @@ def get(self, request, model, **kwargs): class ImageAttachmentEditView(generic.ObjectEditView): queryset = ImageAttachment.objects.all() model_form = forms.ImageAttachmentForm + template_name = 'extras/imageattachment_edit.html' def alter_obj(self, instance, request, args, kwargs): if not instance.pk: @@ -693,16 +695,26 @@ def get_required_permission(self): def get(self, request, job_result_pk): report_content_type = ContentType.objects.get(app_label='extras', model='report') - jobresult = get_object_or_404(JobResult.objects.all(), pk=job_result_pk, obj_type=report_content_type) + result = get_object_or_404(JobResult.objects.all(), pk=job_result_pk, obj_type=report_content_type) # Retrieve the Report and attach the JobResult to it - module, report_name = jobresult.name.split('.') + module, report_name = result.name.split('.') report = get_report(module, report_name) - report.result = jobresult + report.result = result + + # If this is an HTMX request, return only the result HTML + if is_htmx(request): + response = render(request, 'extras/htmx/report_result.html', { + 'report': report, + 'result': result, + }) + if result.completed: + response.status_code = 286 + return response return render(request, 'extras/report_result.html', { 'report': report, - 'result': jobresult, + 'result': result, }) @@ -820,6 +832,16 @@ def get(self, request, job_result_pk): script = self._get_script(result.name) + # If this is an HTMX request, return only the result HTML + if is_htmx(request): + response = render(request, 'extras/htmx/script_result.html', { + 'script': script, + 'result': result, + }) + if result.completed: + response.status_code = 286 + return response + return render(request, 'extras/script_result.html', { 'script': script, 'result': result, diff --git a/netbox/ipam/choices.py b/netbox/ipam/choices.py index 638ef62f6f..526ef07d9b 100644 --- a/netbox/ipam/choices.py +++ b/netbox/ipam/choices.py @@ -135,6 +135,7 @@ class FHRPGroupProtocolChoices(ChoiceSet): PROTOCOL_HSRP = 'hsrp' PROTOCOL_GLBP = 'glbp' PROTOCOL_CARP = 'carp' + PROTOCOL_OTHER = 'other' CHOICES = ( (PROTOCOL_VRRP2, 'VRRPv2'), @@ -142,6 +143,7 @@ class FHRPGroupProtocolChoices(ChoiceSet): (PROTOCOL_HSRP, 'HSRP'), (PROTOCOL_GLBP, 'GLBP'), (PROTOCOL_CARP, 'CARP'), + (PROTOCOL_OTHER, 'Other'), ) diff --git a/netbox/ipam/forms/filtersets.py b/netbox/ipam/forms/filtersets.py index b21dbd6cda..d0f4c23c90 100644 --- a/netbox/ipam/forms/filtersets.py +++ b/netbox/ipam/forms/filtersets.py @@ -48,14 +48,12 @@ class VRFFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): import_target_id = DynamicModelMultipleChoiceField( queryset=RouteTarget.objects.all(), required=False, - label=_('Import targets'), - fetch_trigger='open' + label=_('Import targets') ) export_target_id = DynamicModelMultipleChoiceField( queryset=RouteTarget.objects.all(), required=False, - label=_('Export targets'), - fetch_trigger='open' + label=_('Export targets') ) tag = TagFilterField(model) @@ -70,14 +68,12 @@ class RouteTargetFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): importing_vrf_id = DynamicModelMultipleChoiceField( queryset=VRF.objects.all(), required=False, - label=_('Imported by VRF'), - fetch_trigger='open' + label=_('Imported by VRF') ) exporting_vrf_id = DynamicModelMultipleChoiceField( queryset=VRF.objects.all(), required=False, - label=_('Exported by VRF'), - fetch_trigger='open' + label=_('Exported by VRF') ) tag = TagFilterField(model) @@ -110,8 +106,7 @@ class AggregateFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): rir_id = DynamicModelMultipleChoiceField( queryset=RIR.objects.all(), required=False, - label=_('RIR'), - fetch_trigger='open' + label=_('RIR') ) tag = TagFilterField(model) @@ -127,14 +122,12 @@ class ASNFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): rir_id = DynamicModelMultipleChoiceField( queryset=RIR.objects.all(), required=False, - label=_('RIR'), - fetch_trigger='open' + label=_('RIR') ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), required=False, - label=_('Site'), - fetch_trigger='open' + label=_('Site') ) @@ -180,14 +173,12 @@ class PrefixFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): queryset=VRF.objects.all(), required=False, label=_('Assigned VRF'), - null_option='Global', - fetch_trigger='open' + null_option='Global' ) present_in_vrf_id = DynamicModelChoiceField( queryset=VRF.objects.all(), required=False, - label=_('Present in VRF'), - fetch_trigger='open' + label=_('Present in VRF') ) status = forms.MultipleChoiceField( choices=PrefixStatusChoices, @@ -197,14 +188,12 @@ class PrefixFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, - label=_('Region'), - fetch_trigger='open' + label=_('Region') ) site_group_id = DynamicModelMultipleChoiceField( queryset=SiteGroup.objects.all(), required=False, - label=_('Site group'), - fetch_trigger='open' + label=_('Site group') ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), @@ -213,15 +202,13 @@ class PrefixFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): query_params={ 'region_id': '$region_id' }, - label=_('Site'), - fetch_trigger='open' + label=_('Site') ) role_id = DynamicModelMultipleChoiceField( queryset=Role.objects.all(), required=False, null_option='None', - label=_('Role'), - fetch_trigger='open' + label=_('Role') ) is_pool = forms.NullBooleanField( required=False, @@ -257,8 +244,7 @@ class IPRangeFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): queryset=VRF.objects.all(), required=False, label=_('Assigned VRF'), - null_option='Global', - fetch_trigger='open' + null_option='Global' ) status = forms.MultipleChoiceField( choices=PrefixStatusChoices, @@ -269,8 +255,7 @@ class IPRangeFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): queryset=Role.objects.all(), required=False, null_option='None', - label=_('Role'), - fetch_trigger='open' + label=_('Role') ) tag = TagFilterField(model) @@ -308,14 +293,12 @@ class IPAddressFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): queryset=VRF.objects.all(), required=False, label=_('Assigned VRF'), - null_option='Global', - fetch_trigger='open' + null_option='Global' ) present_in_vrf_id = DynamicModelChoiceField( queryset=VRF.objects.all(), required=False, - label=_('Present in VRF'), - fetch_trigger='open' + label=_('Present in VRF') ) status = forms.MultipleChoiceField( choices=IPAddressStatusChoices, @@ -376,32 +359,27 @@ class VLANGroupFilterForm(CustomFieldModelFilterForm): region = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, - label=_('Region'), - fetch_trigger='open' + label=_('Region') ) sitegroup = DynamicModelMultipleChoiceField( queryset=SiteGroup.objects.all(), required=False, - label=_('Site group'), - fetch_trigger='open' + label=_('Site group') ) site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), required=False, - label=_('Site'), - fetch_trigger='open' + label=_('Site') ) location = DynamicModelMultipleChoiceField( queryset=Location.objects.all(), required=False, - label=_('Location'), - fetch_trigger='open' + label=_('Location') ) rack = DynamicModelMultipleChoiceField( queryset=Rack.objects.all(), required=False, - label=_('Rack'), - fetch_trigger='open' + label=_('Rack') ) tag = TagFilterField(model) @@ -417,14 +395,12 @@ class VLANFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, - label=_('Region'), - fetch_trigger='open' + label=_('Region') ) site_group_id = DynamicModelMultipleChoiceField( queryset=SiteGroup.objects.all(), required=False, - label=_('Site group'), - fetch_trigger='open' + label=_('Site group') ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), @@ -433,8 +409,7 @@ class VLANFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): query_params={ 'region': '$region' }, - label=_('Site'), - fetch_trigger='open' + label=_('Site') ) group_id = DynamicModelMultipleChoiceField( queryset=VLANGroup.objects.all(), @@ -443,8 +418,7 @@ class VLANFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): query_params={ 'region': '$region' }, - label=_('VLAN group'), - fetch_trigger='open' + label=_('VLAN group') ) status = forms.MultipleChoiceField( choices=VLANStatusChoices, @@ -455,8 +429,7 @@ class VLANFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): queryset=Role.objects.all(), required=False, null_option='None', - label=_('Role'), - fetch_trigger='open' + label=_('Role') ) vid = forms.IntegerField( required=False, diff --git a/netbox/ipam/forms/models.py b/netbox/ipam/forms/models.py index 319d8671ea..c5e3146e9f 100644 --- a/netbox/ipam/forms/models.py +++ b/netbox/ipam/forms/models.py @@ -471,6 +471,8 @@ def clean(self): }) elif selected_objects: self.instance.assigned_object = self.cleaned_data[selected_objects[0]] + else: + self.instance.assigned_object = None # Primary IP assignment is only available if an interface has been assigned. interface = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface') diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index cff845a7ae..317caeaf2e 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -5,18 +5,18 @@ from django.urls import reverse from dcim.filtersets import InterfaceFilterSet -from dcim.models import Device, Interface, Site +from dcim.models import Interface, Site from dcim.tables import SiteTable from netbox.views import generic from utilities.tables import paginate_table from utilities.utils import count_related from virtualization.filtersets import VMInterfaceFilterSet -from virtualization.models import VirtualMachine, VMInterface +from virtualization.models import VMInterface from . import filtersets, forms, tables from .constants import * from .models import * from .models import ASN -from .utils import add_requested_prefixes, add_available_vlans +from .utils import add_requested_prefixes, add_available_ipaddresses, add_available_vlans # @@ -418,7 +418,7 @@ def get_extra_context(self, request, instance): ).filter( prefix__net_contains=str(instance.prefix) ).prefetch_related( - 'site', 'role' + 'site', 'role', 'tenant' ) parent_prefix_table = tables.PrefixTable( list(parent_prefixes), @@ -502,6 +502,13 @@ class PrefixIPAddressesView(generic.ObjectChildrenView): def get_children(self, request, parent): return parent.get_child_ips().restrict(request.user, 'view') + def prep_table_data(self, request, queryset, parent): + show_available = bool(request.GET.get('show_available', 'true') == 'true') + if show_available: + return add_available_ipaddresses(parent.prefix, queryset, parent.is_pool) + + return queryset + def get_extra_context(self, request, instance): return { 'bulk_querystring': f"vrf_id={instance.vrf.pk if instance.vrf else '0'}&parent={instance.prefix}", diff --git a/netbox/netbox/authentication.py b/netbox/netbox/authentication.py index a67ec451d1..acb04ce349 100644 --- a/netbox/netbox/authentication.py +++ b/netbox/netbox/authentication.py @@ -105,7 +105,7 @@ def create_unknown_user(self): return settings.REMOTE_AUTH_AUTO_CREATE_USER def configure_groups(self, user, remote_groups): - logger = logging.getLogger('netbox.authentication.RemoteUserBackend') + logger = logging.getLogger('netbox.auth.RemoteUserBackend') # Assign default groups to the user group_list = [] @@ -141,7 +141,7 @@ def authenticate(self, request, remote_user, remote_groups=None): Return None if ``create_unknown_user`` is ``False`` and a ``User`` object with the given username is not found in the database. """ - logger = logging.getLogger('netbox.authentication.RemoteUserBackend') + logger = logging.getLogger('netbox.auth.RemoteUserBackend') logger.debug( f"trying to authenticate {remote_user} with groups {remote_groups}") if not remote_user: @@ -173,7 +173,7 @@ def authenticate(self, request, remote_user, remote_groups=None): return None def _is_superuser(self, user): - logger = logging.getLogger('netbox.authentication.RemoteUserBackend') + logger = logging.getLogger('netbox.auth.RemoteUserBackend') superuser_groups = settings.REMOTE_AUTH_SUPERUSER_GROUPS logger.debug(f"Superuser Groups: {superuser_groups}") superusers = settings.REMOTE_AUTH_SUPERUSERS @@ -189,7 +189,7 @@ def _is_superuser(self, user): return bool(result) def _is_staff(self, user): - logger = logging.getLogger('netbox.authentication.RemoteUserBackend') + logger = logging.getLogger('netbox.auth.RemoteUserBackend') staff_groups = settings.REMOTE_AUTH_STAFF_GROUPS logger.debug(f"Superuser Groups: {staff_groups}") staff_users = settings.REMOTE_AUTH_STAFF_USERS @@ -204,7 +204,7 @@ def _is_staff(self, user): return bool(result) def configure_user(self, request, user): - logger = logging.getLogger('netbox.authentication.RemoteUserBackend') + logger = logging.getLogger('netbox.auth.RemoteUserBackend') if not settings.REMOTE_AUTH_GROUP_SYNC_ENABLED: # Assign default groups to the user group_list = [] diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index af4ce0a4d0..9c2fb0174d 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -19,7 +19,7 @@ # Environment setup # -VERSION = '3.1.2' +VERSION = '3.1.3' # Hostname HOSTNAME = platform.node() diff --git a/netbox/project-static/bundle.js b/netbox/project-static/bundle.js index 100b70ac88..76a1581ad2 100644 --- a/netbox/project-static/bundle.js +++ b/netbox/project-static/bundle.js @@ -40,7 +40,6 @@ async function bundleGraphIQL() { async function bundleNetBox() { const entryPoints = { netbox: 'src/index.ts', - jobs: 'src/jobs.ts', lldp: 'src/device/lldp.ts', config: 'src/device/config.ts', status: 'src/device/status.ts', diff --git a/netbox/project-static/dist/jobs.js b/netbox/project-static/dist/jobs.js deleted file mode 100644 index 2aedf12194..0000000000 --- a/netbox/project-static/dist/jobs.js +++ /dev/null @@ -1,30 +0,0 @@ -(()=>{var mr=Object.create;var le=Object.defineProperty,gr=Object.defineProperties,_r=Object.getOwnPropertyDescriptor,Er=Object.getOwnPropertyDescriptors,vr=Object.getOwnPropertyNames,pn=Object.getOwnPropertySymbols,yr=Object.getPrototypeOf,hn=Object.prototype.hasOwnProperty,br=Object.prototype.propertyIsEnumerable;var mn=(i,t,e)=>t in i?le(i,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):i[t]=e,O=(i,t)=>{for(var e in t||(t={}))hn.call(t,e)&&mn(i,e,t[e]);if(pn)for(var e of pn(t))br.call(t,e)&&mn(i,e,t[e]);return i},je=(i,t)=>gr(i,Er(t)),gn=i=>le(i,"__esModule",{value:!0});var mt=(i,t)=>()=>(t||i((t={exports:{}}).exports,t),t.exports),Tr=(i,t)=>{gn(i);for(var e in t)le(i,e,{get:t[e],enumerable:!0})},Ar=(i,t,e)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of vr(t))!hn.call(i,n)&&n!=="default"&&le(i,n,{get:()=>t[n],enumerable:!(e=_r(t,n))||e.enumerable});return i},_n=i=>Ar(gn(le(i!=null?mr(yr(i)):{},"default",i&&i.__esModule&&"default"in i?{get:()=>i.default,enumerable:!0}:{value:i,enumerable:!0})),i);var ce=(i,t,e)=>new Promise((n,o)=>{var r=u=>{try{l(e.next(u))}catch(h){o(h)}},s=u=>{try{l(e.throw(u))}catch(h){o(h)}},l=u=>u.done?n(u.value):Promise.resolve(u.value).then(r,s);l((e=e.apply(i,t)).next())});var en=mt((Zo,li)=>{(function(i,t){typeof define=="function"&&define.amd?define(t):typeof li=="object"&&li.exports?li.exports=t():i.EvEmitter=t()})(typeof window!="undefined"?window:Zo,function(){"use strict";function i(){}var t=i.prototype;return t.on=function(e,n){if(!(!e||!n)){var o=this._events=this._events||{},r=o[e]=o[e]||[];return r.indexOf(n)==-1&&r.push(n),this}},t.once=function(e,n){if(!(!e||!n)){this.on(e,n);var o=this._onceEvents=this._onceEvents||{},r=o[e]=o[e]||{};return r[n]=!0,this}},t.off=function(e,n){var o=this._events&&this._events[e];if(!(!o||!o.length)){var r=o.indexOf(n);return r!=-1&&o.splice(r,1),this}},t.emitEvent=function(e,n){var o=this._events&&this._events[e];if(!(!o||!o.length)){o=o.slice(0),n=n||[];for(var r=this._onceEvents&&this._onceEvents[e],s=0;s{(function(i,t){typeof define=="function"&&define.amd?define(t):typeof ci=="object"&&ci.exports?ci.exports=t():i.getSize=t()})(window,function(){"use strict";function t(d){var y=parseFloat(d),E=d.indexOf("%")==-1&&!isNaN(y);return E&&y}function e(){}var n=typeof console=="undefined"?e:function(d){console.error(d)},o=["paddingLeft","paddingRight","paddingTop","paddingBottom","marginLeft","marginRight","marginTop","marginBottom","borderLeftWidth","borderRightWidth","borderTopWidth","borderBottomWidth"],r=o.length;function s(){for(var d={width:0,height:0,innerWidth:0,innerHeight:0,outerWidth:0,outerHeight:0},y=0;y