mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-26 01:06:11 -06:00
Merge branch 'netbox-community:develop' into if-list-fix
This commit is contained in:
commit
6dd4b5caa1
@ -52,10 +52,10 @@ as the cornerstone for network automation in thousands of organizations.
|
|||||||
## Project Stats
|
## Project Stats
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://github.com/netbox-community/netbox/commits"><img src="https://images.repography.com/29023055/netbox-community/netbox/recent-activity/31db894eee74b8a5475e3af307a81b6c_timeline.svg" alt="Timeline graph"></a>
|
<a href="https://github.com/netbox-community/netbox/commits"><img src="https://images.repography.com/29023055/netbox-community/netbox/recent-activity/whQtEr_TGD9PhW1BPlhlEQ5jnrgQ0KJpm-LlGtpoGO0/3Kx_iWUSBRJ5-AI4QwJEJWrUDEz3KrX2lvh8aYE0WXY_timeline.svg" alt="Timeline graph"></a>
|
||||||
<a href="https://github.com/netbox-community/netbox/issues"><img src="https://images.repography.com/29023055/netbox-community/netbox/recent-activity/31db894eee74b8a5475e3af307a81b6c_issues.svg" alt="Issues graph"></a>
|
<a href="https://github.com/netbox-community/netbox/issues"><img src="https://images.repography.com/29023055/netbox-community/netbox/recent-activity/whQtEr_TGD9PhW1BPlhlEQ5jnrgQ0KJpm-LlGtpoGO0/3Kx_iWUSBRJ5-AI4QwJEJWrUDEz3KrX2lvh8aYE0WXY_issues.svg" alt="Issues graph"></a>
|
||||||
<a href="https://github.com/netbox-community/netbox/pulls"><img src="https://images.repography.com/29023055/netbox-community/netbox/recent-activity/31db894eee74b8a5475e3af307a81b6c_prs.svg" alt="Pull requests graph"></a>
|
<a href="https://github.com/netbox-community/netbox/pulls"><img src="https://images.repography.com/29023055/netbox-community/netbox/recent-activity/whQtEr_TGD9PhW1BPlhlEQ5jnrgQ0KJpm-LlGtpoGO0/3Kx_iWUSBRJ5-AI4QwJEJWrUDEz3KrX2lvh8aYE0WXY_prs.svg" alt="Pull requests graph"></a>
|
||||||
<a href="https://github.com/netbox-community/netbox/graphs/contributors"><img src="https://images.repography.com/29023055/netbox-community/netbox/recent-activity/31db894eee74b8a5475e3af307a81b6c_users.svg" alt="Top contributors"></a>
|
<a href="https://github.com/netbox-community/netbox/graphs/contributors"><img src="https://images.repography.com/29023055/netbox-community/netbox/recent-activity/whQtEr_TGD9PhW1BPlhlEQ5jnrgQ0KJpm-LlGtpoGO0/3Kx_iWUSBRJ5-AI4QwJEJWrUDEz3KrX2lvh8aYE0WXY_users.svg" alt="Top contributors"></a>
|
||||||
<br />Stats via <a href="https://repography.com">Repography</a>
|
<br />Stats via <a href="https://repography.com">Repography</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,5 +1,27 @@
|
|||||||
# NetBox v3.5
|
# NetBox v3.5
|
||||||
|
|
||||||
|
## v3.5.5 (FUTURE)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#12945](https://github.com/netbox-community/netbox/issues/12945) - Add 100GE QSFP-DD interface type
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#12533](https://github.com/netbox-community/netbox/issues/12533) - Fix REST API validation of null values for several interface attributes
|
||||||
|
* [#12849](https://github.com/netbox-community/netbox/issues/12849) - Fix LDAP group permissions assignment for API clients
|
||||||
|
* [#12953](https://github.com/netbox-community/netbox/issues/12953) - Fix designation of primary IP addresses during interface assignment
|
||||||
|
* [#12960](https://github.com/netbox-community/netbox/issues/12960) - Fix OpenAPI schema for various choice fields
|
||||||
|
* [#12961](https://github.com/netbox-community/netbox/issues/12961) - Set correct return URL for object contacts tabs
|
||||||
|
* [#12966](https://github.com/netbox-community/netbox/issues/12966) - Avoid catching database exceptions when maintenance mode is disabled
|
||||||
|
* [#12975](https://github.com/netbox-community/netbox/issues/12975) - Correct URL for VirtualDeviceContext API serializer
|
||||||
|
* [#12977](https://github.com/netbox-community/netbox/issues/12977) - Fix URL parameters for object count dashboard widgets
|
||||||
|
* [#12983](https://github.com/netbox-community/netbox/issues/12983) - Avoid erroneously clearing many-to-many assignments during bulk edit
|
||||||
|
* [#12989](https://github.com/netbox-community/netbox/issues/12989) - Fix bulk import of tags for device & module types
|
||||||
|
* [#13011](https://github.com/netbox-community/netbox/issues/13011) - Do not escape commas when rendering custom links
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v3.5.4 (2023-06-20)
|
## v3.5.4 (2023-06-20)
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import re
|
import re
|
||||||
import typing
|
import typing
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
from drf_spectacular.extensions import OpenApiSerializerFieldExtension
|
from drf_spectacular.extensions import OpenApiSerializerFieldExtension
|
||||||
from drf_spectacular.openapi import AutoSchema
|
from drf_spectacular.openapi import AutoSchema
|
||||||
@ -28,14 +29,19 @@ class ChoiceFieldFix(OpenApiSerializerFieldExtension):
|
|||||||
target_class = 'netbox.api.fields.ChoiceField'
|
target_class = 'netbox.api.fields.ChoiceField'
|
||||||
|
|
||||||
def map_serializer_field(self, auto_schema, direction):
|
def map_serializer_field(self, auto_schema, direction):
|
||||||
|
build_cf = build_choice_field(self.target)
|
||||||
|
|
||||||
if direction == 'request':
|
if direction == 'request':
|
||||||
return build_choice_field(self.target)
|
return build_cf
|
||||||
|
|
||||||
elif direction == "response":
|
elif direction == "response":
|
||||||
|
value = build_cf
|
||||||
|
label = {**build_basic_type(OpenApiTypes.STR), "enum": list(OrderedDict.fromkeys(self.target.choices.values()))}
|
||||||
|
|
||||||
return build_object_type(
|
return build_object_type(
|
||||||
properties={
|
properties={
|
||||||
"value": build_basic_type(OpenApiTypes.STR),
|
"value": value,
|
||||||
"label": build_basic_type(OpenApiTypes.STR),
|
"label": label
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -707,7 +707,7 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class VirtualDeviceContextSerializer(NetBoxModelSerializer):
|
class VirtualDeviceContextSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualdevicecontext-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
tenant = NestedTenantSerializer(required=False, allow_null=True, default=None)
|
tenant = NestedTenantSerializer(required=False, allow_null=True, default=None)
|
||||||
primary_ip = NestedIPAddressSerializer(read_only=True, allow_null=True)
|
primary_ip = NestedIPAddressSerializer(read_only=True, allow_null=True)
|
||||||
@ -880,12 +880,12 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
|
|||||||
parent = NestedInterfaceSerializer(required=False, allow_null=True)
|
parent = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||||
bridge = NestedInterfaceSerializer(required=False, allow_null=True)
|
bridge = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||||
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||||
mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_blank=True, allow_null=True)
|
mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_blank=True)
|
||||||
duplex = ChoiceField(choices=InterfaceDuplexChoices, required=False, allow_blank=True, allow_null=True)
|
duplex = ChoiceField(choices=InterfaceDuplexChoices, required=False, allow_blank=True, allow_null=True)
|
||||||
rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_blank=True, allow_null=True)
|
rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_blank=True)
|
||||||
rf_channel = ChoiceField(choices=WirelessChannelChoices, required=False, allow_blank=True, allow_null=True)
|
rf_channel = ChoiceField(choices=WirelessChannelChoices, required=False, allow_blank=True)
|
||||||
poe_mode = ChoiceField(choices=InterfacePoEModeChoices, required=False, allow_blank=True, allow_null=True)
|
poe_mode = ChoiceField(choices=InterfacePoEModeChoices, required=False, allow_blank=True)
|
||||||
poe_type = ChoiceField(choices=InterfacePoETypeChoices, required=False, allow_blank=True, allow_null=True)
|
poe_type = ChoiceField(choices=InterfacePoETypeChoices, required=False, allow_blank=True)
|
||||||
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
|
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
|
||||||
tagged_vlans = SerializedPKRelatedField(
|
tagged_vlans = SerializedPKRelatedField(
|
||||||
queryset=VLAN.objects.all(),
|
queryset=VLAN.objects.all(),
|
||||||
@ -907,9 +907,10 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
|
|||||||
mac_address = serializers.CharField(
|
mac_address = serializers.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
default=None,
|
default=None,
|
||||||
|
allow_blank=True,
|
||||||
allow_null=True
|
allow_null=True
|
||||||
)
|
)
|
||||||
wwn = serializers.CharField(required=False, default=None)
|
wwn = serializers.CharField(required=False, default=None, allow_blank=True, allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
|
@ -810,6 +810,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
|||||||
TYPE_100GE_CXP = '100gbase-x-cxp'
|
TYPE_100GE_CXP = '100gbase-x-cxp'
|
||||||
TYPE_100GE_CPAK = '100gbase-x-cpak'
|
TYPE_100GE_CPAK = '100gbase-x-cpak'
|
||||||
TYPE_100GE_QSFP28 = '100gbase-x-qsfp28'
|
TYPE_100GE_QSFP28 = '100gbase-x-qsfp28'
|
||||||
|
TYPE_100GE_QSFP_DD = '100gbase-x-qsfpdd'
|
||||||
TYPE_200GE_CFP2 = '200gbase-x-cfp2'
|
TYPE_200GE_CFP2 = '200gbase-x-cfp2'
|
||||||
TYPE_200GE_QSFP56 = '200gbase-x-qsfp56'
|
TYPE_200GE_QSFP56 = '200gbase-x-qsfp56'
|
||||||
TYPE_200GE_QSFP_DD = '200gbase-x-qsfpdd'
|
TYPE_200GE_QSFP_DD = '200gbase-x-qsfpdd'
|
||||||
@ -959,6 +960,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
|||||||
(TYPE_100GE_CXP, 'CXP (100GE)'),
|
(TYPE_100GE_CXP, 'CXP (100GE)'),
|
||||||
(TYPE_100GE_CPAK, 'Cisco CPAK (100GE)'),
|
(TYPE_100GE_CPAK, 'Cisco CPAK (100GE)'),
|
||||||
(TYPE_100GE_QSFP28, 'QSFP28 (100GE)'),
|
(TYPE_100GE_QSFP28, 'QSFP28 (100GE)'),
|
||||||
|
(TYPE_100GE_QSFP_DD, 'QSFP-DD (100GE)'),
|
||||||
(TYPE_200GE_QSFP56, 'QSFP56 (200GE)'),
|
(TYPE_200GE_QSFP56, 'QSFP56 (200GE)'),
|
||||||
(TYPE_200GE_QSFP_DD, 'QSFP-DD (200GE)'),
|
(TYPE_200GE_QSFP_DD, 'QSFP-DD (200GE)'),
|
||||||
(TYPE_400GE_QSFP_DD, 'QSFP-DD (400GE)'),
|
(TYPE_400GE_QSFP_DD, 'QSFP-DD (400GE)'),
|
||||||
|
@ -306,7 +306,7 @@ class DeviceTypeImportForm(NetBoxModelImportForm):
|
|||||||
model = DeviceType
|
model = DeviceType
|
||||||
fields = [
|
fields = [
|
||||||
'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
|
'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
|
||||||
'subdevice_role', 'airflow', 'description', 'weight', 'weight_unit', 'comments',
|
'subdevice_role', 'airflow', 'description', 'weight', 'weight_unit', 'comments', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -327,7 +327,7 @@ class ModuleTypeImportForm(NetBoxModelImportForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ModuleType
|
model = ModuleType
|
||||||
fields = ['manufacturer', 'model', 'part_number', 'description', 'weight', 'weight_unit', 'comments']
|
fields = ['manufacturer', 'model', 'part_number', 'description', 'weight', 'weight_unit', 'comments', 'tags']
|
||||||
|
|
||||||
|
|
||||||
class DeviceRoleImportForm(NetBoxModelImportForm):
|
class DeviceRoleImportForm(NetBoxModelImportForm):
|
||||||
|
@ -10,7 +10,6 @@ from django.conf import settings
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.http import QueryDict
|
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.urls import NoReverseMatch, resolve, reverse
|
from django.urls import NoReverseMatch, resolve, reverse
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
@ -19,7 +18,7 @@ from extras.utils import FeatureQuery
|
|||||||
from utilities.forms import BootstrapMixin
|
from utilities.forms import BootstrapMixin
|
||||||
from utilities.permissions import get_permission_for_model
|
from utilities.permissions import get_permission_for_model
|
||||||
from utilities.templatetags.builtins.filters import render_markdown
|
from utilities.templatetags.builtins.filters import render_markdown
|
||||||
from utilities.utils import content_type_identifier, content_type_name, get_viewname
|
from utilities.utils import content_type_identifier, content_type_name, dict_to_querydict, get_viewname
|
||||||
from .utils import register_widget
|
from .utils import register_widget
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -170,8 +169,7 @@ class ObjectCountsWidget(DashboardWidget):
|
|||||||
qs = model.objects.restrict(request.user, 'view')
|
qs = model.objects.restrict(request.user, 'view')
|
||||||
# Apply any specified filters
|
# Apply any specified filters
|
||||||
if filters := self.config.get('filters'):
|
if filters := self.config.get('filters'):
|
||||||
params = QueryDict(mutable=True)
|
params = dict_to_querydict(filters)
|
||||||
params.update(filters)
|
|
||||||
filterset = getattr(resolve(url).func.view_class, 'filterset', None)
|
filterset = getattr(resolve(url).func.view_class, 'filterset', None)
|
||||||
qs = filterset(params, qs).qs
|
qs = filterset(params, qs).qs
|
||||||
url = f'{url}?{params.urlencode()}'
|
url = f'{url}?{params.urlencode()}'
|
||||||
|
@ -9,7 +9,7 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.core.validators import ValidationError
|
from django.core.validators import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.http import HttpResponse, QueryDict
|
from django.http import HttpResponse
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
@ -26,7 +26,7 @@ from netbox.models.features import (
|
|||||||
CloningMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin,
|
CloningMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin,
|
||||||
)
|
)
|
||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
from utilities.utils import clean_html, render_jinja2
|
from utilities.utils import clean_html, dict_to_querydict, render_jinja2
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ConfigRevision',
|
'ConfigRevision',
|
||||||
@ -285,7 +285,7 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
text = clean_html(text, allowed_schemes)
|
text = clean_html(text, allowed_schemes)
|
||||||
|
|
||||||
# Sanitize link
|
# Sanitize link
|
||||||
link = urllib.parse.quote(link, safe='/:?&=%+[]@#')
|
link = urllib.parse.quote(link, safe='/:?&=%+[]@#,')
|
||||||
|
|
||||||
# Verify link scheme is allowed
|
# Verify link scheme is allowed
|
||||||
result = urllib.parse.urlparse(link)
|
result = urllib.parse.urlparse(link)
|
||||||
@ -462,8 +462,7 @@ class SavedFilter(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def url_params(self):
|
def url_params(self):
|
||||||
qd = QueryDict(mutable=True)
|
qd = dict_to_querydict(self.parameters)
|
||||||
qd.update(self.parameters)
|
|
||||||
return qd.urlencode()
|
return qd.urlencode()
|
||||||
|
|
||||||
|
|
||||||
|
@ -4,6 +4,8 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
from drf_spectacular.utils import extend_schema_field
|
||||||
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from netaddr.core import AddrFormatError
|
from netaddr.core import AddrFormatError
|
||||||
|
|
||||||
from dcim.models import Device, Interface, Region, Site, SiteGroup
|
from dcim.models import Device, Interface, Region, Site, SiteGroup
|
||||||
@ -414,6 +416,7 @@ class PrefixFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
|||||||
except (AddrFormatError, ValueError):
|
except (AddrFormatError, ValueError):
|
||||||
return queryset.none()
|
return queryset.none()
|
||||||
|
|
||||||
|
@extend_schema_field(OpenApiTypes.STR)
|
||||||
def filter_present_in_vrf(self, queryset, name, vrf):
|
def filter_present_in_vrf(self, queryset, name, vrf):
|
||||||
if vrf is None:
|
if vrf is None:
|
||||||
return queryset.none
|
return queryset.none
|
||||||
@ -659,6 +662,7 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
|||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(address__net_mask_length=value)
|
return queryset.filter(address__net_mask_length=value)
|
||||||
|
|
||||||
|
@extend_schema_field(OpenApiTypes.STR)
|
||||||
def filter_present_in_vrf(self, queryset, name, vrf):
|
def filter_present_in_vrf(self, queryset, name, vrf):
|
||||||
if vrf is None:
|
if vrf is None:
|
||||||
return queryset.none
|
return queryset.none
|
||||||
@ -727,6 +731,7 @@ class FHRPGroupFilterSet(NetBoxModelFilterSet):
|
|||||||
Q(name__icontains=value)
|
Q(name__icontains=value)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@extend_schema_field(OpenApiTypes.STR)
|
||||||
def filter_related_ip(self, queryset, name, value):
|
def filter_related_ip(self, queryset, name, value):
|
||||||
"""
|
"""
|
||||||
Filter by VRF & prefix of assigned IP addresses.
|
Filter by VRF & prefix of assigned IP addresses.
|
||||||
@ -941,9 +946,11 @@ class VLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
|||||||
pass
|
pass
|
||||||
return queryset.filter(qs_filter)
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
|
@extend_schema_field(OpenApiTypes.STR)
|
||||||
def get_for_device(self, queryset, name, value):
|
def get_for_device(self, queryset, name, value):
|
||||||
return queryset.get_for_device(value)
|
return queryset.get_for_device(value)
|
||||||
|
|
||||||
|
@extend_schema_field(OpenApiTypes.STR)
|
||||||
def get_for_virtualmachine(self, queryset, name, value):
|
def get_for_virtualmachine(self, queryset, name, value):
|
||||||
return queryset.get_for_virtualmachine(value)
|
return queryset.get_for_virtualmachine(value)
|
||||||
|
|
||||||
|
@ -345,7 +345,7 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
|
|||||||
})
|
})
|
||||||
elif selected_objects:
|
elif selected_objects:
|
||||||
assigned_object = self.cleaned_data[selected_objects[0]]
|
assigned_object = self.cleaned_data[selected_objects[0]]
|
||||||
if self.cleaned_data['primary_for_parent'] and assigned_object != self.instance.assigned_object:
|
if self.instance.pk and self.cleaned_data['primary_for_parent'] and assigned_object != self.instance.assigned_object:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
"Cannot reassign IP address while it is designated as the primary IP for the parent object"
|
"Cannot reassign IP address while it is designated as the primary IP for the parent object"
|
||||||
)
|
)
|
||||||
|
@ -60,7 +60,7 @@ class TokenAuthentication(authentication.TokenAuthentication):
|
|||||||
|
|
||||||
user = token.user
|
user = token.user
|
||||||
# When LDAP authentication is active try to load user data from LDAP directory
|
# When LDAP authentication is active try to load user data from LDAP directory
|
||||||
if settings.REMOTE_AUTH_BACKEND == 'netbox.authentication.LDAPBackend':
|
if 'netbox.authentication.LDAPBackend' in settings.REMOTE_AUTH_BACKEND:
|
||||||
from netbox.authentication import LDAPBackend
|
from netbox.authentication import LDAPBackend
|
||||||
ldap_backend = LDAPBackend()
|
ldap_backend = LDAPBackend()
|
||||||
|
|
||||||
|
@ -203,7 +203,7 @@ class MaintenanceModeMiddleware:
|
|||||||
"""
|
"""
|
||||||
Prevent any write-related database operations if an exception is raised.
|
Prevent any write-related database operations if an exception is raised.
|
||||||
"""
|
"""
|
||||||
if isinstance(exception, InternalError):
|
if get_config().MAINTENANCE_MODE and isinstance(exception, InternalError):
|
||||||
error_message = 'NetBox is currently operating in maintenance mode and is unable to perform write ' \
|
error_message = 'NetBox is currently operating in maintenance mode and is unable to perform write ' \
|
||||||
'operations. Please try again later.'
|
'operations. Please try again later.'
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW
|
|||||||
# Environment setup
|
# Environment setup
|
||||||
#
|
#
|
||||||
|
|
||||||
VERSION = '3.5.4'
|
VERSION = '3.5.5-dev'
|
||||||
|
|
||||||
# Hostname
|
# Hostname
|
||||||
HOSTNAME = platform.node()
|
HOSTNAME = platform.node()
|
||||||
|
@ -551,7 +551,7 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
|
|||||||
for name, m2m_field in m2m_fields.items():
|
for name, m2m_field in m2m_fields.items():
|
||||||
if name in form.nullable_fields and name in nullified_fields:
|
if name in form.nullable_fields and name in nullified_fields:
|
||||||
getattr(obj, name).clear()
|
getattr(obj, name).clear()
|
||||||
else:
|
elif form.cleaned_data[name]:
|
||||||
getattr(obj, name).set(form.cleaned_data[name])
|
getattr(obj, name).set(form.cleaned_data[name])
|
||||||
|
|
||||||
# Add/remove tags
|
# Add/remove tags
|
||||||
|
@ -3,9 +3,11 @@
|
|||||||
|
|
||||||
{% block extra_controls %}
|
{% block extra_controls %}
|
||||||
{% if perms.tenancy.add_contactassignment %}
|
{% if perms.tenancy.add_contactassignment %}
|
||||||
<a href="{% url 'tenancy:contactassignment_add' %}?content_type={{ object|content_type_id }}&object_id={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm">
|
{% with viewname=object|viewname:"contacts" %}
|
||||||
|
<a href="{% url 'tenancy:contactassignment_add' %}?content_type={{ object|content_type_id }}&object_id={{ object.pk }}&return_url={% url viewname pk=object.pk %}" class="btn btn-primary btn-sm">
|
||||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add a contact
|
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add a contact
|
||||||
</a>
|
</a>
|
||||||
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
from django import template
|
from django import template
|
||||||
from django.http import QueryDict
|
from django.http import QueryDict
|
||||||
|
|
||||||
|
from utilities.utils import dict_to_querydict
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'badge',
|
'badge',
|
||||||
'checkmark',
|
'checkmark',
|
||||||
@ -87,8 +89,7 @@ def htmx_table(context, viewname, return_url=None, **kwargs):
|
|||||||
viewname: The name of the view to use for the HTMX request (e.g. `dcim:site_list`)
|
viewname: The name of the view to use for the HTMX request (e.g. `dcim:site_list`)
|
||||||
return_url: The URL to pass as the `return_url`. If not provided, the current request's path will be used.
|
return_url: The URL to pass as the `return_url`. If not provided, the current request's path will be used.
|
||||||
"""
|
"""
|
||||||
url_params = QueryDict(mutable=True)
|
url_params = dict_to_querydict(kwargs)
|
||||||
url_params.update(kwargs)
|
|
||||||
url_params['return_url'] = return_url or context['request'].path
|
url_params['return_url'] = return_url or context['request'].path
|
||||||
return {
|
return {
|
||||||
'viewname': viewname,
|
'viewname': viewname,
|
||||||
|
@ -11,8 +11,9 @@ from django.core import serializers
|
|||||||
from django.db.models import Count, OuterRef, Subquery
|
from django.db.models import Count, OuterRef, Subquery
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.http import QueryDict
|
from django.http import QueryDict
|
||||||
from django.utils.html import escape
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.utils.datastructures import MultiValueDict
|
||||||
|
from django.utils.html import escape
|
||||||
from django.utils.timezone import localtime
|
from django.utils.timezone import localtime
|
||||||
from jinja2.sandbox import SandboxedEnvironment
|
from jinja2.sandbox import SandboxedEnvironment
|
||||||
from mptt.models import MPTTModel
|
from mptt.models import MPTTModel
|
||||||
@ -231,6 +232,19 @@ def dict_to_filter_params(d, prefix=''):
|
|||||||
return params
|
return params
|
||||||
|
|
||||||
|
|
||||||
|
def dict_to_querydict(d, mutable=True):
|
||||||
|
"""
|
||||||
|
Create a QueryDict instance from a regular Python dictionary.
|
||||||
|
"""
|
||||||
|
qd = QueryDict(mutable=True)
|
||||||
|
for k, v in d.items():
|
||||||
|
item = MultiValueDict({k: v}) if isinstance(v, (list, tuple, set)) else {k: v}
|
||||||
|
qd.update(item)
|
||||||
|
if not mutable:
|
||||||
|
qd._mutable = False
|
||||||
|
return qd
|
||||||
|
|
||||||
|
|
||||||
def normalize_querydict(querydict):
|
def normalize_querydict(querydict):
|
||||||
"""
|
"""
|
||||||
Convert a QueryDict to a normal, mutable dictionary, preserving list values. For example,
|
Convert a QueryDict to a normal, mutable dictionary, preserving list values. For example,
|
||||||
|
Loading…
Reference in New Issue
Block a user