mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-19 09:53:34 -06:00
commit
2c1a60b965
@ -1,5 +1,21 @@
|
|||||||
# NetBox v2.10
|
# NetBox v2.10
|
||||||
|
|
||||||
|
## v2.10.1 (2020-12-15)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#5444](https://github.com/netbox-community/netbox/issues/5444) - Don't force overwriting of boolean fields when bulk editing interfaces
|
||||||
|
* [#5450](https://github.com/netbox-community/netbox/issues/5450) - API serializer foreign count fields do not have a default value
|
||||||
|
* [#5453](https://github.com/netbox-community/netbox/issues/5453) - Correct change log representation when creating a cable
|
||||||
|
* [#5458](https://github.com/netbox-community/netbox/issues/5458) - Creating a component template throws an exception
|
||||||
|
* [#5461](https://github.com/netbox-community/netbox/issues/5461) - Rack Elevations throw reverse match exception
|
||||||
|
* [#5463](https://github.com/netbox-community/netbox/issues/5463) - Back-to-back Circuit Termination throws AttributeError exception
|
||||||
|
* [#5465](https://github.com/netbox-community/netbox/issues/5465) - Correct return URL when disconnecting a cable from a device
|
||||||
|
* [#5466](https://github.com/netbox-community/netbox/issues/5466) - Fix validation for required custom fields
|
||||||
|
* [#5470](https://github.com/netbox-community/netbox/issues/5470) - Fix exception when making `OPTIONS` request for a REST API list endpoint
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v2.10.0 (2020-12-14)
|
## v2.10.0 (2020-12-14)
|
||||||
|
|
||||||
**NOTE:** This release completely removes support for embedded graphs.
|
**NOTE:** This release completely removes support for embedded graphs.
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from django.db.models import Prefetch
|
from django.db.models import Prefetch
|
||||||
|
from django.db.models.functions import Coalesce
|
||||||
from rest_framework.routers import APIRootView
|
from rest_framework.routers import APIRootView
|
||||||
|
|
||||||
from circuits import filters
|
from circuits import filters
|
||||||
@ -24,7 +25,7 @@ class CircuitsRootView(APIRootView):
|
|||||||
|
|
||||||
class ProviderViewSet(CustomFieldModelViewSet):
|
class ProviderViewSet(CustomFieldModelViewSet):
|
||||||
queryset = Provider.objects.prefetch_related('tags').annotate(
|
queryset = Provider.objects.prefetch_related('tags').annotate(
|
||||||
circuit_count=get_subquery(Circuit, 'provider')
|
circuit_count=Coalesce(get_subquery(Circuit, 'provider'), 0)
|
||||||
)
|
)
|
||||||
serializer_class = serializers.ProviderSerializer
|
serializer_class = serializers.ProviderSerializer
|
||||||
filterset_class = filters.ProviderFilterSet
|
filterset_class = filters.ProviderFilterSet
|
||||||
@ -36,7 +37,7 @@ class ProviderViewSet(CustomFieldModelViewSet):
|
|||||||
|
|
||||||
class CircuitTypeViewSet(ModelViewSet):
|
class CircuitTypeViewSet(ModelViewSet):
|
||||||
queryset = CircuitType.objects.annotate(
|
queryset = CircuitType.objects.annotate(
|
||||||
circuit_count=get_subquery(Circuit, 'type')
|
circuit_count=Coalesce(get_subquery(Circuit, 'type'), 0)
|
||||||
)
|
)
|
||||||
serializer_class = serializers.CircuitTypeSerializer
|
serializer_class = serializers.CircuitTypeSerializer
|
||||||
filterset_class = filters.CircuitTypeFilterSet
|
filterset_class = filters.CircuitTypeFilterSet
|
||||||
|
@ -139,7 +139,7 @@ class CircuitView(generic.ObjectView):
|
|||||||
).filter(
|
).filter(
|
||||||
circuit=instance, term_side=CircuitTerminationSideChoices.SIDE_A
|
circuit=instance, term_side=CircuitTerminationSideChoices.SIDE_A
|
||||||
).first()
|
).first()
|
||||||
if termination_a and termination_a.connected_endpoint:
|
if termination_a and termination_a.connected_endpoint and hasattr(termination_a.connected_endpoint, 'ip_addresses'):
|
||||||
termination_a.ip_addresses = termination_a.connected_endpoint.ip_addresses.restrict(request.user, 'view')
|
termination_a.ip_addresses = termination_a.connected_endpoint.ip_addresses.restrict(request.user, 'view')
|
||||||
|
|
||||||
# Z-side termination
|
# Z-side termination
|
||||||
@ -148,7 +148,7 @@ class CircuitView(generic.ObjectView):
|
|||||||
).filter(
|
).filter(
|
||||||
circuit=instance, term_side=CircuitTerminationSideChoices.SIDE_Z
|
circuit=instance, term_side=CircuitTerminationSideChoices.SIDE_Z
|
||||||
).first()
|
).first()
|
||||||
if termination_z and termination_z.connected_endpoint:
|
if termination_z and termination_z.connected_endpoint and hasattr(termination_z.connected_endpoint, 'ip_addresses'):
|
||||||
termination_z.ip_addresses = termination_z.connected_endpoint.ip_addresses.restrict(request.user, 'view')
|
termination_z.ip_addresses = termination_z.connected_endpoint.ip_addresses.restrict(request.user, 'view')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -3,6 +3,7 @@ from collections import OrderedDict
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
|
from django.db.models.functions import Coalesce
|
||||||
from django.http import HttpResponseForbidden, HttpResponse
|
from django.http import HttpResponseForbidden, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from drf_yasg import openapi
|
from drf_yasg import openapi
|
||||||
@ -119,12 +120,12 @@ class SiteViewSet(CustomFieldModelViewSet):
|
|||||||
queryset = Site.objects.prefetch_related(
|
queryset = Site.objects.prefetch_related(
|
||||||
'region', 'tenant', 'tags'
|
'region', 'tenant', 'tags'
|
||||||
).annotate(
|
).annotate(
|
||||||
device_count=get_subquery(Device, 'site'),
|
device_count=Coalesce(get_subquery(Device, 'site'), 0),
|
||||||
rack_count=get_subquery(Rack, 'site'),
|
rack_count=Coalesce(get_subquery(Rack, 'site'), 0),
|
||||||
prefix_count=get_subquery(Prefix, 'site'),
|
prefix_count=Coalesce(get_subquery(Prefix, 'site'), 0),
|
||||||
vlan_count=get_subquery(VLAN, 'site'),
|
vlan_count=Coalesce(get_subquery(VLAN, 'site'), 0),
|
||||||
circuit_count=get_subquery(Circuit, 'terminations__site'),
|
circuit_count=Coalesce(get_subquery(Circuit, 'terminations__site'), 0),
|
||||||
virtualmachine_count=get_subquery(VirtualMachine, 'cluster__site'),
|
virtualmachine_count=Coalesce(get_subquery(VirtualMachine, 'cluster__site'), 0),
|
||||||
)
|
)
|
||||||
serializer_class = serializers.SiteSerializer
|
serializer_class = serializers.SiteSerializer
|
||||||
filterset_class = filters.SiteFilterSet
|
filterset_class = filters.SiteFilterSet
|
||||||
@ -152,7 +153,7 @@ class RackGroupViewSet(ModelViewSet):
|
|||||||
|
|
||||||
class RackRoleViewSet(ModelViewSet):
|
class RackRoleViewSet(ModelViewSet):
|
||||||
queryset = RackRole.objects.annotate(
|
queryset = RackRole.objects.annotate(
|
||||||
rack_count=get_subquery(Rack, 'role')
|
rack_count=Coalesce(get_subquery(Rack, 'role'), 0)
|
||||||
)
|
)
|
||||||
serializer_class = serializers.RackRoleSerializer
|
serializer_class = serializers.RackRoleSerializer
|
||||||
filterset_class = filters.RackRoleFilterSet
|
filterset_class = filters.RackRoleFilterSet
|
||||||
@ -166,8 +167,8 @@ class RackViewSet(CustomFieldModelViewSet):
|
|||||||
queryset = Rack.objects.prefetch_related(
|
queryset = Rack.objects.prefetch_related(
|
||||||
'site', 'group__site', 'role', 'tenant', 'tags'
|
'site', 'group__site', 'role', 'tenant', 'tags'
|
||||||
).annotate(
|
).annotate(
|
||||||
device_count=get_subquery(Device, 'rack'),
|
device_count=Coalesce(get_subquery(Device, 'rack'), 0),
|
||||||
powerfeed_count=get_subquery(PowerFeed, 'rack')
|
powerfeed_count=Coalesce(get_subquery(PowerFeed, 'rack'), 0)
|
||||||
)
|
)
|
||||||
serializer_class = serializers.RackSerializer
|
serializer_class = serializers.RackSerializer
|
||||||
filterset_class = filters.RackFilterSet
|
filterset_class = filters.RackFilterSet
|
||||||
@ -240,9 +241,9 @@ class RackReservationViewSet(ModelViewSet):
|
|||||||
|
|
||||||
class ManufacturerViewSet(ModelViewSet):
|
class ManufacturerViewSet(ModelViewSet):
|
||||||
queryset = Manufacturer.objects.annotate(
|
queryset = Manufacturer.objects.annotate(
|
||||||
devicetype_count=get_subquery(DeviceType, 'manufacturer'),
|
devicetype_count=Coalesce(get_subquery(DeviceType, 'manufacturer'), 0),
|
||||||
inventoryitem_count=get_subquery(InventoryItem, 'manufacturer'),
|
inventoryitem_count=Coalesce(get_subquery(InventoryItem, 'manufacturer'), 0),
|
||||||
platform_count=get_subquery(Platform, 'manufacturer')
|
platform_count=Coalesce(get_subquery(Platform, 'manufacturer'), 0)
|
||||||
)
|
)
|
||||||
serializer_class = serializers.ManufacturerSerializer
|
serializer_class = serializers.ManufacturerSerializer
|
||||||
filterset_class = filters.ManufacturerFilterSet
|
filterset_class = filters.ManufacturerFilterSet
|
||||||
@ -254,7 +255,7 @@ class ManufacturerViewSet(ModelViewSet):
|
|||||||
|
|
||||||
class DeviceTypeViewSet(CustomFieldModelViewSet):
|
class DeviceTypeViewSet(CustomFieldModelViewSet):
|
||||||
queryset = DeviceType.objects.prefetch_related('manufacturer', 'tags').annotate(
|
queryset = DeviceType.objects.prefetch_related('manufacturer', 'tags').annotate(
|
||||||
device_count=get_subquery(Device, 'device_type')
|
device_count=Coalesce(get_subquery(Device, 'device_type'), 0)
|
||||||
)
|
)
|
||||||
serializer_class = serializers.DeviceTypeSerializer
|
serializer_class = serializers.DeviceTypeSerializer
|
||||||
filterset_class = filters.DeviceTypeFilterSet
|
filterset_class = filters.DeviceTypeFilterSet
|
||||||
@ -318,8 +319,8 @@ class DeviceBayTemplateViewSet(ModelViewSet):
|
|||||||
|
|
||||||
class DeviceRoleViewSet(ModelViewSet):
|
class DeviceRoleViewSet(ModelViewSet):
|
||||||
queryset = DeviceRole.objects.annotate(
|
queryset = DeviceRole.objects.annotate(
|
||||||
device_count=get_subquery(Device, 'device_role'),
|
device_count=Coalesce(get_subquery(Device, 'device_role'), 0),
|
||||||
virtualmachine_count=get_subquery(VirtualMachine, 'role')
|
virtualmachine_count=Coalesce(get_subquery(VirtualMachine, 'role'), 0)
|
||||||
)
|
)
|
||||||
serializer_class = serializers.DeviceRoleSerializer
|
serializer_class = serializers.DeviceRoleSerializer
|
||||||
filterset_class = filters.DeviceRoleFilterSet
|
filterset_class = filters.DeviceRoleFilterSet
|
||||||
@ -331,8 +332,8 @@ class DeviceRoleViewSet(ModelViewSet):
|
|||||||
|
|
||||||
class PlatformViewSet(ModelViewSet):
|
class PlatformViewSet(ModelViewSet):
|
||||||
queryset = Platform.objects.annotate(
|
queryset = Platform.objects.annotate(
|
||||||
device_count=get_subquery(Device, 'platform'),
|
device_count=Coalesce(get_subquery(Device, 'platform'), 0),
|
||||||
virtualmachine_count=get_subquery(VirtualMachine, 'platform')
|
virtualmachine_count=Coalesce(get_subquery(VirtualMachine, 'platform'), 0)
|
||||||
)
|
)
|
||||||
serializer_class = serializers.PlatformSerializer
|
serializer_class = serializers.PlatformSerializer
|
||||||
filterset_class = filters.PlatformFilterSet
|
filterset_class = filters.PlatformFilterSet
|
||||||
@ -596,7 +597,7 @@ class CableViewSet(ModelViewSet):
|
|||||||
|
|
||||||
class VirtualChassisViewSet(ModelViewSet):
|
class VirtualChassisViewSet(ModelViewSet):
|
||||||
queryset = VirtualChassis.objects.prefetch_related('tags').annotate(
|
queryset = VirtualChassis.objects.prefetch_related('tags').annotate(
|
||||||
member_count=get_subquery(Device, 'virtual_chassis')
|
member_count=Coalesce(get_subquery(Device, 'virtual_chassis'), 0)
|
||||||
)
|
)
|
||||||
serializer_class = serializers.VirtualChassisSerializer
|
serializer_class = serializers.VirtualChassisSerializer
|
||||||
filterset_class = filters.VirtualChassisFilterSet
|
filterset_class = filters.VirtualChassisFilterSet
|
||||||
@ -610,7 +611,7 @@ class PowerPanelViewSet(ModelViewSet):
|
|||||||
queryset = PowerPanel.objects.prefetch_related(
|
queryset = PowerPanel.objects.prefetch_related(
|
||||||
'site', 'rack_group'
|
'site', 'rack_group'
|
||||||
).annotate(
|
).annotate(
|
||||||
powerfeed_count=get_subquery(PowerFeed, 'power_panel')
|
powerfeed_count=Coalesce(get_subquery(PowerFeed, 'power_panel'), 0)
|
||||||
)
|
)
|
||||||
serializer_class = serializers.PowerPanelSerializer
|
serializer_class = serializers.PowerPanelSerializer
|
||||||
filterset_class = filters.PowerPanelFilterSet
|
filterset_class = filters.PowerPanelFilterSet
|
||||||
|
@ -2839,7 +2839,7 @@ class InterfaceBulkCreateForm(
|
|||||||
|
|
||||||
class InterfaceBulkEditForm(
|
class InterfaceBulkEditForm(
|
||||||
form_from_model(Interface, [
|
form_from_model(Interface, [
|
||||||
'label', 'type', 'enabled', 'lag', 'mac_address', 'mtu', 'mgmt_only', 'description', 'mode'
|
'label', 'type', 'lag', 'mac_address', 'mtu', 'description', 'mode'
|
||||||
]),
|
]),
|
||||||
BootstrapMixin,
|
BootstrapMixin,
|
||||||
AddRemoveTagsForm,
|
AddRemoveTagsForm,
|
||||||
@ -2855,6 +2855,15 @@ class InterfaceBulkEditForm(
|
|||||||
disabled=True,
|
disabled=True,
|
||||||
widget=forms.HiddenInput()
|
widget=forms.HiddenInput()
|
||||||
)
|
)
|
||||||
|
enabled = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
widget=BulkEditNullBooleanSelect
|
||||||
|
)
|
||||||
|
mgmt_only = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
widget=BulkEditNullBooleanSelect,
|
||||||
|
label='Management only'
|
||||||
|
)
|
||||||
untagged_vlan = DynamicModelChoiceField(
|
untagged_vlan = DynamicModelChoiceField(
|
||||||
queryset=VLAN.objects.all(),
|
queryset=VLAN.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
|
@ -147,7 +147,8 @@ class Cable(ChangeLoggedModel, CustomFieldModel):
|
|||||||
return instance
|
return instance
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.label or '#{}'.format(self._pk)
|
pk = self.pk or self._pk
|
||||||
|
return self.label or f'#{pk}'
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('dcim:cable', args=[self.pk])
|
return reverse('dcim:cable', args=[self.pk])
|
||||||
|
@ -302,6 +302,14 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
'comments': 'New comments',
|
'comments': 'New comments',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||||
|
def test_list_rack_elevations(self):
|
||||||
|
"""
|
||||||
|
Test viewing the list of rack elevations.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse('dcim:rack_elevation_list'))
|
||||||
|
self.assertHttpStatus(response, 200)
|
||||||
|
|
||||||
|
|
||||||
class ManufacturerTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
class ManufacturerTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||||
model = Manufacturer
|
model = Manufacturer
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.db.models.functions import Coalesce
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django_rq.queues import get_connection
|
from django_rq.queues import get_connection
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@ -102,7 +103,7 @@ class ExportTemplateViewSet(ModelViewSet):
|
|||||||
|
|
||||||
class TagViewSet(ModelViewSet):
|
class TagViewSet(ModelViewSet):
|
||||||
queryset = Tag.objects.annotate(
|
queryset = Tag.objects.annotate(
|
||||||
tagged_items=get_subquery(TaggedItem, 'tag')
|
tagged_items=Coalesce(get_subquery(TaggedItem, 'tag'), 0)
|
||||||
)
|
)
|
||||||
serializer_class = serializers.TagSerializer
|
serializer_class = serializers.TagSerializer
|
||||||
filterset_class = filters.TagFilterSet
|
filterset_class = filters.TagFilterSet
|
||||||
|
@ -46,13 +46,13 @@ class CustomFieldModelForm(forms.ModelForm):
|
|||||||
# Annotate the field in the list of CustomField form fields
|
# Annotate the field in the list of CustomField form fields
|
||||||
self.custom_fields.append(field_name)
|
self.custom_fields.append(field_name)
|
||||||
|
|
||||||
def save(self, commit=True):
|
def clean(self):
|
||||||
|
|
||||||
# Save custom field data on instance
|
# Save custom field data on instance
|
||||||
for cf_name in self.custom_fields:
|
for cf_name in self.custom_fields:
|
||||||
self.instance.custom_field_data[cf_name[3:]] = self.cleaned_data.get(cf_name)
|
self.instance.custom_field_data[cf_name[3:]] = self.cleaned_data.get(cf_name)
|
||||||
|
|
||||||
return super().save(commit)
|
return super().clean()
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldModelCSVForm(CSVModelForm, CustomFieldModelForm):
|
class CustomFieldModelCSVForm(CSVModelForm, CustomFieldModelForm):
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import re
|
import re
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from datetime import datetime
|
from datetime import datetime, date
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
@ -317,10 +317,11 @@ class CustomField(models.Model):
|
|||||||
|
|
||||||
# Validate date
|
# Validate date
|
||||||
if self.type == CustomFieldTypeChoices.TYPE_DATE:
|
if self.type == CustomFieldTypeChoices.TYPE_DATE:
|
||||||
try:
|
if type(value) is not date:
|
||||||
datetime.strptime(value, '%Y-%m-%d')
|
try:
|
||||||
except ValueError:
|
datetime.strptime(value, '%Y-%m-%d')
|
||||||
raise ValidationError("Date values must be in the format YYYY-MM-DD.")
|
except ValueError:
|
||||||
|
raise ValidationError("Date values must be in the format YYYY-MM-DD.")
|
||||||
|
|
||||||
# Validate selected choice
|
# Validate selected choice
|
||||||
if self.type == CustomFieldTypeChoices.TYPE_SELECT:
|
if self.type == CustomFieldTypeChoices.TYPE_SELECT:
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.db.models.functions import Coalesce
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django_pglocks import advisory_lock
|
from django_pglocks import advisory_lock
|
||||||
from drf_yasg.utils import swagger_auto_schema
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
@ -32,8 +33,8 @@ class VRFViewSet(CustomFieldModelViewSet):
|
|||||||
queryset = VRF.objects.prefetch_related('tenant').prefetch_related(
|
queryset = VRF.objects.prefetch_related('tenant').prefetch_related(
|
||||||
'import_targets', 'export_targets', 'tags'
|
'import_targets', 'export_targets', 'tags'
|
||||||
).annotate(
|
).annotate(
|
||||||
ipaddress_count=get_subquery(IPAddress, 'vrf'),
|
ipaddress_count=Coalesce(get_subquery(IPAddress, 'vrf'), 0),
|
||||||
prefix_count=get_subquery(Prefix, 'vrf')
|
prefix_count=Coalesce(get_subquery(Prefix, 'vrf'), 0)
|
||||||
)
|
)
|
||||||
serializer_class = serializers.VRFSerializer
|
serializer_class = serializers.VRFSerializer
|
||||||
filterset_class = filters.VRFFilterSet
|
filterset_class = filters.VRFFilterSet
|
||||||
@ -55,7 +56,7 @@ class RouteTargetViewSet(CustomFieldModelViewSet):
|
|||||||
|
|
||||||
class RIRViewSet(ModelViewSet):
|
class RIRViewSet(ModelViewSet):
|
||||||
queryset = RIR.objects.annotate(
|
queryset = RIR.objects.annotate(
|
||||||
aggregate_count=get_subquery(Aggregate, 'rir')
|
aggregate_count=Coalesce(get_subquery(Aggregate, 'rir'), 0)
|
||||||
)
|
)
|
||||||
serializer_class = serializers.RIRSerializer
|
serializer_class = serializers.RIRSerializer
|
||||||
filterset_class = filters.RIRFilterSet
|
filterset_class = filters.RIRFilterSet
|
||||||
@ -77,8 +78,8 @@ class AggregateViewSet(CustomFieldModelViewSet):
|
|||||||
|
|
||||||
class RoleViewSet(ModelViewSet):
|
class RoleViewSet(ModelViewSet):
|
||||||
queryset = Role.objects.annotate(
|
queryset = Role.objects.annotate(
|
||||||
prefix_count=get_subquery(Prefix, 'role'),
|
prefix_count=Coalesce(get_subquery(Prefix, 'role'), 0),
|
||||||
vlan_count=get_subquery(VLAN, 'role')
|
vlan_count=Coalesce(get_subquery(VLAN, 'role'), 0)
|
||||||
)
|
)
|
||||||
serializer_class = serializers.RoleSerializer
|
serializer_class = serializers.RoleSerializer
|
||||||
filterset_class = filters.RoleFilterSet
|
filterset_class = filters.RoleFilterSet
|
||||||
@ -272,7 +273,7 @@ class IPAddressViewSet(CustomFieldModelViewSet):
|
|||||||
|
|
||||||
class VLANGroupViewSet(ModelViewSet):
|
class VLANGroupViewSet(ModelViewSet):
|
||||||
queryset = VLANGroup.objects.prefetch_related('site').annotate(
|
queryset = VLANGroup.objects.prefetch_related('site').annotate(
|
||||||
vlan_count=get_subquery(VLAN, 'group')
|
vlan_count=Coalesce(get_subquery(VLAN, 'group'), 0)
|
||||||
)
|
)
|
||||||
serializer_class = serializers.VLANGroupSerializer
|
serializer_class = serializers.VLANGroupSerializer
|
||||||
filterset_class = filters.VLANGroupFilterSet
|
filterset_class = filters.VLANGroupFilterSet
|
||||||
@ -286,7 +287,7 @@ class VLANViewSet(CustomFieldModelViewSet):
|
|||||||
queryset = VLAN.objects.prefetch_related(
|
queryset = VLAN.objects.prefetch_related(
|
||||||
'site', 'group', 'tenant', 'role', 'tags'
|
'site', 'group', 'tenant', 'role', 'tags'
|
||||||
).annotate(
|
).annotate(
|
||||||
prefix_count=get_subquery(Prefix, 'vlan')
|
prefix_count=Coalesce(get_subquery(Prefix, 'vlan'), 0)
|
||||||
)
|
)
|
||||||
serializer_class = serializers.VLANSerializer
|
serializer_class = serializers.VLANSerializer
|
||||||
filterset_class = filters.VLANFilterSet
|
filterset_class = filters.VLANFilterSet
|
||||||
|
@ -1,10 +1,45 @@
|
|||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
|
from django.http import Http404
|
||||||
from django.utils.encoding import force_str
|
from django.utils.encoding import force_str
|
||||||
|
from rest_framework import exceptions
|
||||||
from rest_framework.metadata import SimpleMetadata
|
from rest_framework.metadata import SimpleMetadata
|
||||||
|
from rest_framework.request import clone_request
|
||||||
|
|
||||||
from netbox.api import ContentTypeField
|
from netbox.api import ContentTypeField
|
||||||
|
|
||||||
|
|
||||||
class ContentTypeMetadata(SimpleMetadata):
|
class BulkOperationMetadata(SimpleMetadata):
|
||||||
|
|
||||||
|
def determine_actions(self, request, view):
|
||||||
|
"""
|
||||||
|
Replace the stock determine_actions() method to assess object permissions only
|
||||||
|
when viewing a specific object. This is necessary to support OPTIONS requests
|
||||||
|
with bulk update in place (see #5470).
|
||||||
|
"""
|
||||||
|
actions = {}
|
||||||
|
for method in {'PUT', 'POST'} & set(view.allowed_methods):
|
||||||
|
view.request = clone_request(request, method)
|
||||||
|
try:
|
||||||
|
# Test global permissions
|
||||||
|
if hasattr(view, 'check_permissions'):
|
||||||
|
view.check_permissions(view.request)
|
||||||
|
# Test object permissions (if viewing a specific object)
|
||||||
|
if method == 'PUT' and view.lookup_url_kwarg and hasattr(view, 'get_object'):
|
||||||
|
view.get_object()
|
||||||
|
except (exceptions.APIException, PermissionDenied, Http404):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# If user has appropriate permissions for the view, include
|
||||||
|
# appropriate metadata about the fields that should be supplied.
|
||||||
|
serializer = view.get_serializer()
|
||||||
|
actions[method] = self.get_serializer_info(serializer)
|
||||||
|
finally:
|
||||||
|
view.request = request
|
||||||
|
|
||||||
|
return actions
|
||||||
|
|
||||||
|
|
||||||
|
class ContentTypeMetadata(BulkOperationMetadata):
|
||||||
|
|
||||||
def get_field_info(self, field):
|
def get_field_info(self, field):
|
||||||
field_info = super().get_field_info(field)
|
field_info = super().get_field_info(field)
|
||||||
|
@ -16,7 +16,7 @@ from django.core.validators import URLValidator
|
|||||||
# Environment setup
|
# Environment setup
|
||||||
#
|
#
|
||||||
|
|
||||||
VERSION = '2.10.0'
|
VERSION = '2.10.1'
|
||||||
|
|
||||||
# Hostname
|
# Hostname
|
||||||
HOSTNAME = platform.node()
|
HOSTNAME = platform.node()
|
||||||
@ -467,6 +467,7 @@ REST_FRAMEWORK = {
|
|||||||
'DEFAULT_FILTER_BACKENDS': (
|
'DEFAULT_FILTER_BACKENDS': (
|
||||||
'django_filters.rest_framework.DjangoFilterBackend',
|
'django_filters.rest_framework.DjangoFilterBackend',
|
||||||
),
|
),
|
||||||
|
'DEFAULT_METADATA_CLASS': 'netbox.api.metadata.BulkOperationMetadata',
|
||||||
'DEFAULT_PAGINATION_CLASS': 'netbox.api.pagination.OptionalLimitOffsetPagination',
|
'DEFAULT_PAGINATION_CLASS': 'netbox.api.pagination.OptionalLimitOffsetPagination',
|
||||||
'DEFAULT_PERMISSION_CLASSES': (
|
'DEFAULT_PERMISSION_CLASSES': (
|
||||||
'netbox.api.authentication.TokenPermissions',
|
'netbox.api.authentication.TokenPermissions',
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import base64
|
import base64
|
||||||
|
|
||||||
from Crypto.PublicKey import RSA
|
from Crypto.PublicKey import RSA
|
||||||
|
from django.db.models.functions import Coalesce
|
||||||
from django.http import HttpResponseBadRequest
|
from django.http import HttpResponseBadRequest
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
@ -35,7 +36,7 @@ class SecretsRootView(APIRootView):
|
|||||||
|
|
||||||
class SecretRoleViewSet(ModelViewSet):
|
class SecretRoleViewSet(ModelViewSet):
|
||||||
queryset = SecretRole.objects.annotate(
|
queryset = SecretRole.objects.annotate(
|
||||||
secret_count=get_subquery(Secret, 'role')
|
secret_count=Coalesce(get_subquery(Secret, 'role'), 0)
|
||||||
)
|
)
|
||||||
serializer_class = serializers.SecretRoleSerializer
|
serializer_class = serializers.SecretRoleSerializer
|
||||||
filterset_class = filters.SecretRoleFilterSet
|
filterset_class = filters.SecretRoleFilterSet
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.delete_cable %}
|
{% if perms.dcim.delete_cable %}
|
||||||
<a href="{% url 'dcim:cable_delete' pk=cable.pk %}?return_url={{ device.get_absolute_url }}" title="Remove cable" class="btn btn-danger btn-xs">
|
<a href="{% url 'dcim:cable_delete' pk=cable.pk %}?return_url={{ object.get_absolute_url }}" title="Remove cable" class="btn btn-danger btn-xs">
|
||||||
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i>
|
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -9,18 +9,18 @@
|
|||||||
{% include 'responsive_table.html' %}
|
{% include 'responsive_table.html' %}
|
||||||
<div class="panel-footer noprint">
|
<div class="panel-footer noprint">
|
||||||
{% if table.rows %}
|
{% if table.rows %}
|
||||||
<button type="submit" name="_edit" formaction="{% url table.Meta.model|viewname:"bulk_rename" %}?return_url={{ devicetype.get_absolute_url }}" class="btn btn-xs btn-warning">
|
<button type="submit" name="_edit" formaction="{% url table.Meta.model|viewname:"bulk_rename" %}?return_url={{ object.get_absolute_url }}" class="btn btn-xs btn-warning">
|
||||||
<span class="mdi mdi-pencil" aria-hidden="true"></span> Rename
|
<span class="mdi mdi-pencil" aria-hidden="true"></span> Rename
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="_edit" formaction="{% url table.Meta.model|viewname:"bulk_edit" %}?return_url={{ devicetype.get_absolute_url }}" class="btn btn-xs btn-warning">
|
<button type="submit" name="_edit" formaction="{% url table.Meta.model|viewname:"bulk_edit" %}?return_url={{ object.get_absolute_url }}" class="btn btn-xs btn-warning">
|
||||||
<span class="mdi mdi-pencil" aria-hidden="true"></span> Edit
|
<span class="mdi mdi-pencil" aria-hidden="true"></span> Edit
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="_delete" formaction="{% url table.Meta.model|viewname:"bulk_delete" %}?return_url={{ devicetype.get_absolute_url }}" class="btn btn-xs btn-danger">
|
<button type="submit" name="_delete" formaction="{% url table.Meta.model|viewname:"bulk_delete" %}?return_url={{ object.get_absolute_url }}" class="btn btn-xs btn-danger">
|
||||||
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Delete
|
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Delete
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
<a href="{% url table.Meta.model|viewname:"add" %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}%23tab_{{ tab }}" class="btn btn-primary btn-xs">
|
<a href="{% url table.Meta.model|viewname:"add" %}?device_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_{{ tab }}" class="btn btn-primary btn-xs">
|
||||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span>
|
<span class="mdi mdi-plus-thick" aria-hidden="true"></span>
|
||||||
Add {{ title }}
|
Add {{ title }}
|
||||||
</a>
|
</a>
|
||||||
|
@ -35,7 +35,7 @@
|
|||||||
<br /><small class="text-muted">{{ rack.facility_id }}</small>
|
<br /><small class="text-muted">{{ rack.facility_id }}</small>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% include 'dcim/inc/rack_elevation.html' with face=rack_face %}
|
{% include 'dcim/inc/rack_elevation.html' with object=rack face=rack_face %}
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<strong><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name }}</a></strong>
|
<strong><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name }}</a></strong>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from django.db.models.functions import Coalesce
|
||||||
from rest_framework.routers import APIRootView
|
from rest_framework.routers import APIRootView
|
||||||
|
|
||||||
from circuits.models import Circuit
|
from circuits.models import Circuit
|
||||||
@ -46,13 +47,13 @@ class TenantViewSet(CustomFieldModelViewSet):
|
|||||||
).annotate(
|
).annotate(
|
||||||
circuit_count=get_subquery(Circuit, 'tenant'),
|
circuit_count=get_subquery(Circuit, 'tenant'),
|
||||||
device_count=get_subquery(Device, 'tenant'),
|
device_count=get_subquery(Device, 'tenant'),
|
||||||
ipaddress_count=get_subquery(IPAddress, 'tenant'),
|
ipaddress_count=Coalesce(get_subquery(IPAddress, 'tenant'), 0),
|
||||||
prefix_count=get_subquery(Prefix, 'tenant'),
|
prefix_count=Coalesce(get_subquery(Prefix, 'tenant'), 0),
|
||||||
rack_count=get_subquery(Rack, 'tenant'),
|
rack_count=Coalesce(get_subquery(Rack, 'tenant'), 0),
|
||||||
site_count=get_subquery(Site, 'tenant'),
|
site_count=Coalesce(get_subquery(Site, 'tenant'), 0),
|
||||||
virtualmachine_count=get_subquery(VirtualMachine, 'tenant'),
|
virtualmachine_count=Coalesce(get_subquery(VirtualMachine, 'tenant'), 0),
|
||||||
vlan_count=get_subquery(VLAN, 'tenant'),
|
vlan_count=Coalesce(get_subquery(VLAN, 'tenant'), 0),
|
||||||
vrf_count=get_subquery(VRF, 'tenant')
|
vrf_count=Coalesce(get_subquery(VRF, 'tenant'), 0)
|
||||||
)
|
)
|
||||||
serializer_class = serializers.TenantSerializer
|
serializer_class = serializers.TenantSerializer
|
||||||
filterset_class = filters.TenantFilterSet
|
filterset_class = filters.TenantFilterSet
|
||||||
|
@ -109,6 +109,15 @@ class APIViewTestCases:
|
|||||||
url = self._get_detail_url(instance2)
|
url = self._get_detail_url(instance2)
|
||||||
self.assertHttpStatus(self.client.get(url, **self.header), status.HTTP_404_NOT_FOUND)
|
self.assertHttpStatus(self.client.get(url, **self.header), status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||||
|
def test_options_object(self):
|
||||||
|
"""
|
||||||
|
Make an OPTIONS request for a single object.
|
||||||
|
"""
|
||||||
|
url = self._get_detail_url(self._get_queryset().first())
|
||||||
|
response = self.client.options(url, **self.header)
|
||||||
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||||
|
|
||||||
class ListObjectsViewTestCase(APITestCase):
|
class ListObjectsViewTestCase(APITestCase):
|
||||||
brief_fields = []
|
brief_fields = []
|
||||||
|
|
||||||
@ -174,6 +183,14 @@ class APIViewTestCases:
|
|||||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||||
self.assertEqual(len(response.data['results']), 2)
|
self.assertEqual(len(response.data['results']), 2)
|
||||||
|
|
||||||
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||||
|
def test_options_objects(self):
|
||||||
|
"""
|
||||||
|
Make an OPTIONS request for a list endpoint.
|
||||||
|
"""
|
||||||
|
response = self.client.options(self._get_list_url(), **self.header)
|
||||||
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||||
|
|
||||||
class CreateObjectViewTestCase(APITestCase):
|
class CreateObjectViewTestCase(APITestCase):
|
||||||
create_data = []
|
create_data = []
|
||||||
validation_excluded_fields = []
|
validation_excluded_fields = []
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from django.db.models.functions import Coalesce
|
||||||
from rest_framework.routers import APIRootView
|
from rest_framework.routers import APIRootView
|
||||||
|
|
||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
@ -22,7 +23,7 @@ class VirtualizationRootView(APIRootView):
|
|||||||
|
|
||||||
class ClusterTypeViewSet(ModelViewSet):
|
class ClusterTypeViewSet(ModelViewSet):
|
||||||
queryset = ClusterType.objects.annotate(
|
queryset = ClusterType.objects.annotate(
|
||||||
cluster_count=get_subquery(Cluster, 'type')
|
cluster_count=Coalesce(get_subquery(Cluster, 'type'), 0)
|
||||||
)
|
)
|
||||||
serializer_class = serializers.ClusterTypeSerializer
|
serializer_class = serializers.ClusterTypeSerializer
|
||||||
filterset_class = filters.ClusterTypeFilterSet
|
filterset_class = filters.ClusterTypeFilterSet
|
||||||
@ -30,7 +31,7 @@ class ClusterTypeViewSet(ModelViewSet):
|
|||||||
|
|
||||||
class ClusterGroupViewSet(ModelViewSet):
|
class ClusterGroupViewSet(ModelViewSet):
|
||||||
queryset = ClusterGroup.objects.annotate(
|
queryset = ClusterGroup.objects.annotate(
|
||||||
cluster_count=get_subquery(Cluster, 'group')
|
cluster_count=Coalesce(get_subquery(Cluster, 'group'), 0)
|
||||||
)
|
)
|
||||||
serializer_class = serializers.ClusterGroupSerializer
|
serializer_class = serializers.ClusterGroupSerializer
|
||||||
filterset_class = filters.ClusterGroupFilterSet
|
filterset_class = filters.ClusterGroupFilterSet
|
||||||
@ -40,8 +41,8 @@ class ClusterViewSet(CustomFieldModelViewSet):
|
|||||||
queryset = Cluster.objects.prefetch_related(
|
queryset = Cluster.objects.prefetch_related(
|
||||||
'type', 'group', 'tenant', 'site', 'tags'
|
'type', 'group', 'tenant', 'site', 'tags'
|
||||||
).annotate(
|
).annotate(
|
||||||
device_count=get_subquery(Device, 'cluster'),
|
device_count=Coalesce(get_subquery(Device, 'cluster'), 0),
|
||||||
virtualmachine_count=get_subquery(VirtualMachine, 'cluster')
|
virtualmachine_count=Coalesce(get_subquery(VirtualMachine, 'cluster'), 0)
|
||||||
)
|
)
|
||||||
serializer_class = serializers.ClusterSerializer
|
serializer_class = serializers.ClusterSerializer
|
||||||
filterset_class = filters.ClusterFilterSet
|
filterset_class = filters.ClusterFilterSet
|
||||||
|
Loading…
Reference in New Issue
Block a user