mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 01:41:22 -06:00
Merge branch 'develop' into feature
This commit is contained in:
commit
1b88b36820
@ -4,7 +4,7 @@ bleach
|
|||||||
|
|
||||||
# The Python web framework on which NetBox is built
|
# The Python web framework on which NetBox is built
|
||||||
# https://github.com/django/django
|
# https://github.com/django/django
|
||||||
Django
|
Django<4.1
|
||||||
|
|
||||||
# Django middleware which permits cross-domain API requests
|
# Django middleware which permits cross-domain API requests
|
||||||
# https://github.com/OttoYiu/django-cors-headers
|
# https://github.com/OttoYiu/django-cors-headers
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
# NetBox v3.2
|
# NetBox v3.2
|
||||||
|
|
||||||
## v3.2.8 (FUTURE)
|
## v3.2.9 (FUTURE)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v3.2.8 (2022-08-08)
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
@ -11,13 +15,20 @@
|
|||||||
* [#9881](https://github.com/netbox-community/netbox/issues/9881) - Increase granularity in utilization graph values
|
* [#9881](https://github.com/netbox-community/netbox/issues/9881) - Increase granularity in utilization graph values
|
||||||
* [#9882](https://github.com/netbox-community/netbox/issues/9882) - Add manufacturer column to modules table
|
* [#9882](https://github.com/netbox-community/netbox/issues/9882) - Add manufacturer column to modules table
|
||||||
* [#9883](https://github.com/netbox-community/netbox/issues/9883) - Linkify location column in power panels table
|
* [#9883](https://github.com/netbox-community/netbox/issues/9883) - Linkify location column in power panels table
|
||||||
|
* [#9906](https://github.com/netbox-community/netbox/issues/9906) - Include `color` attribute in front & rear port YAML import/export
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#9827](https://github.com/netbox-community/netbox/issues/9827) - Fix assignment of module bay position during bulk creation
|
||||||
* [#9871](https://github.com/netbox-community/netbox/issues/9871) - Fix utilization graph value alignments
|
* [#9871](https://github.com/netbox-community/netbox/issues/9871) - Fix utilization graph value alignments
|
||||||
* [#9884](https://github.com/netbox-community/netbox/issues/9884) - Prevent querying assigned VRF on prefix object init
|
* [#9884](https://github.com/netbox-community/netbox/issues/9884) - Prevent querying assigned VRF on prefix object init
|
||||||
* [#9885](https://github.com/netbox-community/netbox/issues/9885) - Fix child prefix counts when editing/deleting aggregates in bulk
|
* [#9885](https://github.com/netbox-community/netbox/issues/9885) - Fix child prefix counts when editing/deleting aggregates in bulk
|
||||||
* [#9891](https://github.com/netbox-community/netbox/issues/9891) - Ensure consistent ordering for tags during object serialization
|
* [#9891](https://github.com/netbox-community/netbox/issues/9891) - Ensure consistent ordering for tags during object serialization
|
||||||
|
* [#9919](https://github.com/netbox-community/netbox/issues/9919) - Fix potential XSS avenue via linked objects in tables
|
||||||
|
* [#9948](https://github.com/netbox-community/netbox/issues/9948) - Fix TypeError exception when requesting API tokens list as non-authenticated user
|
||||||
|
* [#9949](https://github.com/netbox-community/netbox/issues/9949) - Fix KeyError exception resulting from invalid API token provisioning request
|
||||||
|
* [#9950](https://github.com/netbox-community/netbox/issues/9950) - Prevent redirection to arbitrary URLs via `next` parameter on login URL
|
||||||
|
* [#9952](https://github.com/netbox-community/netbox/issues/9952) - Prevent InvalidMove when attempting to assign a nested child object as parent
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -156,7 +156,7 @@ class FrontPortTemplateImportForm(ComponentTemplateImportForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = FrontPortTemplate
|
model = FrontPortTemplate
|
||||||
fields = [
|
fields = [
|
||||||
'device_type', 'module_type', 'name', 'type', 'rear_port', 'rear_port_position', 'label', 'description',
|
'device_type', 'module_type', 'name', 'type', 'color', 'rear_port', 'rear_port_position', 'label', 'description',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -168,7 +168,7 @@ class RearPortTemplateImportForm(ComponentTemplateImportForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = RearPortTemplate
|
model = RearPortTemplate
|
||||||
fields = [
|
fields = [
|
||||||
'device_type', 'module_type', 'name', 'type', 'positions', 'label', 'description',
|
'device_type', 'module_type', 'name', 'type', 'color', 'positions', 'label', 'description',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -478,6 +478,7 @@ class FrontPortTemplate(ModularComponentTemplateModel):
|
|||||||
return {
|
return {
|
||||||
'name': self.name,
|
'name': self.name,
|
||||||
'type': self.type,
|
'type': self.type,
|
||||||
|
'color': self.color,
|
||||||
'rear_port': self.rear_port.name,
|
'rear_port': self.rear_port.name,
|
||||||
'rear_port_position': self.rear_port_position,
|
'rear_port_position': self.rear_port_position,
|
||||||
'label': self.label,
|
'label': self.label,
|
||||||
@ -527,6 +528,7 @@ class RearPortTemplate(ModularComponentTemplateModel):
|
|||||||
return {
|
return {
|
||||||
'name': self.name,
|
'name': self.name,
|
||||||
'type': self.type,
|
'type': self.type,
|
||||||
|
'color': self.color,
|
||||||
'positions': self.positions,
|
'positions': self.positions,
|
||||||
'label': self.label,
|
'label': self.label,
|
||||||
'description': self.description,
|
'description': self.description,
|
||||||
|
@ -2721,6 +2721,7 @@ class DeviceBulkAddModuleBayView(generic.BulkComponentCreateView):
|
|||||||
filterset = filtersets.DeviceFilterSet
|
filterset = filtersets.DeviceFilterSet
|
||||||
table = tables.DeviceTable
|
table = tables.DeviceTable
|
||||||
default_return_url = 'dcim:device_list'
|
default_return_url = 'dcim:device_list'
|
||||||
|
patterned_fields = ('name', 'label', 'position')
|
||||||
|
|
||||||
|
|
||||||
class DeviceBulkAddDeviceBayView(generic.BulkComponentCreateView):
|
class DeviceBulkAddDeviceBayView(generic.BulkComponentCreateView):
|
||||||
@ -3066,7 +3067,7 @@ class VirtualChassisAddMemberView(ObjectPermissionRequiredMixin, GetReturnURLMix
|
|||||||
if membership_form.is_valid():
|
if membership_form.is_valid():
|
||||||
|
|
||||||
membership_form.save()
|
membership_form.save()
|
||||||
msg = 'Added member <a href="{}">{}</a>'.format(device.get_absolute_url(), escape(device))
|
msg = f'Added member <a href="{device.get_absolute_url()}">{escape(device)}</a>'
|
||||||
messages.success(request, mark_safe(msg))
|
messages.success(request, mark_safe(msg))
|
||||||
|
|
||||||
if '_addanother' in request.POST:
|
if '_addanother' in request.POST:
|
||||||
@ -3111,8 +3112,7 @@ class VirtualChassisRemoveMemberView(ObjectPermissionRequiredMixin, GetReturnURL
|
|||||||
# Protect master device from being removed
|
# Protect master device from being removed
|
||||||
virtual_chassis = VirtualChassis.objects.filter(master=device).first()
|
virtual_chassis = VirtualChassis.objects.filter(master=device).first()
|
||||||
if virtual_chassis is not None:
|
if virtual_chassis is not None:
|
||||||
msg = 'Unable to remove master device {} from the virtual chassis.'.format(escape(device))
|
messages.error(request, f'Unable to remove master device {device} from the virtual chassis.')
|
||||||
messages.error(request, mark_safe(msg))
|
|
||||||
return redirect(device.get_absolute_url())
|
return redirect(device.get_absolute_url())
|
||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
@ -109,9 +109,9 @@ class NestedGroupModel(NetBoxFeatureSet, MPTTModel):
|
|||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
# An MPTT model cannot be its own parent
|
# An MPTT model cannot be its own parent
|
||||||
if self.pk and self.parent_id == self.pk:
|
if self.pk and self.parent and self.parent in self.get_descendants(include_self=True):
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
"parent": "Cannot assign self as parent."
|
"parent": f"Cannot assign self or child {self._meta.verbose_name} as parent."
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ from django.contrib.auth.models import AnonymousUser
|
|||||||
from django.db.models import DateField, DateTimeField
|
from django.db.models import DateField, DateTimeField
|
||||||
from django.template import Context, Template
|
from django.template import Context, Template
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils.html import escape
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django_tables2.columns import library
|
from django_tables2.columns import library
|
||||||
@ -428,8 +429,8 @@ class CustomFieldColumn(tables.Column):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _likify_item(item):
|
def _likify_item(item):
|
||||||
if hasattr(item, 'get_absolute_url'):
|
if hasattr(item, 'get_absolute_url'):
|
||||||
return f'<a href="{item.get_absolute_url()}">{item}</a>'
|
return f'<a href="{item.get_absolute_url()}">{escape(item)}</a>'
|
||||||
return item
|
return escape(item)
|
||||||
|
|
||||||
def render(self, value):
|
def render(self, value):
|
||||||
if self.customfield.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value is True:
|
if self.customfield.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value is True:
|
||||||
@ -437,13 +438,13 @@ class CustomFieldColumn(tables.Column):
|
|||||||
if self.customfield.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value is False:
|
if self.customfield.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value is False:
|
||||||
return mark_safe('<i class="mdi mdi-close-thick text-danger"></i>')
|
return mark_safe('<i class="mdi mdi-close-thick text-danger"></i>')
|
||||||
if self.customfield.type == CustomFieldTypeChoices.TYPE_URL:
|
if self.customfield.type == CustomFieldTypeChoices.TYPE_URL:
|
||||||
return mark_safe(f'<a href="{value}">{value}</a>')
|
return mark_safe(f'<a href="{escape(value)}">{escape(value)}</a>')
|
||||||
if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
|
if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
|
||||||
return ', '.join(v for v in value)
|
return ', '.join(v for v in value)
|
||||||
if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
|
if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
|
||||||
return mark_safe(', '.join([
|
return mark_safe(', '.join(
|
||||||
self._likify_item(obj) for obj in self.customfield.deserialize(value)
|
self._likify_item(obj) for obj in self.customfield.deserialize(value)
|
||||||
]))
|
))
|
||||||
if value is not None:
|
if value is not None:
|
||||||
obj = self.customfield.deserialize(value)
|
obj = self.customfield.deserialize(value)
|
||||||
return mark_safe(self._likify_item(obj))
|
return mark_safe(self._likify_item(obj))
|
||||||
|
@ -770,6 +770,7 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView):
|
|||||||
model_form = None
|
model_form = None
|
||||||
filterset = None
|
filterset = None
|
||||||
table = None
|
table = None
|
||||||
|
patterned_fields = ('name', 'label')
|
||||||
|
|
||||||
def get_required_permission(self):
|
def get_required_permission(self):
|
||||||
return f'dcim.add_{self.queryset.model._meta.model_name}'
|
return f'dcim.add_{self.queryset.model._meta.model_name}'
|
||||||
@ -805,16 +806,16 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView):
|
|||||||
|
|
||||||
for obj in data['pk']:
|
for obj in data['pk']:
|
||||||
|
|
||||||
names = data['name_pattern']
|
pattern_count = len(data[f'{self.patterned_fields[0]}_pattern'])
|
||||||
labels = data['label_pattern'] if 'label_pattern' in data else None
|
for i in range(pattern_count):
|
||||||
for i, name in enumerate(names):
|
|
||||||
label = labels[i] if labels else None
|
|
||||||
|
|
||||||
component_data = {
|
component_data = {
|
||||||
self.parent_field: obj.pk,
|
self.parent_field: obj.pk
|
||||||
'name': name,
|
|
||||||
'label': label
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for field_name in self.patterned_fields:
|
||||||
|
if data.get(f'{field_name}_pattern'):
|
||||||
|
component_data[field_name] = data[f'{field_name}_pattern'][i]
|
||||||
|
|
||||||
component_data.update(data)
|
component_data.update(data)
|
||||||
component_form = self.model_form(component_data)
|
component_form = self.model_form(component_data)
|
||||||
if component_form.is_valid():
|
if component_form.is_valid():
|
||||||
|
@ -389,10 +389,10 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView):
|
|||||||
)
|
)
|
||||||
logger.info(f"{msg} {obj} (PK: {obj.pk})")
|
logger.info(f"{msg} {obj} (PK: {obj.pk})")
|
||||||
if hasattr(obj, 'get_absolute_url'):
|
if hasattr(obj, 'get_absolute_url'):
|
||||||
msg = '{} <a href="{}">{}</a>'.format(msg, obj.get_absolute_url(), escape(obj))
|
msg = mark_safe(f'{msg} <a href="{obj.get_absolute_url()}">{escape(obj)}</a>')
|
||||||
else:
|
else:
|
||||||
msg = '{} {}'.format(msg, escape(obj))
|
msg = f'{msg} {obj}'
|
||||||
messages.success(request, mark_safe(msg))
|
messages.success(request, msg)
|
||||||
|
|
||||||
if '_addanother' in request.POST:
|
if '_addanother' in request.POST:
|
||||||
redirect_url = request.path
|
redirect_url = request.path
|
||||||
|
@ -58,6 +58,8 @@ class TokenViewSet(NetBoxModelViewSet):
|
|||||||
# Workaround for schema generation (drf_yasg)
|
# Workaround for schema generation (drf_yasg)
|
||||||
if getattr(self, 'swagger_fake_view', False):
|
if getattr(self, 'swagger_fake_view', False):
|
||||||
return queryset.none()
|
return queryset.none()
|
||||||
|
if not self.request.user.is_authenticated:
|
||||||
|
return queryset.none()
|
||||||
if self.request.user.is_superuser:
|
if self.request.user.is_superuser:
|
||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(user=self.request.user)
|
return queryset.filter(user=self.request.user)
|
||||||
@ -74,11 +76,11 @@ class TokenProvisionView(APIView):
|
|||||||
serializer.is_valid()
|
serializer.is_valid()
|
||||||
|
|
||||||
# Authenticate the user account based on the provided credentials
|
# Authenticate the user account based on the provided credentials
|
||||||
user = authenticate(
|
username = serializer.data.get('username')
|
||||||
request=request,
|
password = serializer.data.get('password')
|
||||||
username=serializer.data['username'],
|
if not username or not password:
|
||||||
password=serializer.data['password']
|
raise AuthenticationFailed("Username and password must be provided to provision a token.")
|
||||||
)
|
user = authenticate(request=request, username=username, password=password)
|
||||||
if user is None:
|
if user is None:
|
||||||
raise AuthenticationFailed("Invalid username/password")
|
raise AuthenticationFailed("Invalid username/password")
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ from django.http import HttpResponseRedirect
|
|||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.utils.http import url_has_allowed_host_and_scheme
|
||||||
from django.views.decorators.debug import sensitive_post_parameters
|
from django.views.decorators.debug import sensitive_post_parameters
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
from social_core.backends.utils import load_backends
|
from social_core.backends.utils import load_backends
|
||||||
@ -92,7 +93,7 @@ class LoginView(View):
|
|||||||
data = request.POST if request.method == "POST" else request.GET
|
data = request.POST if request.method == "POST" else request.GET
|
||||||
redirect_url = data.get('next', settings.LOGIN_REDIRECT_URL)
|
redirect_url = data.get('next', settings.LOGIN_REDIRECT_URL)
|
||||||
|
|
||||||
if redirect_url and redirect_url.startswith('/'):
|
if redirect_url and url_has_allowed_host_and_scheme(redirect_url, allowed_hosts=None):
|
||||||
logger.debug(f"Redirecting user to {redirect_url}")
|
logger.debug(f"Redirecting user to {redirect_url}")
|
||||||
else:
|
else:
|
||||||
if redirect_url:
|
if redirect_url:
|
||||||
|
@ -86,8 +86,8 @@ def placeholder(value):
|
|||||||
"""
|
"""
|
||||||
if value not in ('', None):
|
if value not in ('', None):
|
||||||
return value
|
return value
|
||||||
placeholder = '<span class="text-muted">—</span>'
|
|
||||||
return mark_safe(placeholder)
|
return mark_safe('<span class="text-muted">—</span>')
|
||||||
|
|
||||||
|
|
||||||
@register.filter()
|
@register.filter()
|
||||||
|
@ -109,9 +109,7 @@ def annotated_date(date_value):
|
|||||||
long_ts = date(date_value, 'DATETIME_FORMAT')
|
long_ts = date(date_value, 'DATETIME_FORMAT')
|
||||||
short_ts = date(date_value, 'SHORT_DATETIME_FORMAT')
|
short_ts = date(date_value, 'SHORT_DATETIME_FORMAT')
|
||||||
|
|
||||||
span = f'<span title="{long_ts}">{short_ts}</span>'
|
return mark_safe(f'<span title="{long_ts}">{short_ts}</span>')
|
||||||
|
|
||||||
return mark_safe(span)
|
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag
|
@register.simple_tag
|
||||||
|
@ -26,7 +26,7 @@ netaddr==0.8.0
|
|||||||
Pillow==9.2.0
|
Pillow==9.2.0
|
||||||
psycopg2-binary==2.9.3
|
psycopg2-binary==2.9.3
|
||||||
PyYAML==6.0
|
PyYAML==6.0
|
||||||
sentry-sdk==1.9.0
|
sentry-sdk==1.9.2
|
||||||
social-auth-app-django==5.0.0
|
social-auth-app-django==5.0.0
|
||||||
social-auth-core==4.3.0
|
social-auth-core==4.3.0
|
||||||
svgwrite==1.4.3
|
svgwrite==1.4.3
|
||||||
|
Loading…
Reference in New Issue
Block a user