mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-18 19:32:24 -06:00
Merge release v2.4.6
This commit is contained in:
@@ -54,6 +54,16 @@ class ProviderTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_providers_brief(self):
|
||||
|
||||
url = reverse('circuits-api:provider-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['id', 'name', 'slug', 'url']
|
||||
)
|
||||
|
||||
def test_create_provider(self):
|
||||
|
||||
data = {
|
||||
@@ -145,6 +155,16 @@ class CircuitTypeTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_circuittypes_brief(self):
|
||||
|
||||
url = reverse('circuits-api:circuittype-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['id', 'name', 'slug', 'url']
|
||||
)
|
||||
|
||||
def test_create_circuittype(self):
|
||||
|
||||
data = {
|
||||
@@ -214,6 +234,16 @@ class CircuitTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_circuits_brief(self):
|
||||
|
||||
url = reverse('circuits-api:circuit-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['cid', 'id', 'url']
|
||||
)
|
||||
|
||||
def test_create_circuit(self):
|
||||
|
||||
data = {
|
||||
|
||||
@@ -490,6 +490,15 @@ class ConsolePortSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
fields = ['id', 'device', 'name', 'cs_port', 'connection_status', 'tags']
|
||||
|
||||
|
||||
class NestedConsolePortSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ['id', 'url', 'device', 'name']
|
||||
|
||||
|
||||
#
|
||||
# Power outlets
|
||||
#
|
||||
@@ -527,6 +536,15 @@ class PowerPortSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
fields = ['id', 'device', 'name', 'power_outlet', 'connection_status', 'tags']
|
||||
|
||||
|
||||
class NestedPowerPortSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = ['id', 'url', 'device', 'name']
|
||||
|
||||
|
||||
#
|
||||
# Interfaces
|
||||
#
|
||||
@@ -650,10 +668,11 @@ class DeviceBaySerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
|
||||
class NestedDeviceBaySerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = DeviceBay
|
||||
fields = ['id', 'url', 'name']
|
||||
fields = ['id', 'url', 'device', 'name']
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -236,6 +236,11 @@ class DeviceViewSet(CustomFieldModelViewSet):
|
||||
"""
|
||||
if self.action == 'retrieve':
|
||||
return serializers.DeviceWithConfigContextSerializer
|
||||
|
||||
request = self.get_serializer_context()['request']
|
||||
if request.query_params.get('brief', False):
|
||||
return serializers.NestedDeviceSerializer
|
||||
|
||||
return serializers.DeviceSerializer
|
||||
|
||||
@action(detail=True, url_path='napalm')
|
||||
|
||||
@@ -1370,7 +1370,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
})
|
||||
|
||||
# Validate manufacturer/platform
|
||||
if self.device_type and self.platform:
|
||||
if hasattr(self, 'device_type') and self.platform:
|
||||
if self.platform.manufacturer and self.platform.manufacturer != self.device_type.manufacturer:
|
||||
raise ValidationError({
|
||||
'platform': "The assigned platform is limited to {} device types, but this device's type belongs "
|
||||
|
||||
@@ -42,6 +42,16 @@ class RegionTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_regions_brief(self):
|
||||
|
||||
url = reverse('dcim-api:region-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['id', 'name', 'slug', 'url']
|
||||
)
|
||||
|
||||
def test_create_region(self):
|
||||
|
||||
data = {
|
||||
@@ -156,6 +166,16 @@ class SiteTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_sites_brief(self):
|
||||
|
||||
url = reverse('dcim-api:site-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['id', 'name', 'slug', 'url']
|
||||
)
|
||||
|
||||
def test_create_site(self):
|
||||
|
||||
data = {
|
||||
@@ -260,6 +280,16 @@ class RackGroupTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_rackgroups_brief(self):
|
||||
|
||||
url = reverse('dcim-api:rackgroup-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['id', 'name', 'slug', 'url']
|
||||
)
|
||||
|
||||
def test_create_rackgroup(self):
|
||||
|
||||
data = {
|
||||
@@ -358,6 +388,16 @@ class RackRoleTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_rackroles_brief(self):
|
||||
|
||||
url = reverse('dcim-api:rackrole-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['id', 'name', 'slug', 'url']
|
||||
)
|
||||
|
||||
def test_create_rackrole(self):
|
||||
|
||||
data = {
|
||||
@@ -475,6 +515,16 @@ class RackTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_racks_brief(self):
|
||||
|
||||
url = reverse('dcim-api:rack-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['display_name', 'id', 'name', 'url']
|
||||
)
|
||||
|
||||
def test_create_rack(self):
|
||||
|
||||
data = {
|
||||
@@ -691,6 +741,16 @@ class ManufacturerTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_manufacturers_brief(self):
|
||||
|
||||
url = reverse('dcim-api:manufacturer-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['id', 'name', 'slug', 'url']
|
||||
)
|
||||
|
||||
def test_create_manufacturer(self):
|
||||
|
||||
data = {
|
||||
@@ -790,6 +850,16 @@ class DeviceTypeTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_devicetypes_brief(self):
|
||||
|
||||
url = reverse('dcim-api:devicetype-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['id', 'manufacturer', 'model', 'slug', 'url']
|
||||
)
|
||||
|
||||
def test_create_devicetype(self):
|
||||
|
||||
data = {
|
||||
@@ -1494,6 +1564,16 @@ class DeviceRoleTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_deviceroles_brief(self):
|
||||
|
||||
url = reverse('dcim-api:devicerole-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['id', 'name', 'slug', 'url']
|
||||
)
|
||||
|
||||
def test_create_devicerole(self):
|
||||
|
||||
data = {
|
||||
@@ -1592,6 +1672,16 @@ class PlatformTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_platforms_brief(self):
|
||||
|
||||
url = reverse('dcim-api:platform-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['id', 'name', 'slug', 'url']
|
||||
)
|
||||
|
||||
def test_create_platform(self):
|
||||
|
||||
data = {
|
||||
@@ -1720,6 +1810,16 @@ class DeviceTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_devices_brief(self):
|
||||
|
||||
url = reverse('dcim-api:device-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['display_name', 'id', 'name', 'url']
|
||||
)
|
||||
|
||||
def test_create_device(self):
|
||||
|
||||
data = {
|
||||
@@ -1846,6 +1946,16 @@ class ConsolePortTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_consoleports_brief(self):
|
||||
|
||||
url = reverse('dcim-api:consoleport-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['device', 'id', 'name', 'url']
|
||||
)
|
||||
|
||||
def test_create_consoleport(self):
|
||||
|
||||
data = {
|
||||
@@ -1951,6 +2061,16 @@ class ConsoleServerPortTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_consoleserverports_brief(self):
|
||||
|
||||
url = reverse('dcim-api:consoleserverport-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['device', 'id', 'name', 'url']
|
||||
)
|
||||
|
||||
def test_create_consoleserverport(self):
|
||||
|
||||
data = {
|
||||
@@ -2052,6 +2172,16 @@ class PowerPortTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_powerports_brief(self):
|
||||
|
||||
url = reverse('dcim-api:powerport-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['device', 'id', 'name', 'url']
|
||||
)
|
||||
|
||||
def test_create_powerport(self):
|
||||
|
||||
data = {
|
||||
@@ -2157,6 +2287,16 @@ class PowerOutletTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_poweroutlets_brief(self):
|
||||
|
||||
url = reverse('dcim-api:poweroutlet-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['device', 'id', 'name', 'url']
|
||||
)
|
||||
|
||||
def test_create_poweroutlet(self):
|
||||
|
||||
data = {
|
||||
@@ -2283,6 +2423,16 @@ class InterfaceTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_interfaces_brief(self):
|
||||
|
||||
url = reverse('dcim-api:interface-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['device', 'id', 'name', 'url']
|
||||
)
|
||||
|
||||
def test_create_interface(self):
|
||||
|
||||
data = {
|
||||
@@ -2454,6 +2604,16 @@ class DeviceBayTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_devicebays_brief(self):
|
||||
|
||||
url = reverse('dcim-api:devicebay-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['device', 'id', 'name', 'url']
|
||||
)
|
||||
|
||||
def test_create_devicebay(self):
|
||||
|
||||
data = {
|
||||
@@ -2776,6 +2936,16 @@ class InterfaceConnectionTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_interfaceconnections_brief(self):
|
||||
|
||||
url = reverse('dcim-api:interfaceconnection-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['connection_status', 'id', 'url']
|
||||
)
|
||||
|
||||
def test_create_interfaceconnection(self):
|
||||
|
||||
data = {
|
||||
@@ -2971,6 +3141,16 @@ class VirtualChassisTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 2)
|
||||
|
||||
def test_list_virtualchassis_brief(self):
|
||||
|
||||
url = reverse('dcim-api:virtualchassis-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['id', 'url']
|
||||
)
|
||||
|
||||
def test_create_virtualchassis(self):
|
||||
|
||||
data = {
|
||||
|
||||
@@ -32,6 +32,16 @@ class VRFTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_vrfs_brief(self):
|
||||
|
||||
url = reverse('ipam-api:vrf-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['id', 'name', 'rd', 'url']
|
||||
)
|
||||
|
||||
def test_create_vrf(self):
|
||||
|
||||
data = {
|
||||
@@ -123,6 +133,16 @@ class RIRTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_rirs_brief(self):
|
||||
|
||||
url = reverse('ipam-api:rir-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['id', 'name', 'slug', 'url']
|
||||
)
|
||||
|
||||
def test_create_rir(self):
|
||||
|
||||
data = {
|
||||
@@ -216,6 +236,16 @@ class AggregateTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_aggregates_brief(self):
|
||||
|
||||
url = reverse('ipam-api:aggregate-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['family', 'id', 'prefix', 'url']
|
||||
)
|
||||
|
||||
def test_create_aggregate(self):
|
||||
|
||||
data = {
|
||||
@@ -307,6 +337,16 @@ class RoleTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_roles_brief(self):
|
||||
|
||||
url = reverse('ipam-api:role-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['id', 'name', 'slug', 'url']
|
||||
)
|
||||
|
||||
def test_create_role(self):
|
||||
|
||||
data = {
|
||||
@@ -395,13 +435,23 @@ class PrefixTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['prefix'], str(self.prefix1.prefix))
|
||||
|
||||
def test_list_prefixs(self):
|
||||
def test_list_prefixes(self):
|
||||
|
||||
url = reverse('ipam-api:prefix-list')
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_prefixes_brief(self):
|
||||
|
||||
url = reverse('ipam-api:prefix-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['family', 'id', 'prefix', 'url']
|
||||
)
|
||||
|
||||
def test_create_prefix(self):
|
||||
|
||||
data = {
|
||||
@@ -628,6 +678,16 @@ class IPAddressTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_ipaddresses_brief(self):
|
||||
|
||||
url = reverse('ipam-api:ipaddress-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['address', 'family', 'id', 'url']
|
||||
)
|
||||
|
||||
def test_create_ipaddress(self):
|
||||
|
||||
data = {
|
||||
@@ -716,6 +776,16 @@ class VLANGroupTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_vlangroups_brief(self):
|
||||
|
||||
url = reverse('ipam-api:vlangroup-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['id', 'name', 'slug', 'url']
|
||||
)
|
||||
|
||||
def test_create_vlangroup(self):
|
||||
|
||||
data = {
|
||||
@@ -807,6 +877,16 @@ class VLANTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_vlans_brief(self):
|
||||
|
||||
url = reverse('ipam-api:vlan-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['display_name', 'id', 'name', 'url', 'vid']
|
||||
)
|
||||
|
||||
def test_create_vlan(self):
|
||||
|
||||
data = {
|
||||
|
||||
@@ -989,6 +989,9 @@ class ServiceCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
obj.virtual_machine = get_object_or_404(VirtualMachine, pk=url_kwargs['virtualmachine'])
|
||||
return obj
|
||||
|
||||
def get_return_url(self, request, service):
|
||||
return service.parent.get_absolute_url()
|
||||
|
||||
|
||||
class ServiceEditView(ServiceCreateView):
|
||||
permission_required = 'ipam.change_service'
|
||||
|
||||
@@ -82,7 +82,7 @@ $(document).ready(function() {
|
||||
}
|
||||
|
||||
if ($(parent).val() || $(parent).attr('nullable') == 'true') {
|
||||
var api_url = child_field.attr('api-url');
|
||||
var api_url = child_field.attr('api-url') + '&limit=0&brief=1';
|
||||
var disabled_indicator = child_field.attr('disabled-indicator');
|
||||
var initial_value = child_field.attr('initial');
|
||||
var display_field = child_field.attr('display-field') || 'name';
|
||||
|
||||
@@ -71,6 +71,16 @@ class SecretRoleTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_secretroles_brief(self):
|
||||
|
||||
url = reverse('secrets-api:secretrole-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['id', 'name', 'slug', 'url']
|
||||
)
|
||||
|
||||
def test_create_secretrole(self):
|
||||
|
||||
data = {
|
||||
|
||||
@@ -64,8 +64,10 @@ $(document).ready(function() {
|
||||
}
|
||||
|
||||
// Clean up hostnames/interfaces learned via LLDP
|
||||
var lldp_device = neighbor['hostname'].split(".")[0]; // Strip off any trailing domain name
|
||||
var lldp_interface = neighbor['port'].split(".")[0]; // Strip off any trailing subinterface ID
|
||||
var neighbor_host = neighbor['hostname'] || ""; // sanitize hostname if it's null to avoid breaking the split func
|
||||
var neighbor_port = neighbor['port'] || ""; // sanitize port if it's null to avoid breaking the split func
|
||||
var lldp_device = neighbor_host.split(".")[0]; // Strip off any trailing domain name
|
||||
var lldp_interface = neighbor_port.split(".")[0]; // Strip off any trailing subinterface ID
|
||||
|
||||
// Add LLDP neighbors to table
|
||||
row.children('td.device').html(lldp_device);
|
||||
|
||||
@@ -10,8 +10,12 @@
|
||||
<div class="panel panel-{% if token.is_expired %}danger{% else %}default{% endif %}">
|
||||
<div class="panel-heading">
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'user:token_edit' pk=token.pk %}" class="btn btn-xs btn-warning">Edit</a>
|
||||
<a href="{% url 'user:token_delete' pk=token.pk %}" class="btn btn-xs btn-danger">Delete</a>
|
||||
{% if perms.users.change_token %}
|
||||
<a href="{% url 'user:token_edit' pk=token.pk %}" class="btn btn-xs btn-warning">Edit</a>
|
||||
{% endif %}
|
||||
{% if perms.users.delete_token %}
|
||||
<a href="{% url 'user:token_delete' pk=token.pk %}" class="btn btn-xs btn-danger">Delete</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<i class="fa fa-key"></i> {{ token.key }}
|
||||
{% if token.is_expired %}
|
||||
@@ -49,10 +53,16 @@
|
||||
{% empty %}
|
||||
<p>You do not have any API tokens.</p>
|
||||
{% endfor %}
|
||||
<a href="{% url 'user:token_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add a token
|
||||
</a>
|
||||
{% if perms.users.add_token %}
|
||||
<a href="{% url 'user:token_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add a token
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="alert alert-info text-center" role="alert">
|
||||
You do not have permission to create new API tokens. If needed, ask an administrator to enable token creation for your account or an assigned group.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -29,6 +29,16 @@ class TenantGroupTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_tenantgroups_brief(self):
|
||||
|
||||
url = reverse('tenancy-api:tenantgroup-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['id', 'name', 'slug', 'url']
|
||||
)
|
||||
|
||||
def test_create_tenantgroup(self):
|
||||
|
||||
data = {
|
||||
@@ -122,6 +132,16 @@ class TenantTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_tenants_brief(self):
|
||||
|
||||
url = reverse('tenancy-api:tenant-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['id', 'name', 'slug', 'url']
|
||||
)
|
||||
|
||||
def test_create_tenant(self):
|
||||
|
||||
data = {
|
||||
|
||||
17
netbox/users/migrations/0003_token_permissions.py
Normal file
17
netbox/users/migrations/0003_token_permissions.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 2.0.8 on 2018-10-05 14:32
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0001_api_tokens_squashed_0002_unicode_literals'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='token',
|
||||
options={},
|
||||
),
|
||||
]
|
||||
@@ -39,7 +39,7 @@ class Token(models.Model):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
default_permissions = []
|
||||
pass
|
||||
|
||||
def __str__(self):
|
||||
# Only display the last 24 bits of the token to avoid accidental exposure.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import login as auth_login, logout as auth_logout, update_session_auth_hash
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
|
||||
from django.http import HttpResponseForbidden, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
@@ -217,8 +217,12 @@ class TokenEditView(LoginRequiredMixin, View):
|
||||
def get(self, request, pk=None):
|
||||
|
||||
if pk is not None:
|
||||
if not request.user.has_perm('users.change_token'):
|
||||
return HttpResponseForbidden()
|
||||
token = get_object_or_404(Token.objects.filter(user=request.user), pk=pk)
|
||||
else:
|
||||
if not request.user.has_perm('users.add_token'):
|
||||
return HttpResponseForbidden()
|
||||
token = Token(user=request.user)
|
||||
|
||||
form = TokenForm(instance=token)
|
||||
@@ -260,7 +264,8 @@ class TokenEditView(LoginRequiredMixin, View):
|
||||
})
|
||||
|
||||
|
||||
class TokenDeleteView(LoginRequiredMixin, View):
|
||||
class TokenDeleteView(PermissionRequiredMixin, View):
|
||||
permission_required = 'users.delete_token'
|
||||
|
||||
def get(self, request, pk):
|
||||
|
||||
|
||||
@@ -190,6 +190,19 @@ class ModelViewSet(_ModelViewSet):
|
||||
|
||||
return super(ModelViewSet, self).get_serializer(*args, **kwargs)
|
||||
|
||||
def get_serializer_class(self):
|
||||
|
||||
# If 'brief' has been passed as a query param, find and return the nested serializer for this model, if one
|
||||
# exists
|
||||
request = self.get_serializer_context()['request']
|
||||
if request.query_params.get('brief', False):
|
||||
serializer_class = get_serializer_for_model(self.queryset.model, prefix='Nested')
|
||||
if serializer_class is not None:
|
||||
return serializer_class
|
||||
|
||||
# Fall back to the hard-coded serializer class
|
||||
return self.serializer_class
|
||||
|
||||
|
||||
class FieldChoicesViewSet(ViewSet):
|
||||
"""
|
||||
|
||||
@@ -2,6 +2,7 @@ import csv
|
||||
from io import StringIO
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
@@ -148,6 +149,11 @@ def add_blank_choice(choices):
|
||||
return ((None, '---------'),) + tuple(choices)
|
||||
|
||||
|
||||
def utf8_encoder(data):
|
||||
for line in data:
|
||||
yield line.encode('utf-8')
|
||||
|
||||
|
||||
#
|
||||
# Widgets
|
||||
#
|
||||
@@ -301,7 +307,12 @@ class CSVDataField(forms.CharField):
|
||||
def to_python(self, value):
|
||||
|
||||
records = []
|
||||
reader = csv.reader(StringIO(value))
|
||||
|
||||
# Python 2 hack for Unicode support in the CSV reader
|
||||
if sys.version_info[0] < 3:
|
||||
reader = csv.reader(utf8_encoder(StringIO(value)))
|
||||
else:
|
||||
reader = csv.reader(StringIO(value))
|
||||
|
||||
# Consume and validate the first line of CSV data as column headers
|
||||
headers = next(reader)
|
||||
|
||||
@@ -167,7 +167,8 @@ class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
|
||||
class NestedInterfaceSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:interface-detail')
|
||||
virtual_machine = NestedVirtualMachineSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ['id', 'url', 'name']
|
||||
fields = ['id', 'url', 'virtual_machine', 'name']
|
||||
|
||||
@@ -54,6 +54,11 @@ class VirtualMachineViewSet(CustomFieldModelViewSet):
|
||||
"""
|
||||
if self.action == 'retrieve':
|
||||
return serializers.VirtualMachineWithConfigContextSerializer
|
||||
|
||||
request = self.get_serializer_context()['request']
|
||||
if request.query_params.get('brief', False):
|
||||
return serializers.NestedVirtualMachineSerializer
|
||||
|
||||
return serializers.VirtualMachineSerializer
|
||||
|
||||
|
||||
@@ -63,3 +68,10 @@ class InterfaceViewSet(ModelViewSet):
|
||||
).select_related('virtual_machine').prefetch_related('tags')
|
||||
serializer_class = serializers.InterfaceSerializer
|
||||
filter_class = filters.InterfaceFilter
|
||||
|
||||
def get_serializer_class(self):
|
||||
request = self.get_serializer_context()['request']
|
||||
if request.query_params.get('brief', False):
|
||||
# Override get_serializer_for_model(), which will return the DCIM NestedInterfaceSerializer
|
||||
return serializers.NestedInterfaceSerializer
|
||||
return serializers.InterfaceSerializer
|
||||
|
||||
@@ -251,7 +251,7 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
model = VirtualMachine
|
||||
fields = [
|
||||
'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6',
|
||||
'vcpus', 'memory', 'disk', 'comments', 'tags',
|
||||
'vcpus', 'memory', 'disk', 'comments', 'tags', 'local_context_data',
|
||||
]
|
||||
help_texts = {
|
||||
'local_context_data': "Local config context data overwrites all sources contexts in the final rendered config context",
|
||||
|
||||
@@ -33,6 +33,16 @@ class ClusterTypeTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_clustertypes_brief(self):
|
||||
|
||||
url = reverse('virtualization-api:clustertype-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['id', 'name', 'slug', 'url']
|
||||
)
|
||||
|
||||
def test_create_clustertype(self):
|
||||
|
||||
data = {
|
||||
@@ -124,6 +134,16 @@ class ClusterGroupTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_clustergroups_brief(self):
|
||||
|
||||
url = reverse('virtualization-api:clustergroup-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['id', 'name', 'slug', 'url']
|
||||
)
|
||||
|
||||
def test_create_clustergroup(self):
|
||||
|
||||
data = {
|
||||
@@ -218,6 +238,16 @@ class ClusterTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_clusters_brief(self):
|
||||
|
||||
url = reverse('virtualization-api:cluster-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['id', 'name', 'url']
|
||||
)
|
||||
|
||||
def test_create_cluster(self):
|
||||
|
||||
data = {
|
||||
@@ -322,6 +352,16 @@ class VirtualMachineTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_virtualmachines_brief(self):
|
||||
|
||||
url = reverse('virtualization-api:virtualmachine-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['id', 'name', 'url']
|
||||
)
|
||||
|
||||
def test_create_virtualmachine(self):
|
||||
|
||||
data = {
|
||||
@@ -445,6 +485,16 @@ class InterfaceTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
def test_list_interfaces_brief(self):
|
||||
|
||||
url = reverse('virtualization-api:interface-list')
|
||||
response = self.client.get('{}?brief=1'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['id', 'name', 'url', 'virtual_machine']
|
||||
)
|
||||
|
||||
def test_create_interface(self):
|
||||
|
||||
data = {
|
||||
|
||||
Reference in New Issue
Block a user