From bd573fd5cf3273456d1ac5faabbde85509cb8387 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Mon, 18 Feb 2019 21:37:00 -0500 Subject: [PATCH 001/140] implemented #2350 - config context included by default in API --- CHANGELOG.md | 14 ++++++++++++++ netbox/dcim/api/views.py | 15 +++++++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a36f7c21b..5c97e0d21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +v2.6.0 (FUTURE) + +## Changes + +### API Device/VM Config Context Included by Default ([#2350](https://github.com/digitalocean/netbox/issues/2350)) + +The rendered Config Context for Devices and VMs is now included by default in all API results (list and detail views). +Previously the rendered Config Context was only available in the detail view for objects. Users with large amounts of +context data may observe a performance drop when returning multiple objects. To combat this, in cases where the rendered +Config Context is not needed, the query parameter `?exclude=config_context` may be added to the request as to remove +the Config Context from being included in any results. + +--- + v2.5.7 (FUTURE) ## Enhancements diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 4e14d8163..477fffe07 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -291,16 +291,23 @@ class DeviceViewSet(CustomFieldModelViewSet): def get_serializer_class(self): """ - Include rendered config context when retrieving a single Device. + Select the specific serializer based on the request context. + + If the `brief` query param equates to True, return the NestedDeviceSerializer + + If the `exclude` query param includes `config_context` as a value, return the DeviceSerializer + + Else, return the DeviceWithConfigContextSerializer """ - 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 + elif 'config_context' in request.query_params.get('exclude', []): + return serializers.DeviceSerializer + + return serializers.DeviceWithConfigContextSerializer @action(detail=True, url_path='napalm') def napalm(self, request, pk): From e521508de99bbcab21c5f952838cef8ccc929a38 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Mon, 18 Feb 2019 22:10:05 -0500 Subject: [PATCH 002/140] #2350 - added virtual machines and test cases --- netbox/dcim/tests/test_api.py | 34 ++++++++++++++++++++---- netbox/virtualization/api/views.py | 15 ++++++++--- netbox/virtualization/tests/test_api.py | 35 ++++++++++++++++++++----- 3 files changed, 69 insertions(+), 15 deletions(-) diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index aa57d4790..3f9021bc6 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -1791,6 +1791,16 @@ class DeviceTest(APITestCase): site=self.site1, cluster=self.cluster1 ) + self.device_with_context_data = Device.objects.create( + device_type=self.devicetype1, + device_role=self.devicerole1, + name='Device with context data', + site=self.site1, + local_context_data={ + 'A': 1, + 'B': 2 + } + ) def test_get_device(self): @@ -1806,7 +1816,7 @@ class DeviceTest(APITestCase): url = reverse('dcim-api:device-list') response = self.client.get(url, **self.header) - self.assertEqual(response.data['count'], 3) + self.assertEqual(response.data['count'], 4) def test_list_devices_brief(self): @@ -1832,7 +1842,7 @@ class DeviceTest(APITestCase): response = self.client.post(url, data, format='json', **self.header) self.assertHttpStatus(response, status.HTTP_201_CREATED) - self.assertEqual(Device.objects.count(), 4) + self.assertEqual(Device.objects.count(), 5) device4 = Device.objects.get(pk=response.data['id']) self.assertEqual(device4.device_type_id, data['device_type']) self.assertEqual(device4.device_role_id, data['device_role']) @@ -1867,7 +1877,7 @@ class DeviceTest(APITestCase): response = self.client.post(url, data, format='json', **self.header) self.assertHttpStatus(response, status.HTTP_201_CREATED) - self.assertEqual(Device.objects.count(), 6) + self.assertEqual(Device.objects.count(), 7) self.assertEqual(response.data[0]['name'], data[0]['name']) self.assertEqual(response.data[1]['name'], data[1]['name']) self.assertEqual(response.data[2]['name'], data[2]['name']) @@ -1891,7 +1901,7 @@ class DeviceTest(APITestCase): response = self.client.put(url, data, format='json', **self.header) self.assertHttpStatus(response, status.HTTP_200_OK) - self.assertEqual(Device.objects.count(), 3) + self.assertEqual(Device.objects.count(), 4) device1 = Device.objects.get(pk=response.data['id']) self.assertEqual(device1.device_type_id, data['device_type']) self.assertEqual(device1.device_role_id, data['device_role']) @@ -1906,7 +1916,21 @@ class DeviceTest(APITestCase): response = self.client.delete(url, **self.header) self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT) - self.assertEqual(Device.objects.count(), 2) + self.assertEqual(Device.objects.count(), 3) + + def test_config_context_included_by_default_in_list_view(self): + + url = reverse('dcim-api:device-list') + '?slug=device-with-context-data' + response = self.client.get(url, **self.header) + + self.assertEqual(response.data['results'][0].get('config_context', {}).get('A'), 1) + + def test_config_context_excluded(self): + + url = reverse('dcim-api:device-list') + '?exclude=config_context' + response = self.client.get(url, **self.header) + + self.assertFalse('config_context' in response.data['results'][0]) class ConsolePortTest(APITestCase): diff --git a/netbox/virtualization/api/views.py b/netbox/virtualization/api/views.py index 3b0c02b22..ce7ee4934 100644 --- a/netbox/virtualization/api/views.py +++ b/netbox/virtualization/api/views.py @@ -50,16 +50,23 @@ class VirtualMachineViewSet(CustomFieldModelViewSet): def get_serializer_class(self): """ - Include rendered config context when retrieving a single VirtualMachine. + Select the specific serializer based on the request context. + + If the `brief` query param equates to True, return the NestedVirtualMachineSerializer + + If the `exclude` query param includes `config_context` as a value, return the VirtualMachineSerializer + + Else, return the VirtualMachineWithConfigContextSerializer """ - 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 + elif 'config_context' in request.query_params.get('exclude', []): + return serializers.VirtualMachineSerializer + + return serializers.VirtualMachineWithConfigContextSerializer class InterfaceViewSet(ModelViewSet): diff --git a/netbox/virtualization/tests/test_api.py b/netbox/virtualization/tests/test_api.py index 91792f8fb..d618ebe30 100644 --- a/netbox/virtualization/tests/test_api.py +++ b/netbox/virtualization/tests/test_api.py @@ -337,6 +337,14 @@ class VirtualMachineTest(APITestCase): self.virtualmachine1 = VirtualMachine.objects.create(name='Test Virtual Machine 1', cluster=self.cluster1) self.virtualmachine2 = VirtualMachine.objects.create(name='Test Virtual Machine 2', cluster=self.cluster1) self.virtualmachine3 = VirtualMachine.objects.create(name='Test Virtual Machine 3', cluster=self.cluster1) + self.virtualmachine_with_context_data = VirtualMachine.objects.create( + name='VM with context data', + cluster=self.cluster1, + local_context_data={ + 'A': 1, + 'B': 2 + } + ) def test_get_virtualmachine(self): @@ -350,7 +358,7 @@ class VirtualMachineTest(APITestCase): url = reverse('virtualization-api:virtualmachine-list') response = self.client.get(url, **self.header) - self.assertEqual(response.data['count'], 3) + self.assertEqual(response.data['count'], 4) def test_list_virtualmachines_brief(self): @@ -373,7 +381,7 @@ class VirtualMachineTest(APITestCase): response = self.client.post(url, data, format='json', **self.header) self.assertHttpStatus(response, status.HTTP_201_CREATED) - self.assertEqual(VirtualMachine.objects.count(), 4) + self.assertEqual(VirtualMachine.objects.count(), 5) virtualmachine4 = VirtualMachine.objects.get(pk=response.data['id']) self.assertEqual(virtualmachine4.name, data['name']) self.assertEqual(virtualmachine4.cluster.pk, data['cluster']) @@ -388,7 +396,7 @@ class VirtualMachineTest(APITestCase): response = self.client.post(url, data, format='json', **self.header) self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST) - self.assertEqual(VirtualMachine.objects.count(), 3) + self.assertEqual(VirtualMachine.objects.count(), 4) def test_create_virtualmachine_bulk(self): @@ -411,7 +419,7 @@ class VirtualMachineTest(APITestCase): response = self.client.post(url, data, format='json', **self.header) self.assertHttpStatus(response, status.HTTP_201_CREATED) - self.assertEqual(VirtualMachine.objects.count(), 6) + self.assertEqual(VirtualMachine.objects.count(), 7) self.assertEqual(response.data[0]['name'], data[0]['name']) self.assertEqual(response.data[1]['name'], data[1]['name']) self.assertEqual(response.data[2]['name'], data[2]['name']) @@ -438,7 +446,7 @@ class VirtualMachineTest(APITestCase): response = self.client.put(url, data, format='json', **self.header) self.assertHttpStatus(response, status.HTTP_200_OK) - self.assertEqual(VirtualMachine.objects.count(), 3) + self.assertEqual(VirtualMachine.objects.count(), 4) virtualmachine1 = VirtualMachine.objects.get(pk=response.data['id']) self.assertEqual(virtualmachine1.name, data['name']) self.assertEqual(virtualmachine1.cluster.pk, data['cluster']) @@ -451,7 +459,22 @@ class VirtualMachineTest(APITestCase): response = self.client.delete(url, **self.header) self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT) - self.assertEqual(VirtualMachine.objects.count(), 2) + self.assertEqual(VirtualMachine.objects.count(), 3) + + def test_config_context_included_by_default_in_list_view(self): + + url = reverse('virtualization-api:virtualmachine-list') + url = '{}?id__in={}'.format(url, self.virtualmachine_with_context_data.pk) + response = self.client.get(url, **self.header) + + self.assertEqual(response.data['results'][0].get('config_context', {}).get('A'), 1) + + def test_config_context_excluded(self): + + url = reverse('virtualization-api:virtualmachine-list') + '?exclude=config_context' + response = self.client.get(url, **self.header) + + self.assertFalse('config_context' in response.data['results'][0]) class InterfaceTest(APITestCase): From fc2bb724fa91e5d9de83269ad3999e71753c99af Mon Sep 17 00:00:00 2001 From: John Anderson Date: Wed, 20 Feb 2019 03:52:47 -0500 Subject: [PATCH 003/140] initial pass on migrating to custom tag model with color and comments fields --- CHANGELOG.md | 5 ++ .../migrations/0015_custom_tag_models.py | 25 ++++++ netbox/circuits/models.py | 6 +- .../dcim/migrations/0070_custom_tag_models.py | 85 +++++++++++++++++++ netbox/dcim/models.py | 30 +++---- netbox/extras/api/serializers.py | 4 +- netbox/extras/api/views.py | 4 +- netbox/extras/filters.py | 3 +- netbox/extras/forms.py | 8 +- .../extras/migrations/0017_tag_taggeditem.py | 46 ++++++++++ .../migrations/0018_rename_tag_tables.py | 46 ++++++++++ .../migrations/0019_delete_taggit_models.py | 52 ++++++++++++ .../0020_add_color_comments_to_tag.py | 24 ++++++ netbox/extras/models.py | 22 +++++ netbox/extras/tables.py | 8 +- netbox/extras/tests/test_api.py | 3 +- netbox/extras/tests/test_views.py | 3 +- netbox/extras/views.py | 14 +-- .../ipam/migrations/0025_custom_tag_models.py | 45 ++++++++++ netbox/ipam/models.py | 14 +-- netbox/netbox/admin.py | 15 +++- .../migrations/0006_custom_tag_models.py | 20 +++++ netbox/secrets/models.py | 4 +- netbox/templates/extras/tag.html | 18 ++++ netbox/templates/extras/tag_edit.html | 19 +++++ .../templates/utilities/templatetags/tag.html | 4 +- .../migrations/0006_custom_tag_models.py | 20 +++++ netbox/tenancy/models.py | 4 +- netbox/utilities/filters.py | 3 +- netbox/utilities/views.py | 2 +- .../migrations/0009_custom_tag_models.py | 25 ++++++ netbox/virtualization/models.py | 6 +- 32 files changed, 524 insertions(+), 63 deletions(-) create mode 100644 netbox/circuits/migrations/0015_custom_tag_models.py create mode 100644 netbox/dcim/migrations/0070_custom_tag_models.py create mode 100644 netbox/extras/migrations/0017_tag_taggeditem.py create mode 100644 netbox/extras/migrations/0018_rename_tag_tables.py create mode 100644 netbox/extras/migrations/0019_delete_taggit_models.py create mode 100644 netbox/extras/migrations/0020_add_color_comments_to_tag.py create mode 100644 netbox/ipam/migrations/0025_custom_tag_models.py create mode 100644 netbox/secrets/migrations/0006_custom_tag_models.py create mode 100644 netbox/templates/extras/tag_edit.html create mode 100644 netbox/tenancy/migrations/0006_custom_tag_models.py create mode 100644 netbox/virtualization/migrations/0009_custom_tag_models.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c97e0d21..30fcca532 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,11 @@ context data may observe a performance drop when returning multiple objects. To Config Context is not needed, the query parameter `?exclude=config_context` may be added to the request as to remove the Config Context from being included in any results. +## Enhancements + +* [#2324](https://github.com/digitalocean/netbox/issues/2324) - Add color option for tags +* [#2791](https://github.com/digitalocean/netbox/issues/2791) - Add a comment field for tags + --- v2.5.7 (FUTURE) diff --git a/netbox/circuits/migrations/0015_custom_tag_models.py b/netbox/circuits/migrations/0015_custom_tag_models.py new file mode 100644 index 000000000..657b20071 --- /dev/null +++ b/netbox/circuits/migrations/0015_custom_tag_models.py @@ -0,0 +1,25 @@ +# Generated by Django 2.1.4 on 2019-02-20 06:56 + +from django.db import migrations +import taggit.managers + + +class Migration(migrations.Migration): + + dependencies = [ + ('circuits', '0014_circuittermination_description'), + ('extras', '0018_rename_tag_tables'), + ] + + operations = [ + migrations.AlterField( + model_name='circuit', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AlterField( + model_name='provider', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + ] diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index b558d5007..5824ad65f 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -6,7 +6,7 @@ from taggit.managers import TaggableManager from dcim.constants import CONNECTION_STATUS_CHOICES, STATUS_CLASSES from dcim.fields import ASNField from dcim.models import CableTermination -from extras.models import CustomFieldModel, ObjectChange +from extras.models import CustomFieldModel, ObjectChange, TaggedItem from utilities.models import ChangeLoggedModel from utilities.utils import serialize_object from .constants import CIRCUIT_STATUS_ACTIVE, CIRCUIT_STATUS_CHOICES, TERM_SIDE_CHOICES @@ -55,7 +55,7 @@ class Provider(ChangeLoggedModel, CustomFieldModel): object_id_field='obj_id' ) - tags = TaggableManager() + tags = TaggableManager(through=TaggedItem) csv_headers = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments'] @@ -165,7 +165,7 @@ class Circuit(ChangeLoggedModel, CustomFieldModel): object_id_field='obj_id' ) - tags = TaggableManager() + tags = TaggableManager(through=TaggedItem) csv_headers = [ 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments', diff --git a/netbox/dcim/migrations/0070_custom_tag_models.py b/netbox/dcim/migrations/0070_custom_tag_models.py new file mode 100644 index 000000000..6b82a0dfd --- /dev/null +++ b/netbox/dcim/migrations/0070_custom_tag_models.py @@ -0,0 +1,85 @@ +# Generated by Django 2.1.4 on 2019-02-20 06:56 + +from django.db import migrations +import taggit.managers + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0069_deprecate_nullablecharfield'), + ('extras', '0018_rename_tag_tables'), + ] + + operations = [ + migrations.AlterField( + model_name='consoleport', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AlterField( + model_name='consoleserverport', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AlterField( + model_name='device', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AlterField( + model_name='devicebay', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AlterField( + model_name='devicetype', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AlterField( + model_name='frontport', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AlterField( + model_name='interface', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AlterField( + model_name='inventoryitem', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AlterField( + model_name='poweroutlet', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AlterField( + model_name='powerport', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AlterField( + model_name='rack', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AlterField( + model_name='rearport', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AlterField( + model_name='site', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AlterField( + model_name='virtualchassis', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index f7892b2af..3715e3033 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -15,7 +15,7 @@ from mptt.models import MPTTModel, TreeForeignKey from taggit.managers import TaggableManager from timezone_field import TimeZoneField -from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange +from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange, TaggedItem from utilities.fields import ColorField from utilities.managers import NaturalOrderingManager from utilities.models import ChangeLoggedModel @@ -319,7 +319,7 @@ class Site(ChangeLoggedModel, CustomFieldModel): ) objects = NaturalOrderingManager() - tags = TaggableManager() + tags = TaggableManager(through=TaggedItem) csv_headers = [ 'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address', @@ -566,7 +566,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel): ) objects = NaturalOrderingManager() - tags = TaggableManager() + tags = TaggableManager(through=TaggedItem) csv_headers = [ 'site', 'group_name', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag', 'width', @@ -914,7 +914,7 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel): object_id_field='obj_id' ) - tags = TaggableManager() + tags = TaggableManager(through=TaggedItem) csv_headers = [ 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'comments', @@ -1455,7 +1455,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): ) objects = NaturalOrderingManager() - tags = TaggableManager() + tags = TaggableManager(through=TaggedItem) csv_headers = [ 'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status', @@ -1743,7 +1743,7 @@ class ConsolePort(CableTermination, ComponentModel): ) objects = DeviceComponentManager() - tags = TaggableManager() + tags = TaggableManager(through=TaggedItem) csv_headers = ['device', 'name'] @@ -1786,7 +1786,7 @@ class ConsoleServerPort(CableTermination, ComponentModel): ) objects = DeviceComponentManager() - tags = TaggableManager() + tags = TaggableManager(through=TaggedItem) csv_headers = ['device', 'name'] @@ -1835,7 +1835,7 @@ class PowerPort(CableTermination, ComponentModel): ) objects = DeviceComponentManager() - tags = TaggableManager() + tags = TaggableManager(through=TaggedItem) csv_headers = ['device', 'name'] @@ -1878,7 +1878,7 @@ class PowerOutlet(CableTermination, ComponentModel): ) objects = DeviceComponentManager() - tags = TaggableManager() + tags = TaggableManager(through=TaggedItem) csv_headers = ['device', 'name'] @@ -1998,7 +1998,7 @@ class Interface(CableTermination, ComponentModel): ) objects = InterfaceManager() - tags = TaggableManager() + tags = TaggableManager(through=TaggedItem) csv_headers = [ 'device', 'virtual_machine', 'name', 'lag', 'form_factor', 'enabled', 'mac_address', 'mtu', 'mgmt_only', @@ -2199,7 +2199,7 @@ class FrontPort(CableTermination, ComponentModel): ) objects = DeviceComponentManager() - tags = TaggableManager() + tags = TaggableManager(through=TaggedItem) csv_headers = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'description'] @@ -2265,7 +2265,7 @@ class RearPort(CableTermination, ComponentModel): ) objects = DeviceComponentManager() - tags = TaggableManager() + tags = TaggableManager(through=TaggedItem) csv_headers = ['device', 'name', 'type', 'positions', 'description'] @@ -2312,7 +2312,7 @@ class DeviceBay(ComponentModel): ) objects = DeviceComponentManager() - tags = TaggableManager() + tags = TaggableManager(through=TaggedItem) csv_headers = ['device', 'name', 'installed_device'] @@ -2405,7 +2405,7 @@ class InventoryItem(ComponentModel): blank=True ) - tags = TaggableManager() + tags = TaggableManager(through=TaggedItem) csv_headers = [ 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description', @@ -2452,7 +2452,7 @@ class VirtualChassis(ChangeLoggedModel): blank=True ) - tags = TaggableManager() + tags = TaggableManager(through=TaggedItem) csv_headers = ['master', 'domain'] diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 7643562bb..9d24589b0 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -1,6 +1,5 @@ from django.core.exceptions import ObjectDoesNotExist from rest_framework import serializers -from taggit.models import Tag from dcim.api.nested_serializers import ( NestedDeviceSerializer, NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedRackSerializer, @@ -10,6 +9,7 @@ from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site from extras.constants import * from extras.models import ( ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap, + Tag ) from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer from tenancy.models import Tenant, TenantGroup @@ -80,7 +80,7 @@ class TagSerializer(ValidatedModelSerializer): class Meta: model = Tag - fields = ['id', 'name', 'slug', 'tagged_items'] + fields = ['id', 'name', 'slug', 'color', 'comments', 'tagged_items'] # diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index 0453b1f1c..17070c7b4 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -6,11 +6,11 @@ from rest_framework.decorators import action from rest_framework.exceptions import PermissionDenied from rest_framework.response import Response from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet -from taggit.models import Tag from extras import filters from extras.models import ( ConfigContext, CustomField, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap, + Tag ) from extras.reports import get_report, get_reports from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet @@ -115,7 +115,7 @@ class TopologyMapViewSet(ModelViewSet): # class TagViewSet(ModelViewSet): - queryset = Tag.objects.annotate(tagged_items=Count('taggit_taggeditem_items')) + queryset = Tag.objects.annotate(tagged_items=Count('extras_taggeditem_items')) serializer_class = serializers.TagSerializer filterset_class = filters.TagFilter diff --git a/netbox/extras/filters.py b/netbox/extras/filters.py index d0a801b48..eadbbae42 100644 --- a/netbox/extras/filters.py +++ b/netbox/extras/filters.py @@ -1,12 +1,11 @@ import django_filters from django.contrib.contenttypes.models import ContentType from django.db.models import Q -from taggit.models import Tag from dcim.models import DeviceRole, Platform, Region, Site from tenancy.models import Tenant, TenantGroup from .constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_TYPE_BOOLEAN, CF_TYPE_SELECT -from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, TopologyMap +from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, Tag, TopologyMap class CustomFieldFilter(django_filters.Filter): diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index b48482c93..b92955b1b 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -6,19 +6,18 @@ from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist from mptt.forms import TreeNodeMultipleChoiceField from taggit.forms import TagField -from taggit.models import Tag from dcim.models import DeviceRole, Platform, Region, Site from tenancy.models import Tenant, TenantGroup from utilities.forms import ( add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ContentTypeSelect, - FilterChoiceField, FilterTreeNodeMultipleChoiceField, LaxURLField, JSONField, SlugField, + FilterChoiceField, FilterTreeNodeMultipleChoiceField, LaxURLField, JSONField, SlugField, CommentField ) from .constants import ( CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL, OBJECTCHANGE_ACTION_CHOICES, ) -from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachment, ObjectChange +from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachment, ObjectChange, Tag # @@ -190,11 +189,12 @@ class CustomFieldFilterForm(forms.Form): class TagForm(BootstrapMixin, forms.ModelForm): slug = SlugField() + comments = CommentField() class Meta: model = Tag fields = [ - 'name', 'slug', + 'name', 'slug', 'color', 'comments' ] diff --git a/netbox/extras/migrations/0017_tag_taggeditem.py b/netbox/extras/migrations/0017_tag_taggeditem.py new file mode 100644 index 000000000..d76abf2f1 --- /dev/null +++ b/netbox/extras/migrations/0017_tag_taggeditem.py @@ -0,0 +1,46 @@ +# Generated by Django 2.1.4 on 2019-02-20 06:56 + +from django.db import migrations, models +import django.db.models.deletion +import utilities.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('extras', '0016_exporttemplate_add_cable'), + ] + + state_operations = [ + migrations.CreateModel( + name='Tag', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=100, unique=True)), + ('slug', models.SlugField(max_length=100, unique=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='TaggedItem', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)), + ('object_id', models.IntegerField(db_index=True)), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extras_taggeditem_tagged_items', to='contenttypes.ContentType')), + ('tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extras_taggeditem_items', to='extras.Tag')), + ], + options={ + 'abstract': False, + }, + ), + ] + + operations = [ + migrations.SeparateDatabaseAndState( + database_operations=None, + state_operations=state_operations + ) + ] diff --git a/netbox/extras/migrations/0018_rename_tag_tables.py b/netbox/extras/migrations/0018_rename_tag_tables.py new file mode 100644 index 000000000..521db2cb2 --- /dev/null +++ b/netbox/extras/migrations/0018_rename_tag_tables.py @@ -0,0 +1,46 @@ +# Generated by Django 2.1.4 on 2019-02-20 06:59 + +from django.db import migrations + + +class AppTaggitAlterModelTable(migrations.AlterModelTable): + """ + A special subclass of AlterModelTable which hardcodes the app_label to 'taggit' + + This is needed because the migration deals with models which belong to the taggit + app, however because taggit is a 3rd party app, we cannot create our own migrations + there. + """ + + def state_forwards(self, app_label, state): + super().state_forwards('taggit', state) + + def database_forwards(self, app_label, schema_editor, from_state, to_state): + super().database_forwards('taggit', schema_editor, from_state, to_state) + + def database_backwards(self, app_label, schema_editor, from_state, to_state): + super().database_backwards('taggit', schema_editor, from_state, to_state) + + def reduce(self, operation, app_label=None): + if app_label: + app_label = 'taggit' + super().reduce(operation, app_label=app_label) + + +class Migration(migrations.Migration): + + dependencies = [ + ('taggit', '0001_initial'), + ('extras', '0017_tag_taggeditem'), + ] + + operations = [ + AppTaggitAlterModelTable( + name='Tag', + table='extras_tag' + ), + AppTaggitAlterModelTable( + name='TaggedItem', + table='extras_taggeditem' + ), + ] diff --git a/netbox/extras/migrations/0019_delete_taggit_models.py b/netbox/extras/migrations/0019_delete_taggit_models.py new file mode 100644 index 000000000..fadb658e7 --- /dev/null +++ b/netbox/extras/migrations/0019_delete_taggit_models.py @@ -0,0 +1,52 @@ +# Generated by Django 2.1.4 on 2019-02-20 07:05 + +from django.db import migrations + + +class AppTaggitDeleteModel(migrations.DeleteModel): + """ + A special subclass of DeleteModel which hardcodes the app_label to 'taggit' + + This is needed because the migration deals with models which belong to the taggit + app, however because taggit is a 3rd party app, we cannot create our own migrations + there. + """ + + def state_forwards(self, app_label, state): + super().state_forwards('taggit', state) + + def database_forwards(self, app_label, schema_editor, from_state, to_state): + super().database_forwards('taggit', schema_editor, from_state, to_state) + + def database_backwards(self, app_label, schema_editor, from_state, to_state): + super().database_backwards('taggit', schema_editor, from_state, to_state) + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0018_rename_tag_tables'), + ('circuits', '0015_custom_tag_models'), + ('dcim', '0070_custom_tag_models'), + ('ipam', '0025_custom_tag_models'), + ('secrets', '0006_custom_tag_models'), + ('tenancy', '0006_custom_tag_models'), + ('virtualization', '0009_custom_tag_models'), + ] + + state_operations = [ + AppTaggitDeleteModel( + name='Tag', + ), + AppTaggitDeleteModel( + name='TaggedItem', + ), + ] + + database_operations = [] + operations = [ + migrations.SeparateDatabaseAndState( + database_operations=None, + state_operations=state_operations + ) + ] diff --git a/netbox/extras/migrations/0020_add_color_comments_to_tag.py b/netbox/extras/migrations/0020_add_color_comments_to_tag.py new file mode 100644 index 000000000..44800eaf4 --- /dev/null +++ b/netbox/extras/migrations/0020_add_color_comments_to_tag.py @@ -0,0 +1,24 @@ +# Generated by Django 2.1.4 on 2019-02-20 07:38 + +from django.db import migrations, models +import utilities.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0019_delete_taggit_models'), + ] + + operations = [ + migrations.AddField( + model_name='tag', + name='color', + field=utilities.fields.ColorField(max_length=6), + ), + migrations.AddField( + model_name='tag', + name='comments', + field=models.TextField(blank=True), + ), + ] diff --git a/netbox/extras/models.py b/netbox/extras/models.py index d3b9f4eff..55365048a 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -12,8 +12,10 @@ from django.db.models import F, Q from django.http import HttpResponse from django.template import Template, Context from django.urls import reverse +from taggit.models import TagBase, GenericTaggedItemBase from dcim.constants import CONNECTION_STATUS_CONNECTED +from utilities.fields import ColorField from utilities.utils import deepmerge, foreground_color from .constants import * from .querysets import ConfigContextQuerySet @@ -860,3 +862,23 @@ class ObjectChange(models.Model): self.object_repr, self.object_data, ) + + +# +# Tags +# + + +class Tag(TagBase): + color = ColorField() + comments = models.TextField( + blank=True + ) + + +class TaggedItem(GenericTaggedItemBase): + tag = models.ForeignKey( + to=Tag, + related_name="%(app_label)s_%(class)s_items", + on_delete=models.CASCADE + ) diff --git a/netbox/extras/tables.py b/netbox/extras/tables.py index 5fab8910f..e646ad821 100644 --- a/netbox/extras/tables.py +++ b/netbox/extras/tables.py @@ -1,9 +1,8 @@ import django_tables2 as tables from django_tables2.utils import Accessor -from taggit.models import Tag, TaggedItem -from utilities.tables import BaseTable, BooleanColumn, ToggleColumn -from .models import ConfigContext, ObjectChange +from utilities.tables import BaseTable, BooleanColumn, ColorColumn, ToggleColumn +from .models import ConfigContext, ObjectChange, Tag, TaggedItem TAG_ACTIONS = """ {% if perms.taggit.change_tag %} @@ -71,10 +70,11 @@ class TagTable(BaseTable): attrs={'td': {'class': 'text-right'}}, verbose_name='' ) + color = ColorColumn() class Meta(BaseTable.Meta): model = Tag - fields = ('pk', 'name', 'items', 'slug', 'actions') + fields = ('pk', 'name', 'items', 'slug', 'color', 'actions') class TaggedItemTable(BaseTable): diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index cccb00a8a..3fe36c9ef 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -1,11 +1,10 @@ from django.contrib.contenttypes.models import ContentType from django.urls import reverse from rest_framework import status -from taggit.models import Tag from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Platform, Region, Site from extras.constants import GRAPH_TYPE_SITE -from extras.models import ConfigContext, Graph, ExportTemplate +from extras.models import ConfigContext, Graph, ExportTemplate, Tag from tenancy.models import Tenant, TenantGroup from utilities.testing import APITestCase diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py index d478f069c..4c91fbb50 100644 --- a/netbox/extras/tests/test_views.py +++ b/netbox/extras/tests/test_views.py @@ -4,10 +4,9 @@ import uuid from django.contrib.auth.models import User from django.test import Client, TestCase from django.urls import reverse -from taggit.models import Tag from dcim.models import Site -from extras.models import ConfigContext, ObjectChange +from extras.models import ConfigContext, ObjectChange, Tag class TagTestCase(TestCase): diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 713143af8..517c3e5b4 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -9,7 +9,6 @@ from django.shortcuts import get_object_or_404, redirect, render from django.utils.safestring import mark_safe from django.views.generic import View from django_tables2 import RequestConfig -from taggit.models import Tag, TaggedItem from utilities.forms import ConfirmationForm from utilities.paginator import EnhancedPaginator @@ -19,7 +18,7 @@ from .forms import ( ConfigContextForm, ConfigContextBulkEditForm, ConfigContextFilterForm, ImageAttachmentForm, ObjectChangeFilterForm, TagFilterForm, TagForm, ) -from .models import ConfigContext, ImageAttachment, ObjectChange, ReportResult +from .models import ConfigContext, ImageAttachment, ObjectChange, ReportResult, Tag, TaggedItem from .reports import get_report, get_reports from .tables import ConfigContextTable, ObjectChangeTable, TagTable, TaggedItemTable @@ -30,7 +29,7 @@ from .tables import ConfigContextTable, ObjectChangeTable, TagTable, TaggedItemT class TagListView(ObjectListView): queryset = Tag.objects.annotate( - items=Count('taggit_taggeditem_items') + items=Count('extras_taggeditem_items') ).order_by( 'name' ) @@ -69,22 +68,23 @@ class TagView(View): class TagEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'taggit.change_tag' + permission_required = 'extras.change_tag' model = Tag model_form = TagForm default_return_url = 'extras:tag_list' + template_name = 'extras/tag_edit.html' class TagDeleteView(PermissionRequiredMixin, ObjectDeleteView): - permission_required = 'taggit.delete_tag' + permission_required = 'extras.delete_tag' model = Tag default_return_url = 'extras:tag_list' class TagBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): - permission_required = 'taggit.delete_tag' + permission_required = 'extras.delete_tag' queryset = Tag.objects.annotate( - items=Count('taggit_taggeditem_items') + items=Count('extras_taggeditem_items') ).order_by( 'name' ) diff --git a/netbox/ipam/migrations/0025_custom_tag_models.py b/netbox/ipam/migrations/0025_custom_tag_models.py new file mode 100644 index 000000000..05895219c --- /dev/null +++ b/netbox/ipam/migrations/0025_custom_tag_models.py @@ -0,0 +1,45 @@ +# Generated by Django 2.1.4 on 2019-02-20 06:56 + +from django.db import migrations +import taggit.managers + + +class Migration(migrations.Migration): + + dependencies = [ + ('ipam', '0024_vrf_allow_null_rd'), + ('extras', '0018_rename_tag_tables'), + ] + + operations = [ + migrations.AlterField( + model_name='aggregate', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AlterField( + model_name='ipaddress', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AlterField( + model_name='prefix', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AlterField( + model_name='service', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AlterField( + model_name='vlan', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AlterField( + model_name='vrf', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + ] diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index 181852ad3..a8a6185fb 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -10,7 +10,7 @@ from django.urls import reverse from taggit.managers import TaggableManager from dcim.models import Interface -from extras.models import CustomFieldModel +from extras.models import CustomFieldModel, TaggedItem from utilities.models import ChangeLoggedModel from .constants import * from .fields import IPNetworkField, IPAddressField @@ -55,7 +55,7 @@ class VRF(ChangeLoggedModel, CustomFieldModel): object_id_field='obj_id' ) - tags = TaggableManager() + tags = TaggableManager(through=TaggedItem) csv_headers = ['name', 'rd', 'tenant', 'enforce_unique', 'description'] @@ -154,7 +154,7 @@ class Aggregate(ChangeLoggedModel, CustomFieldModel): object_id_field='obj_id' ) - tags = TaggableManager() + tags = TaggableManager(through=TaggedItem) csv_headers = ['prefix', 'rir', 'date_added', 'description'] @@ -324,7 +324,7 @@ class Prefix(ChangeLoggedModel, CustomFieldModel): ) objects = PrefixQuerySet.as_manager() - tags = TaggableManager() + tags = TaggableManager(through=TaggedItem) csv_headers = [ 'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan_vid', 'status', 'role', 'is_pool', 'description', @@ -583,7 +583,7 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel): ) objects = IPAddressManager() - tags = TaggableManager() + tags = TaggableManager(through=TaggedItem) csv_headers = [ 'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface_name', 'is_primary', @@ -790,7 +790,7 @@ class VLAN(ChangeLoggedModel, CustomFieldModel): object_id_field='obj_id' ) - tags = TaggableManager() + tags = TaggableManager(through=TaggedItem) csv_headers = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description'] @@ -892,7 +892,7 @@ class Service(ChangeLoggedModel, CustomFieldModel): object_id_field='obj_id' ) - tags = TaggableManager() + tags = TaggableManager(through=TaggedItem) csv_headers = ['device', 'virtual_machine', 'name', 'protocol', 'description'] diff --git a/netbox/netbox/admin.py b/netbox/netbox/admin.py index 61796aabd..a0dca834a 100644 --- a/netbox/netbox/admin.py +++ b/netbox/netbox/admin.py @@ -2,8 +2,17 @@ from django.conf import settings from django.contrib.admin import AdminSite from django.contrib.auth.admin import GroupAdmin, UserAdmin from django.contrib.auth.models import Group, User -from taggit.admin import TagAdmin -from taggit.models import Tag +from taggit.admin import TagAdmin, TaggedItemInline + +from extras.models import Tag, TaggedItem + + +class NetBoxTaggedItemInline(TaggedItemInline): + model = TaggedItem + + +class NetBoxTagAdmin(TagAdmin): + inlines = [NetBoxTaggedItemInline] class NetBoxAdminSite(AdminSite): @@ -20,7 +29,7 @@ admin_site = NetBoxAdminSite(name='admin') # Register external models admin_site.register(Group, GroupAdmin) admin_site.register(User, UserAdmin) -admin_site.register(Tag, TagAdmin) +admin_site.register(Tag, NetBoxTagAdmin) # Modify the template to include an RQ link if django_rq is installed (see RQ_SHOW_ADMIN_LINK) if settings.WEBHOOKS_ENABLED: diff --git a/netbox/secrets/migrations/0006_custom_tag_models.py b/netbox/secrets/migrations/0006_custom_tag_models.py new file mode 100644 index 000000000..19699ad05 --- /dev/null +++ b/netbox/secrets/migrations/0006_custom_tag_models.py @@ -0,0 +1,20 @@ +# Generated by Django 2.1.4 on 2019-02-20 06:56 + +from django.db import migrations +import taggit.managers + + +class Migration(migrations.Migration): + + dependencies = [ + ('secrets', '0005_change_logging'), + ('extras', '0018_rename_tag_tables'), + ] + + operations = [ + migrations.AlterField( + model_name='secret', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + ] diff --git a/netbox/secrets/models.py b/netbox/secrets/models.py index 8190cd1dd..6dcb5abee 100644 --- a/netbox/secrets/models.py +++ b/netbox/secrets/models.py @@ -14,7 +14,7 @@ from django.urls import reverse from django.utils.encoding import force_bytes from taggit.managers import TaggableManager -from extras.models import CustomFieldModel +from extras.models import CustomFieldModel, TaggedItem from utilities.models import ChangeLoggedModel from .exceptions import InvalidKey from .hashers import SecretValidationHasher @@ -345,7 +345,7 @@ class Secret(ChangeLoggedModel, CustomFieldModel): object_id_field='obj_id' ) - tags = TaggableManager() + tags = TaggableManager(through=TaggedItem) plaintext = None csv_headers = ['device', 'role', 'name', 'plaintext'] diff --git a/netbox/templates/extras/tag.html b/netbox/templates/extras/tag.html index 748cad0bf..49e67c395 100644 --- a/netbox/templates/extras/tag.html +++ b/netbox/templates/extras/tag.html @@ -59,8 +59,26 @@ {{ items_count }} + + Color + +   + + +
+
+ Comments +
+
+ {% if tag.comments %} + {{ tag.comments|gfm }} + {% else %} + None + {% endif %} +
+
{% include 'panel_table.html' with table=items_table heading='Tagged Objects' %} diff --git a/netbox/templates/extras/tag_edit.html b/netbox/templates/extras/tag_edit.html new file mode 100644 index 000000000..800db1d26 --- /dev/null +++ b/netbox/templates/extras/tag_edit.html @@ -0,0 +1,19 @@ +{% extends 'utilities/obj_edit.html' %} +{% load form_helpers %} + +{% block form %} +
+
Tag
+
+ {% render_field form.name %} + {% render_field form.slug %} + {% render_field form.color %} +
+
+
+
Comments
+
+ {% render_field form.comments %} +
+
+{% endblock %} diff --git a/netbox/templates/utilities/templatetags/tag.html b/netbox/templates/utilities/templatetags/tag.html index 09b885d42..0be4e7ca5 100644 --- a/netbox/templates/utilities/templatetags/tag.html +++ b/netbox/templates/utilities/templatetags/tag.html @@ -1,5 +1,7 @@ +{% load helpers %} + {% if url_name %} - {{ tag }} + {{ tag }} {% else %} {{ tag }} {% endif %} diff --git a/netbox/tenancy/migrations/0006_custom_tag_models.py b/netbox/tenancy/migrations/0006_custom_tag_models.py new file mode 100644 index 000000000..ceeee437f --- /dev/null +++ b/netbox/tenancy/migrations/0006_custom_tag_models.py @@ -0,0 +1,20 @@ +# Generated by Django 2.1.4 on 2019-02-20 06:56 + +from django.db import migrations +import taggit.managers + + +class Migration(migrations.Migration): + + dependencies = [ + ('tenancy', '0005_change_logging'), + ('extras', '0018_rename_tag_tables'), + ] + + operations = [ + migrations.AlterField( + model_name='tenant', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + ] diff --git a/netbox/tenancy/models.py b/netbox/tenancy/models.py index 045679b90..bc67804d6 100644 --- a/netbox/tenancy/models.py +++ b/netbox/tenancy/models.py @@ -3,7 +3,7 @@ from django.db import models from django.urls import reverse from taggit.managers import TaggableManager -from extras.models import CustomFieldModel +from extras.models import CustomFieldModel, TaggedItem from utilities.models import ChangeLoggedModel @@ -70,7 +70,7 @@ class Tenant(ChangeLoggedModel, CustomFieldModel): object_id_field='obj_id' ) - tags = TaggableManager() + tags = TaggableManager(through=TaggedItem) csv_headers = ['name', 'slug', 'group', 'description', 'comments'] diff --git a/netbox/utilities/filters.py b/netbox/utilities/filters.py index 40e687077..674aee639 100644 --- a/netbox/utilities/filters.py +++ b/netbox/utilities/filters.py @@ -1,7 +1,8 @@ import django_filters from django.conf import settings from django.db.models import Q -from taggit.models import Tag + +from extras.models import Tag class NumericInFilter(django_filters.BaseInFilter, django_filters.NumberFilter): diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index f52f4ea9e..02441549f 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -157,7 +157,7 @@ class ObjectListView(View): # Construct queryset for tags list if hasattr(model, 'tags'): - tags = model.tags.annotate(count=Count('taggit_taggeditem_items')).order_by('name') + tags = model.tags.annotate(count=Count('extras_taggeditem_items')).order_by('name') else: tags = None diff --git a/netbox/virtualization/migrations/0009_custom_tag_models.py b/netbox/virtualization/migrations/0009_custom_tag_models.py new file mode 100644 index 000000000..02569bee2 --- /dev/null +++ b/netbox/virtualization/migrations/0009_custom_tag_models.py @@ -0,0 +1,25 @@ +# Generated by Django 2.1.4 on 2019-02-20 06:56 + +from django.db import migrations +import taggit.managers + + +class Migration(migrations.Migration): + + dependencies = [ + ('virtualization', '0008_virtualmachine_local_context_data'), + ('extras', '0018_rename_tag_tables'), + ] + + operations = [ + migrations.AlterField( + model_name='cluster', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AlterField( + model_name='virtualmachine', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + ] diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index ff9f39ee9..2ef782dfd 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -6,7 +6,7 @@ from django.urls import reverse from taggit.managers import TaggableManager from dcim.models import Device -from extras.models import ConfigContextModel, CustomFieldModel +from extras.models import ConfigContextModel, CustomFieldModel, TaggedItem from utilities.models import ChangeLoggedModel from .constants import DEVICE_STATUS_ACTIVE, VM_STATUS_CHOICES, VM_STATUS_CLASSES @@ -119,7 +119,7 @@ class Cluster(ChangeLoggedModel, CustomFieldModel): object_id_field='obj_id' ) - tags = TaggableManager() + tags = TaggableManager(through=TaggedItem) csv_headers = ['name', 'type', 'group', 'site', 'comments'] @@ -238,7 +238,7 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): object_id_field='obj_id' ) - tags = TaggableManager() + tags = TaggableManager(through=TaggedItem) csv_headers = [ 'name', 'status', 'role', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments', From 0a06d92c2ec334fd379712cf37c893b750e4345d Mon Sep 17 00:00:00 2001 From: John Anderson Date: Wed, 20 Feb 2019 03:56:32 -0500 Subject: [PATCH 004/140] added default values for comments and color fields on tag --- netbox/extras/migrations/0020_add_color_comments_to_tag.py | 4 ++-- netbox/extras/models.py | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/netbox/extras/migrations/0020_add_color_comments_to_tag.py b/netbox/extras/migrations/0020_add_color_comments_to_tag.py index 44800eaf4..0eebd4fbc 100644 --- a/netbox/extras/migrations/0020_add_color_comments_to_tag.py +++ b/netbox/extras/migrations/0020_add_color_comments_to_tag.py @@ -14,11 +14,11 @@ class Migration(migrations.Migration): migrations.AddField( model_name='tag', name='color', - field=utilities.fields.ColorField(max_length=6), + field=utilities.fields.ColorField(max_length=6, default='9e9e9e'), ), migrations.AddField( model_name='tag', name='comments', - field=models.TextField(blank=True), + field=models.TextField(blank=True, default=''), ), ] diff --git a/netbox/extras/models.py b/netbox/extras/models.py index 55365048a..da8e4744c 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -870,9 +870,12 @@ class ObjectChange(models.Model): class Tag(TagBase): - color = ColorField() + color = ColorField( + default='9e9e9e' + ) comments = models.TextField( - blank=True + blank=True, + default='' ) From bb6fb81fc0e5dfd5512f5928b4038a8ae4b7e19b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 20 Feb 2019 14:34:05 -0500 Subject: [PATCH 005/140] Closes #2643: Add description field to console/power components and device bays --- CHANGELOG.md | 1 + netbox/dcim/api/serializers.py | 18 +++--- netbox/dcim/filters.py | 22 +++---- netbox/dcim/forms.py | 58 +++++++++++++++++-- .../0071_device_components_add_description.py | 38 ++++++++++++ netbox/dcim/models.py | 35 +++++------ netbox/templates/dcim/device.html | 3 + netbox/templates/dcim/inc/consoleport.html | 5 ++ .../templates/dcim/inc/consoleserverport.html | 9 ++- netbox/templates/dcim/inc/devicebay.html | 32 +++++++--- netbox/templates/dcim/inc/poweroutlet.html | 9 ++- netbox/templates/dcim/inc/powerport.html | 5 ++ 12 files changed, 181 insertions(+), 54 deletions(-) create mode 100644 netbox/dcim/migrations/0071_device_components_add_description.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 30fcca532..13bb12931 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ the Config Context from being included in any results. ## Enhancements * [#2324](https://github.com/digitalocean/netbox/issues/2324) - Add color option for tags +* [#2643](https://github.com/digitalocean/netbox/issues/2643) - Add `description` field to console/power components and device bays * [#2791](https://github.com/digitalocean/netbox/issues/2791) - Add a comment field for tags --- diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index c17400a35..82bd9b6be 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -346,8 +346,8 @@ class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer) class Meta: model = ConsoleServerPort fields = [ - 'id', 'device', 'name', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', - 'tags', + 'id', 'device', 'name', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', + 'cable', 'tags', ] @@ -359,8 +359,8 @@ class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer): class Meta: model = ConsolePort fields = [ - 'id', 'device', 'name', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', - 'tags', + 'id', 'device', 'name', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', + 'cable', 'tags', ] @@ -372,8 +372,8 @@ class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer): class Meta: model = PowerOutlet fields = [ - 'id', 'device', 'name', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', - 'tags', + 'id', 'device', 'name', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', + 'cable', 'tags', ] @@ -385,8 +385,8 @@ class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer): class Meta: model = PowerPort fields = [ - 'id', 'device', 'name', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', - 'tags', + 'id', 'device', 'name', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', + 'cable', 'tags', ] @@ -475,7 +475,7 @@ class DeviceBaySerializer(TaggitSerializer, ValidatedModelSerializer): class Meta: model = DeviceBay - fields = ['id', 'device', 'name', 'installed_device', 'tags'] + fields = ['id', 'device', 'name', 'description', 'installed_device', 'tags'] # diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 4974c3b4d..4b22b2002 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -689,7 +689,8 @@ class DeviceComponentFilterSet(django_filters.FilterSet): if not value.strip(): return queryset return queryset.filter( - Q(name__icontains=value) + Q(name__icontains=value) | + Q(description__icontains=value) ) @@ -702,7 +703,7 @@ class ConsolePortFilter(DeviceComponentFilterSet): class Meta: model = ConsolePort - fields = ['name', 'connection_status'] + fields = ['name', 'description', 'connection_status'] class ConsoleServerPortFilter(DeviceComponentFilterSet): @@ -714,7 +715,7 @@ class ConsoleServerPortFilter(DeviceComponentFilterSet): class Meta: model = ConsoleServerPort - fields = ['name', 'connection_status'] + fields = ['name', 'description', 'connection_status'] class PowerPortFilter(DeviceComponentFilterSet): @@ -726,7 +727,7 @@ class PowerPortFilter(DeviceComponentFilterSet): class Meta: model = PowerPort - fields = ['name', 'connection_status'] + fields = ['name', 'description', 'connection_status'] class PowerOutletFilter(DeviceComponentFilterSet): @@ -738,7 +739,7 @@ class PowerOutletFilter(DeviceComponentFilterSet): class Meta: model = PowerOutlet - fields = ['name', 'connection_status'] + fields = ['name', 'description', 'connection_status'] class InterfaceFilter(django_filters.FilterSet): @@ -793,13 +794,14 @@ class InterfaceFilter(django_filters.FilterSet): class Meta: model = Interface - fields = ['name', 'connection_status', 'form_factor', 'enabled', 'mtu', 'mgmt_only'] + fields = ['name', 'connection_status', 'form_factor', 'enabled', 'mtu', 'mgmt_only', 'description'] def search(self, queryset, name, value): if not value.strip(): return queryset return queryset.filter( - Q(name__icontains=value) + Q(name__icontains=value) | + Q(description__icontains=value) ).distinct() def filter_device(self, queryset, name, value): @@ -857,7 +859,7 @@ class FrontPortFilter(DeviceComponentFilterSet): class Meta: model = FrontPort - fields = ['name', 'type'] + fields = ['name', 'type', 'description'] class RearPortFilter(DeviceComponentFilterSet): @@ -869,14 +871,14 @@ class RearPortFilter(DeviceComponentFilterSet): class Meta: model = RearPort - fields = ['name', 'type'] + fields = ['name', 'type', 'description'] class DeviceBayFilter(DeviceComponentFilterSet): class Meta: model = DeviceBay - fields = ['name'] + fields = ['name', 'description'] class InventoryItemFilter(DeviceComponentFilterSet): diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index bf774dfcb..18627c2f5 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -1854,7 +1854,7 @@ class ConsolePortForm(BootstrapMixin, forms.ModelForm): class Meta: model = ConsolePort fields = [ - 'device', 'name', 'tags', + 'device', 'name', 'description', 'tags', ] widgets = { 'device': forms.HiddenInput(), @@ -1865,6 +1865,10 @@ class ConsolePortCreateForm(ComponentForm): name_pattern = ExpandableNameField( label='Name' ) + description = forms.CharField( + max_length=100, + required=False + ) tags = TagField( required=False ) @@ -1882,7 +1886,7 @@ class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm): class Meta: model = ConsoleServerPort fields = [ - 'device', 'name', 'tags', + 'device', 'name', 'description', 'tags', ] widgets = { 'device': forms.HiddenInput(), @@ -1893,11 +1897,31 @@ class ConsoleServerPortCreateForm(ComponentForm): name_pattern = ExpandableNameField( label='Name' ) + description = forms.CharField( + max_length=100, + required=False + ) tags = TagField( required=False ) +class ConsoleServerPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=ConsoleServerPort.objects.all(), + widget=forms.MultipleHiddenInput() + ) + description = forms.CharField( + max_length=100, + required=False + ) + + class Meta: + nullable_fields = [ + 'description', + ] + + class ConsoleServerPortBulkRenameForm(BulkRenameForm): pk = forms.ModelMultipleChoiceField( queryset=ConsoleServerPort.objects.all(), @@ -1924,7 +1948,7 @@ class PowerPortForm(BootstrapMixin, forms.ModelForm): class Meta: model = PowerPort fields = [ - 'device', 'name', 'tags', + 'device', 'name', 'description', 'tags', ] widgets = { 'device': forms.HiddenInput(), @@ -1935,6 +1959,10 @@ class PowerPortCreateForm(ComponentForm): name_pattern = ExpandableNameField( label='Name' ) + description = forms.CharField( + max_length=100, + required=False + ) tags = TagField( required=False ) @@ -1952,7 +1980,7 @@ class PowerOutletForm(BootstrapMixin, forms.ModelForm): class Meta: model = PowerOutlet fields = [ - 'device', 'name', 'tags', + 'device', 'name', 'description', 'tags', ] widgets = { 'device': forms.HiddenInput(), @@ -1963,11 +1991,31 @@ class PowerOutletCreateForm(ComponentForm): name_pattern = ExpandableNameField( label='Name' ) + description = forms.CharField( + max_length=100, + required=False + ) tags = TagField( required=False ) +class PowerOutletBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=PowerOutlet.objects.all(), + widget=forms.MultipleHiddenInput() + ) + description = forms.CharField( + max_length=100, + required=False + ) + + class Meta: + nullable_fields = [ + 'description', + ] + + class PowerOutletBulkRenameForm(BulkRenameForm): pk = forms.ModelMultipleChoiceField( queryset=PowerOutlet.objects.all(), @@ -2776,7 +2824,7 @@ class DeviceBayForm(BootstrapMixin, forms.ModelForm): class Meta: model = DeviceBay fields = [ - 'device', 'name', 'tags', + 'device', 'name', 'description', 'tags', ] widgets = { 'device': forms.HiddenInput(), diff --git a/netbox/dcim/migrations/0071_device_components_add_description.py b/netbox/dcim/migrations/0071_device_components_add_description.py new file mode 100644 index 000000000..d38f8a88b --- /dev/null +++ b/netbox/dcim/migrations/0071_device_components_add_description.py @@ -0,0 +1,38 @@ +# Generated by Django 2.1.7 on 2019-02-20 18:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0070_custom_tag_models'), + ] + + operations = [ + migrations.AddField( + model_name='consoleport', + name='description', + field=models.CharField(blank=True, max_length=100), + ), + migrations.AddField( + model_name='consoleserverport', + name='description', + field=models.CharField(blank=True, max_length=100), + ), + migrations.AddField( + model_name='devicebay', + name='description', + field=models.CharField(blank=True, max_length=100), + ), + migrations.AddField( + model_name='poweroutlet', + name='description', + field=models.CharField(blank=True, max_length=100), + ), + migrations.AddField( + model_name='powerport', + name='description', + field=models.CharField(blank=True, max_length=100), + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 3715e3033..b7675b34e 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -46,6 +46,10 @@ class ComponentTemplateModel(models.Model): class ComponentModel(models.Model): + description = models.CharField( + max_length=100, + blank=True + ) class Meta: abstract = True @@ -1745,7 +1749,7 @@ class ConsolePort(CableTermination, ComponentModel): objects = DeviceComponentManager() tags = TaggableManager(through=TaggedItem) - csv_headers = ['device', 'name'] + csv_headers = ['device', 'name', 'description'] class Meta: ordering = ['device', 'name'] @@ -1761,6 +1765,7 @@ class ConsolePort(CableTermination, ComponentModel): return ( self.device.identifier, self.name, + self.description, ) @@ -1788,7 +1793,7 @@ class ConsoleServerPort(CableTermination, ComponentModel): objects = DeviceComponentManager() tags = TaggableManager(through=TaggedItem) - csv_headers = ['device', 'name'] + csv_headers = ['device', 'name', 'description'] class Meta: unique_together = ['device', 'name'] @@ -1803,6 +1808,7 @@ class ConsoleServerPort(CableTermination, ComponentModel): return ( self.device.identifier, self.name, + self.description, ) @@ -1841,7 +1847,7 @@ class PowerPort(CableTermination, ComponentModel): class Meta: ordering = ['device', 'name'] - unique_together = ['device', 'name'] + unique_together = ['device', 'name', 'description'] def __str__(self): return self.name @@ -1853,6 +1859,7 @@ class PowerPort(CableTermination, ComponentModel): return ( self.device.identifier, self.name, + self.description, ) @@ -1883,7 +1890,7 @@ class PowerOutlet(CableTermination, ComponentModel): csv_headers = ['device', 'name'] class Meta: - unique_together = ['device', 'name'] + unique_together = ['device', 'name', 'description'] def __str__(self): return self.name @@ -1895,6 +1902,7 @@ class PowerOutlet(CableTermination, ComponentModel): return ( self.device.identifier, self.name, + self.description, ) @@ -1973,10 +1981,6 @@ class Interface(CableTermination, ComponentModel): verbose_name='OOB Management', help_text='This interface is used only for out-of-band management' ) - description = models.CharField( - max_length=100, - blank=True - ) mode = models.PositiveSmallIntegerField( choices=IFACE_MODE_CHOICES, blank=True, @@ -2193,10 +2197,6 @@ class FrontPort(CableTermination, ComponentModel): default=1, validators=[MinValueValidator(1), MaxValueValidator(64)] ) - description = models.CharField( - max_length=100, - blank=True - ) objects = DeviceComponentManager() tags = TaggableManager(through=TaggedItem) @@ -2259,10 +2259,6 @@ class RearPort(CableTermination, ComponentModel): default=1, validators=[MinValueValidator(1), MaxValueValidator(64)] ) - description = models.CharField( - max_length=100, - blank=True - ) objects = DeviceComponentManager() tags = TaggableManager(through=TaggedItem) @@ -2314,7 +2310,7 @@ class DeviceBay(ComponentModel): objects = DeviceComponentManager() tags = TaggableManager(through=TaggedItem) - csv_headers = ['device', 'name', 'installed_device'] + csv_headers = ['device', 'name', 'installed_device', 'description'] class Meta: ordering = ['device', 'name'] @@ -2331,6 +2327,7 @@ class DeviceBay(ComponentModel): self.device.identifier, self.name, self.installed_device.identifier if self.installed_device else None, + self.description, ) def clean(self): @@ -2400,10 +2397,6 @@ class InventoryItem(ComponentModel): default=False, verbose_name='Discovered' ) - description = models.CharField( - max_length=100, - blank=True - ) tags = TaggableManager(through=TaggedItem) diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 716752c57..895f5eb59 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -445,6 +445,7 @@ {% endif %} Name Status + Description Installed Device @@ -570,6 +571,7 @@ {% endif %} Name + Description Cable Connection @@ -625,6 +627,7 @@ {% endif %} Name + Description Cable Connection diff --git a/netbox/templates/dcim/inc/consoleport.html b/netbox/templates/dcim/inc/consoleport.html index e78ab7ea7..74b13fdbc 100644 --- a/netbox/templates/dcim/inc/consoleport.html +++ b/netbox/templates/dcim/inc/consoleport.html @@ -5,6 +5,11 @@ {{ cp }} + {# Description #} + + {{ cp.description }} + + {# Cable #} {% if cp.cable %} diff --git a/netbox/templates/dcim/inc/consoleserverport.html b/netbox/templates/dcim/inc/consoleserverport.html index 212846b00..93f89a1e0 100644 --- a/netbox/templates/dcim/inc/consoleserverport.html +++ b/netbox/templates/dcim/inc/consoleserverport.html @@ -1,3 +1,5 @@ +{% load helpers %} + {# Checkbox #} @@ -12,12 +14,17 @@ {{ csp }} + {# Description #} + + {{ csp.description|placeholder }} + + {# Cable #} {% if csp.cable %} {{ csp.cable }} {% else %} - — + {% endif %} diff --git a/netbox/templates/dcim/inc/devicebay.html b/netbox/templates/dcim/inc/devicebay.html index dc2977855..9ae8958ec 100644 --- a/netbox/templates/dcim/inc/devicebay.html +++ b/netbox/templates/dcim/inc/devicebay.html @@ -1,16 +1,35 @@ +{% load helpers %} + {% if perms.dcim.change_devicebay or perms.dcim.delete_devicebay %} {% endif %} + + {# Name #} {{ devicebay.name }} + + {# Status #} + + {% if devicebay.installed_device %} + + {{ devicebay.installed_device.get_status_display }} + + {% else %} + Vacant + {% endif %} + + + {# Description #} + + {{ devicebay.description|placeholder }} + + + {# Installed device #} {% if devicebay.installed_device %} - - {{ devicebay.installed_device.get_status_display }} - {{ devicebay.installed_device }} @@ -18,11 +37,10 @@ {{ devicebay.installed_device.device_type.display_name }} {% else %} - - - Vacant - + {% endif %} + + {# Actions #} {% if perms.dcim.change_devicebay %} {% if devicebay.installed_device %} diff --git a/netbox/templates/dcim/inc/poweroutlet.html b/netbox/templates/dcim/inc/poweroutlet.html index 54ce414ad..7b359d36d 100644 --- a/netbox/templates/dcim/inc/poweroutlet.html +++ b/netbox/templates/dcim/inc/poweroutlet.html @@ -1,3 +1,5 @@ +{% load helpers %} + {# Checkbox #} @@ -12,12 +14,17 @@ {{ po }} + {# Description #} + + {{ po.description|placeholder }} + + {# Cable #} {% if po.cable %} {{ po.cable }} {% else %} - — + {% endif %} diff --git a/netbox/templates/dcim/inc/powerport.html b/netbox/templates/dcim/inc/powerport.html index 4bfbdadcf..55838399e 100644 --- a/netbox/templates/dcim/inc/powerport.html +++ b/netbox/templates/dcim/inc/powerport.html @@ -5,6 +5,11 @@ {{ pp }} + {# Description #} + + {{ pp.description }} + + {# Cable #} {% if pp.cable %} From fba6d286036d0fdaf8dcae16f8ef11787c05f6be Mon Sep 17 00:00:00 2001 From: John Anderson Date: Thu, 21 Feb 2019 00:15:15 -0500 Subject: [PATCH 006/140] removed migration to delete taggit models --- .../extras/migrations/0017_tag_taggeditem.py | 4 ++ .../migrations/0018_rename_tag_tables.py | 7 +++ ...g.py => 0019_add_color_comments_to_tag.py} | 8 ++- .../migrations/0019_delete_taggit_models.py | 52 ------------------- 4 files changed, 18 insertions(+), 53 deletions(-) rename netbox/extras/migrations/{0020_add_color_comments_to_tag.py => 0019_add_color_comments_to_tag.py} (62%) delete mode 100644 netbox/extras/migrations/0019_delete_taggit_models.py diff --git a/netbox/extras/migrations/0017_tag_taggeditem.py b/netbox/extras/migrations/0017_tag_taggeditem.py index d76abf2f1..8dbf32a75 100644 --- a/netbox/extras/migrations/0017_tag_taggeditem.py +++ b/netbox/extras/migrations/0017_tag_taggeditem.py @@ -36,6 +36,10 @@ class Migration(migrations.Migration): 'abstract': False, }, ), + migrations.AlterIndexTogether( + name='taggeditem', + index_together={('content_type', 'object_id')}, + ), ] operations = [ diff --git a/netbox/extras/migrations/0018_rename_tag_tables.py b/netbox/extras/migrations/0018_rename_tag_tables.py index 521db2cb2..9cdb41d06 100644 --- a/netbox/extras/migrations/0018_rename_tag_tables.py +++ b/netbox/extras/migrations/0018_rename_tag_tables.py @@ -28,6 +28,13 @@ class AppTaggitAlterModelTable(migrations.AlterModelTable): class Migration(migrations.Migration): + """ + Rename the tables from taggit_* to extras_* + + Note that while we change the database state, we are not deleting the django + model state for the taggit models. Doing so would result in makemigrations + recreating them. + """ dependencies = [ ('taggit', '0001_initial'), diff --git a/netbox/extras/migrations/0020_add_color_comments_to_tag.py b/netbox/extras/migrations/0019_add_color_comments_to_tag.py similarity index 62% rename from netbox/extras/migrations/0020_add_color_comments_to_tag.py rename to netbox/extras/migrations/0019_add_color_comments_to_tag.py index 0eebd4fbc..5c4b5807c 100644 --- a/netbox/extras/migrations/0020_add_color_comments_to_tag.py +++ b/netbox/extras/migrations/0019_add_color_comments_to_tag.py @@ -7,7 +7,13 @@ import utilities.fields class Migration(migrations.Migration): dependencies = [ - ('extras', '0019_delete_taggit_models'), + ('extras', '0018_rename_tag_tables'), + ('circuits', '0015_custom_tag_models'), + ('dcim', '0070_custom_tag_models'), + ('ipam', '0025_custom_tag_models'), + ('secrets', '0006_custom_tag_models'), + ('tenancy', '0006_custom_tag_models'), + ('virtualization', '0009_custom_tag_models'), ] operations = [ diff --git a/netbox/extras/migrations/0019_delete_taggit_models.py b/netbox/extras/migrations/0019_delete_taggit_models.py deleted file mode 100644 index fadb658e7..000000000 --- a/netbox/extras/migrations/0019_delete_taggit_models.py +++ /dev/null @@ -1,52 +0,0 @@ -# Generated by Django 2.1.4 on 2019-02-20 07:05 - -from django.db import migrations - - -class AppTaggitDeleteModel(migrations.DeleteModel): - """ - A special subclass of DeleteModel which hardcodes the app_label to 'taggit' - - This is needed because the migration deals with models which belong to the taggit - app, however because taggit is a 3rd party app, we cannot create our own migrations - there. - """ - - def state_forwards(self, app_label, state): - super().state_forwards('taggit', state) - - def database_forwards(self, app_label, schema_editor, from_state, to_state): - super().database_forwards('taggit', schema_editor, from_state, to_state) - - def database_backwards(self, app_label, schema_editor, from_state, to_state): - super().database_backwards('taggit', schema_editor, from_state, to_state) - - -class Migration(migrations.Migration): - - dependencies = [ - ('extras', '0018_rename_tag_tables'), - ('circuits', '0015_custom_tag_models'), - ('dcim', '0070_custom_tag_models'), - ('ipam', '0025_custom_tag_models'), - ('secrets', '0006_custom_tag_models'), - ('tenancy', '0006_custom_tag_models'), - ('virtualization', '0009_custom_tag_models'), - ] - - state_operations = [ - AppTaggitDeleteModel( - name='Tag', - ), - AppTaggitDeleteModel( - name='TaggedItem', - ), - ] - - database_operations = [] - operations = [ - migrations.SeparateDatabaseAndState( - database_operations=None, - state_operations=state_operations - ) - ] From b9d11aa4ca32796b17a3da72edc1f250a4950590 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Fri, 22 Feb 2019 01:32:31 -0500 Subject: [PATCH 007/140] refactor tag migrations and add changelog fields to tag --- .../migrations/0015_custom_tag_models.py | 2 +- .../dcim/migrations/0070_custom_tag_models.py | 2 +- .../extras/migrations/0017_tag_taggeditem.py | 9 +-- .../migrations/0018_rename_tag_tables.py | 53 --------------- netbox/extras/migrations/0018_tag_data.py | 65 +++++++++++++++++++ ...19_add_color_comments_changelog_to_tag.py} | 18 +++-- netbox/extras/models.py | 9 ++- .../ipam/migrations/0025_custom_tag_models.py | 2 +- .../migrations/0006_custom_tag_models.py | 2 +- .../migrations/0006_custom_tag_models.py | 2 +- .../migrations/0009_custom_tag_models.py | 2 +- 11 files changed, 90 insertions(+), 76 deletions(-) delete mode 100644 netbox/extras/migrations/0018_rename_tag_tables.py create mode 100644 netbox/extras/migrations/0018_tag_data.py rename netbox/extras/migrations/{0019_add_color_comments_to_tag.py => 0019_add_color_comments_changelog_to_tag.py} (59%) diff --git a/netbox/circuits/migrations/0015_custom_tag_models.py b/netbox/circuits/migrations/0015_custom_tag_models.py index 657b20071..e4926b923 100644 --- a/netbox/circuits/migrations/0015_custom_tag_models.py +++ b/netbox/circuits/migrations/0015_custom_tag_models.py @@ -8,7 +8,7 @@ class Migration(migrations.Migration): dependencies = [ ('circuits', '0014_circuittermination_description'), - ('extras', '0018_rename_tag_tables'), + ('extras', '0017_tag_taggeditem'), ] operations = [ diff --git a/netbox/dcim/migrations/0070_custom_tag_models.py b/netbox/dcim/migrations/0070_custom_tag_models.py index 6b82a0dfd..255b9c3ff 100644 --- a/netbox/dcim/migrations/0070_custom_tag_models.py +++ b/netbox/dcim/migrations/0070_custom_tag_models.py @@ -8,7 +8,7 @@ class Migration(migrations.Migration): dependencies = [ ('dcim', '0069_deprecate_nullablecharfield'), - ('extras', '0018_rename_tag_tables'), + ('extras', '0017_tag_taggeditem'), ] operations = [ diff --git a/netbox/extras/migrations/0017_tag_taggeditem.py b/netbox/extras/migrations/0017_tag_taggeditem.py index 8dbf32a75..91f47240b 100644 --- a/netbox/extras/migrations/0017_tag_taggeditem.py +++ b/netbox/extras/migrations/0017_tag_taggeditem.py @@ -12,7 +12,7 @@ class Migration(migrations.Migration): ('extras', '0016_exporttemplate_add_cable'), ] - state_operations = [ + operations = [ migrations.CreateModel( name='Tag', fields=[ @@ -41,10 +41,3 @@ class Migration(migrations.Migration): index_together={('content_type', 'object_id')}, ), ] - - operations = [ - migrations.SeparateDatabaseAndState( - database_operations=None, - state_operations=state_operations - ) - ] diff --git a/netbox/extras/migrations/0018_rename_tag_tables.py b/netbox/extras/migrations/0018_rename_tag_tables.py deleted file mode 100644 index 9cdb41d06..000000000 --- a/netbox/extras/migrations/0018_rename_tag_tables.py +++ /dev/null @@ -1,53 +0,0 @@ -# Generated by Django 2.1.4 on 2019-02-20 06:59 - -from django.db import migrations - - -class AppTaggitAlterModelTable(migrations.AlterModelTable): - """ - A special subclass of AlterModelTable which hardcodes the app_label to 'taggit' - - This is needed because the migration deals with models which belong to the taggit - app, however because taggit is a 3rd party app, we cannot create our own migrations - there. - """ - - def state_forwards(self, app_label, state): - super().state_forwards('taggit', state) - - def database_forwards(self, app_label, schema_editor, from_state, to_state): - super().database_forwards('taggit', schema_editor, from_state, to_state) - - def database_backwards(self, app_label, schema_editor, from_state, to_state): - super().database_backwards('taggit', schema_editor, from_state, to_state) - - def reduce(self, operation, app_label=None): - if app_label: - app_label = 'taggit' - super().reduce(operation, app_label=app_label) - - -class Migration(migrations.Migration): - """ - Rename the tables from taggit_* to extras_* - - Note that while we change the database state, we are not deleting the django - model state for the taggit models. Doing so would result in makemigrations - recreating them. - """ - - dependencies = [ - ('taggit', '0001_initial'), - ('extras', '0017_tag_taggeditem'), - ] - - operations = [ - AppTaggitAlterModelTable( - name='Tag', - table='extras_tag' - ), - AppTaggitAlterModelTable( - name='TaggedItem', - table='extras_taggeditem' - ), - ] diff --git a/netbox/extras/migrations/0018_tag_data.py b/netbox/extras/migrations/0018_tag_data.py new file mode 100644 index 000000000..86d024a66 --- /dev/null +++ b/netbox/extras/migrations/0018_tag_data.py @@ -0,0 +1,65 @@ +# Generated by Django 2.1.4 on 2019-02-20 06:56 + +from django.db import migrations, models +import django.db.models.deletion +import utilities.fields + + +def copy_tags(apps, schema_editor): + """ + Copy data from taggit_tag to extras_tag + """ + TaggitTag = apps.get_model('taggit', 'Tag') + ExtrasTag = apps.get_model('extras', 'Tag') + + tags_values = TaggitTag.objects.all().values('id', 'name', 'slug') + tags = [ExtrasTag(**tag) for tag in tags_values] + ExtrasTag.objects.bulk_create(tags) + + +def copy_taggeditems(apps, schema_editor): + """ + Copy data from taggit_taggeditem to extras_taggeditem + """ + TaggitTaggedItem = apps.get_model('taggit', 'TaggedItem') + ExtrasTaggedItem = apps.get_model('extras', 'TaggedItem') + + tagged_items_values = TaggitTaggedItem.objects.all().values('id', 'object_id', 'content_type_id', 'tag_id') + tagged_items = [ExtrasTaggedItem(**tagged_item) for tagged_item in tagged_items_values] + ExtrasTaggedItem.objects.bulk_create(tagged_items) + + +def delete_taggit_taggeditems(apps, schema_editor): + """ + Delete all TaggedItem instances from taggit_taggeditem + """ + TaggitTaggedItem = apps.get_model('taggit', 'TaggedItem') + TaggitTaggedItem.objects.all().delete() + + +def delete_taggit_tags(apps, schema_editor): + """ + Delete all Tag instances from taggit_tag + """ + TaggitTag = apps.get_model('taggit', 'Tag') + TaggitTag.objects.all().delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0017_tag_taggeditem'), + ('circuits', '0015_custom_tag_models'), + ('dcim', '0070_custom_tag_models'), + ('ipam', '0025_custom_tag_models'), + ('secrets', '0006_custom_tag_models'), + ('tenancy', '0006_custom_tag_models'), + ('virtualization', '0009_custom_tag_models'), + ] + + operations = [ + migrations.RunPython(copy_tags), + migrations.RunPython(copy_taggeditems), + migrations.RunPython(delete_taggit_taggeditems), + migrations.RunPython(delete_taggit_tags), + ] diff --git a/netbox/extras/migrations/0019_add_color_comments_to_tag.py b/netbox/extras/migrations/0019_add_color_comments_changelog_to_tag.py similarity index 59% rename from netbox/extras/migrations/0019_add_color_comments_to_tag.py rename to netbox/extras/migrations/0019_add_color_comments_changelog_to_tag.py index 5c4b5807c..8a81d85ed 100644 --- a/netbox/extras/migrations/0019_add_color_comments_to_tag.py +++ b/netbox/extras/migrations/0019_add_color_comments_changelog_to_tag.py @@ -7,13 +7,7 @@ import utilities.fields class Migration(migrations.Migration): dependencies = [ - ('extras', '0018_rename_tag_tables'), - ('circuits', '0015_custom_tag_models'), - ('dcim', '0070_custom_tag_models'), - ('ipam', '0025_custom_tag_models'), - ('secrets', '0006_custom_tag_models'), - ('tenancy', '0006_custom_tag_models'), - ('virtualization', '0009_custom_tag_models'), + ('extras', '0018_tag_data'), ] operations = [ @@ -27,4 +21,14 @@ class Migration(migrations.Migration): name='comments', field=models.TextField(blank=True, default=''), ), + migrations.AddField( + model_name='tag', + name='created', + field=models.DateField(auto_now_add=True, null=True), + ), + migrations.AddField( + model_name='tag', + name='last_updated', + field=models.DateTimeField(auto_now=True, null=True), + ), ] diff --git a/netbox/extras/models.py b/netbox/extras/models.py index da8e4744c..b24844089 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -868,8 +868,8 @@ class ObjectChange(models.Model): # Tags # - -class Tag(TagBase): +from utilities.models import ChangeLoggedModel +class Tag(TagBase, ChangeLoggedModel): color = ColorField( default='9e9e9e' ) @@ -885,3 +885,8 @@ class TaggedItem(GenericTaggedItemBase): related_name="%(app_label)s_%(class)s_items", on_delete=models.CASCADE ) + + class Meta: + index_together = ( + ("content_type", "object_id") + ) diff --git a/netbox/ipam/migrations/0025_custom_tag_models.py b/netbox/ipam/migrations/0025_custom_tag_models.py index 05895219c..89075f3ab 100644 --- a/netbox/ipam/migrations/0025_custom_tag_models.py +++ b/netbox/ipam/migrations/0025_custom_tag_models.py @@ -8,7 +8,7 @@ class Migration(migrations.Migration): dependencies = [ ('ipam', '0024_vrf_allow_null_rd'), - ('extras', '0018_rename_tag_tables'), + ('extras', '0017_tag_taggeditem'), ] operations = [ diff --git a/netbox/secrets/migrations/0006_custom_tag_models.py b/netbox/secrets/migrations/0006_custom_tag_models.py index 19699ad05..2f196ad72 100644 --- a/netbox/secrets/migrations/0006_custom_tag_models.py +++ b/netbox/secrets/migrations/0006_custom_tag_models.py @@ -8,7 +8,7 @@ class Migration(migrations.Migration): dependencies = [ ('secrets', '0005_change_logging'), - ('extras', '0018_rename_tag_tables'), + ('extras', '0017_tag_taggeditem'), ] operations = [ diff --git a/netbox/tenancy/migrations/0006_custom_tag_models.py b/netbox/tenancy/migrations/0006_custom_tag_models.py index ceeee437f..9104398ad 100644 --- a/netbox/tenancy/migrations/0006_custom_tag_models.py +++ b/netbox/tenancy/migrations/0006_custom_tag_models.py @@ -8,7 +8,7 @@ class Migration(migrations.Migration): dependencies = [ ('tenancy', '0005_change_logging'), - ('extras', '0018_rename_tag_tables'), + ('extras', '0017_tag_taggeditem'), ] operations = [ diff --git a/netbox/virtualization/migrations/0009_custom_tag_models.py b/netbox/virtualization/migrations/0009_custom_tag_models.py index 02569bee2..883d844f7 100644 --- a/netbox/virtualization/migrations/0009_custom_tag_models.py +++ b/netbox/virtualization/migrations/0009_custom_tag_models.py @@ -8,7 +8,7 @@ class Migration(migrations.Migration): dependencies = [ ('virtualization', '0008_virtualmachine_local_context_data'), - ('extras', '0018_rename_tag_tables'), + ('extras', '0017_tag_taggeditem'), ] operations = [ From de52f219053651314bf759cbe7dfcc2050e43726 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Fri, 22 Feb 2019 01:42:17 -0500 Subject: [PATCH 008/140] fix circular import for ObjectChange, for now... --- netbox/extras/models.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/netbox/extras/models.py b/netbox/extras/models.py index b24844089..285be6c44 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -868,7 +868,10 @@ class ObjectChange(models.Model): # Tags # -from utilities.models import ChangeLoggedModel +# TODO: figure out a way around this circular import for ObjectChange +from utilities.models import ChangeLoggedModel # noqa: E402 + + class Tag(TagBase, ChangeLoggedModel): color = ColorField( default='9e9e9e' From 8e548605c8a2c7d5157d7b60d286dbe0fad4e1b5 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Fri, 22 Feb 2019 02:29:00 -0500 Subject: [PATCH 009/140] added changelog views for Tag --- CHANGELOG.md | 5 +++++ netbox/extras/models.py | 3 +++ netbox/extras/tables.py | 3 +++ netbox/extras/urls.py | 3 +++ netbox/templates/extras/tag.html | 9 +++++++++ 5 files changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30fcca532..074de88d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,15 @@ context data may observe a performance drop when returning multiple objects. To Config Context is not needed, the query parameter `?exclude=config_context` may be added to the request as to remove the Config Context from being included in any results. +### Tag Permissions Changed + +NetBox now makes use of its own `Tag` model instead of the vanilla model which ships with django-taggit. This new model lives in the `extras` app and thus any permissions that you may have configured using "Taggit | Tag" should be changed to now use "Extras | Tag." + ## Enhancements * [#2324](https://github.com/digitalocean/netbox/issues/2324) - Add color option for tags * [#2791](https://github.com/digitalocean/netbox/issues/2791) - Add a comment field for tags +* [#2926](https://github.com/digitalocean/netbox/issues/2926) - Add changelog to the Tag model --- diff --git a/netbox/extras/models.py b/netbox/extras/models.py index 285be6c44..9d907c84e 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -881,6 +881,9 @@ class Tag(TagBase, ChangeLoggedModel): default='' ) + def get_absolute_url(self): + return reverse('extras:tag', args=[self.slug]) + class TaggedItem(GenericTaggedItemBase): tag = models.ForeignKey( diff --git a/netbox/extras/tables.py b/netbox/extras/tables.py index e646ad821..d1454db4f 100644 --- a/netbox/extras/tables.py +++ b/netbox/extras/tables.py @@ -5,6 +5,9 @@ from utilities.tables import BaseTable, BooleanColumn, ColorColumn, ToggleColumn from .models import ConfigContext, ObjectChange, Tag, TaggedItem TAG_ACTIONS = """ + + + {% if perms.taggit.change_tag %} {% endif %} diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py index 35a6fb110..d475ab5a3 100644 --- a/netbox/extras/urls.py +++ b/netbox/extras/urls.py @@ -1,6 +1,8 @@ from django.conf.urls import url from extras import views +from extras.models import Tag + app_name = 'extras' urlpatterns = [ @@ -11,6 +13,7 @@ urlpatterns = [ url(r'^tags/(?P[\w-]+)/$', views.TagView.as_view(), name='tag'), url(r'^tags/(?P[\w-]+)/edit/$', views.TagEditView.as_view(), name='tag_edit'), url(r'^tags/(?P[\w-]+)/delete/$', views.TagDeleteView.as_view(), name='tag_delete'), + url(r'^tags/(?P[\w-]+)/changelog/$', views.ObjectChangeLogView.as_view(), name='tag_changelog', kwargs={'model': Tag}), # Config contexts url(r'^config-contexts/$', views.ConfigContextListView.as_view(), name='configcontext_list'), diff --git a/netbox/templates/extras/tag.html b/netbox/templates/extras/tag.html index 49e67c395..c8bd8442a 100644 --- a/netbox/templates/extras/tag.html +++ b/netbox/templates/extras/tag.html @@ -31,6 +31,15 @@ {% endif %}

{% block title %}Tag: {{ tag }}{% endblock %}

+ {% include 'inc/created_updated.html' with obj=tag %} + {% endblock %} {% block content %} From 231a5aa9fd9211c9dcf57accb2349f86e719791a Mon Sep 17 00:00:00 2001 From: John Anderson Date: Fri, 1 Mar 2019 23:16:08 -0500 Subject: [PATCH 010/140] v2.6.0-dev version update --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index a85a5d78e..2cf1e1f0a 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -22,7 +22,7 @@ except ImportError: ) -VERSION = '2.5.8-dev' +VERSION = '2.6.0-dev' BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) From 80b3a5ffc46b9262e709d622c7d6a9268c9cac4c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 4 Mar 2019 15:57:21 -0500 Subject: [PATCH 011/140] Fixed erroneous addition of 'description' field to unique_together --- netbox/dcim/models.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 7fb789235..541fd646e 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -1843,11 +1843,11 @@ class PowerPort(CableTermination, ComponentModel): objects = DeviceComponentManager() tags = TaggableManager(through=TaggedItem) - csv_headers = ['device', 'name'] + csv_headers = ['device', 'name', 'description'] class Meta: ordering = ['device', 'name'] - unique_together = ['device', 'name', 'description'] + unique_together = ['device', 'name'] def __str__(self): return self.name @@ -1887,10 +1887,10 @@ class PowerOutlet(CableTermination, ComponentModel): objects = DeviceComponentManager() tags = TaggableManager(through=TaggedItem) - csv_headers = ['device', 'name'] + csv_headers = ['device', 'name', 'description'] class Meta: - unique_together = ['device', 'name', 'description'] + unique_together = ['device', 'name'] def __str__(self): return self.name From b9f4a9e57bc8c6fb83ccf5253fa35df895127c0c Mon Sep 17 00:00:00 2001 From: Grokzen Date: Mon, 4 Mar 2019 21:58:40 +0100 Subject: [PATCH 012/140] Closes #1792 - Add CustomFieldChoices API endpoint (#2941) * Add new api endpoint for CustomFieldChoices * Add changelog item for #1792 * Add tests for CustomFieldchoiceAPI endpoint --- CHANGELOG.md | 1 + netbox/extras/api/urls.py | 3 ++ netbox/extras/api/views.py | 36 ++++++++++++++++++++++-- netbox/extras/tests/test_customfields.py | 33 +++++++++++++++++++++- 4 files changed, 70 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00615d515..3a803df20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ to now use "Extras | Tag." * [#2643](https://github.com/digitalocean/netbox/issues/2643) - Add `description` field to console/power components and device bays * [#2791](https://github.com/digitalocean/netbox/issues/2791) - Add a comment field for tags * [#2926](https://github.com/digitalocean/netbox/issues/2926) - Add changelog to the Tag model +* [#1792](https://github.com/digitalocean/netbox/issues/1792) - Add CustomFieldChoices API endpoint --- diff --git a/netbox/extras/api/urls.py b/netbox/extras/api/urls.py index 1bdcf181b..c135280ea 100644 --- a/netbox/extras/api/urls.py +++ b/netbox/extras/api/urls.py @@ -17,6 +17,9 @@ router.APIRootView = ExtrasRootView # Field choices router.register(r'_choices', views.ExtrasFieldChoicesViewSet, basename='field-choice') +# Custom field choices +router.register(r'_custom_field_choices', views.CustomFieldChoicesViewSet, base_name='custom-field-choice') + # Graphs router.register(r'graphs', views.GraphViewSet) diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index 17070c7b4..b964b22da 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -1,3 +1,5 @@ +from collections import OrderedDict + from django.contrib.contenttypes.models import ContentType from django.db.models import Count from django.http import Http404, HttpResponse @@ -9,8 +11,8 @@ from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet from extras import filters from extras.models import ( - ConfigContext, CustomField, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap, - Tag + ConfigContext, CustomField, CustomFieldChoice, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap, + Tag, ) from extras.reports import get_report, get_reports from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet @@ -28,6 +30,36 @@ class ExtrasFieldChoicesViewSet(FieldChoicesViewSet): ) +# +# Custom field choices +# + +class CustomFieldChoicesViewSet(ViewSet): + """ + """ + permission_classes = [IsAuthenticatedOrLoginNotRequired] + + def __init__(self, *args, **kwargs): + super(CustomFieldChoicesViewSet, self).__init__(*args, **kwargs) + + self._fields = OrderedDict() + + for cfc in CustomFieldChoice.objects.all(): + self._fields.setdefault(cfc.field.name, {}) + self._fields[cfc.field.name][cfc.value] = cfc.pk + + def list(self, request): + return Response(self._fields) + + def retrieve(self, request, pk): + if pk not in self._fields: + raise Http404 + return Response(self._fields[pk]) + + def get_view_name(self): + return "Custom Field choices" + + # # Custom fields # diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py index b02e787c1..96f3483bc 100644 --- a/netbox/extras/tests/test_customfields.py +++ b/netbox/extras/tests/test_customfields.py @@ -6,9 +6,10 @@ from django.urls import reverse from rest_framework import status from dcim.models import Site -from extras.constants import CF_TYPE_TEXT, CF_TYPE_INTEGER, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_SELECT, CF_TYPE_URL +from extras.constants import CF_TYPE_TEXT, CF_TYPE_INTEGER, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_SELECT, CF_TYPE_URL, CF_TYPE_SELECT from extras.models import CustomField, CustomFieldValue, CustomFieldChoice from utilities.testing import APITestCase +from virtualization.models import VirtualMachine class CustomFieldTest(TestCase): @@ -299,3 +300,33 @@ class CustomFieldAPITest(APITestCase): self.assertEqual(response.data['custom_fields'].get('magic_choice'), data['custom_fields']['magic_choice']) cfv = self.site.custom_field_values.get(field=self.cf_select) self.assertEqual(cfv.value.pk, data['custom_fields']['magic_choice']) + + +class CustomFieldChoiceAPITest(APITestCase): + def setUp(self): + super().setUp() + + vm_content_type = ContentType.objects.get_for_model(VirtualMachine) + + self.cf_1 = CustomField.objects.create(name="cf_1", type=CF_TYPE_SELECT) + self.cf_2 = CustomField.objects.create(name="cf_2", type=CF_TYPE_SELECT) + + self.cf_choice_1 = CustomFieldChoice.objects.create(field=self.cf_1, value="cf_field_1", weight=100) + self.cf_choice_2 = CustomFieldChoice.objects.create(field=self.cf_1, value="cf_field_2", weight=50) + self.cf_choice_3 = CustomFieldChoice.objects.create(field=self.cf_2, value="cf_field_3", weight=10) + + def test_list_cfc(self): + url = reverse('extras-api:custom-field-choice-list') + response = self.client.get(url, **self.header) + + self.assertEqual(len(response.data), 2) + self.assertEqual(len(response.data[self.cf_1.name]), 2) + self.assertEqual(len(response.data[self.cf_2.name]), 1) + + self.assertTrue(self.cf_choice_1.value in response.data[self.cf_1.name]) + self.assertTrue(self.cf_choice_2.value in response.data[self.cf_1.name]) + self.assertTrue(self.cf_choice_3.value in response.data[self.cf_2.name]) + + self.assertEqual(self.cf_choice_1.pk, response.data[self.cf_1.name][self.cf_choice_1.value]) + self.assertEqual(self.cf_choice_2.pk, response.data[self.cf_1.name][self.cf_choice_2.value]) + self.assertEqual(self.cf_choice_3.pk, response.data[self.cf_2.name][self.cf_choice_3.value]) From 66d9d9d9cbbceaa7b1e7efcaecd9e9c988f7f8fb Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 4 Mar 2019 16:01:43 -0500 Subject: [PATCH 013/140] Rearranged changelog items --- CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a803df20..52230def4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,11 +18,11 @@ to now use "Extras | Tag." ## Enhancements -* [#2324](https://github.com/digitalocean/netbox/issues/2324) - Add color option for tags -* [#2643](https://github.com/digitalocean/netbox/issues/2643) - Add `description` field to console/power components and device bays -* [#2791](https://github.com/digitalocean/netbox/issues/2791) - Add a comment field for tags -* [#2926](https://github.com/digitalocean/netbox/issues/2926) - Add changelog to the Tag model * [#1792](https://github.com/digitalocean/netbox/issues/1792) - Add CustomFieldChoices API endpoint +* [#2324](https://github.com/digitalocean/netbox/issues/2324) - Add `color` option for tags +* [#2643](https://github.com/digitalocean/netbox/issues/2643) - Add `description` field to console/power components and device bays +* [#2791](https://github.com/digitalocean/netbox/issues/2791) - Add a `comment` field for tags +* [#2926](https://github.com/digitalocean/netbox/issues/2926) - Add changelog to the Tag model --- From 8a1d7fdb371ae3a51309150ff6a07224db15ab18 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 5 Mar 2019 13:46:37 -0500 Subject: [PATCH 014/140] Updated migrations to account for extras/0017_exporttemplate_mime_type_length --- netbox/circuits/migrations/0015_custom_tag_models.py | 2 +- netbox/dcim/migrations/0070_custom_tag_models.py | 2 +- .../{0017_tag_taggeditem.py => 0018_tag_taggeditem.py} | 2 +- netbox/extras/migrations/{0018_tag_data.py => 0019_tag_data.py} | 2 +- ...og_to_tag.py => 0020_add_color_comments_changelog_to_tag.py} | 2 +- netbox/ipam/migrations/0025_custom_tag_models.py | 2 +- netbox/secrets/migrations/0006_custom_tag_models.py | 2 +- netbox/tenancy/migrations/0006_custom_tag_models.py | 2 +- netbox/virtualization/migrations/0009_custom_tag_models.py | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) rename netbox/extras/migrations/{0017_tag_taggeditem.py => 0018_tag_taggeditem.py} (96%) rename netbox/extras/migrations/{0018_tag_data.py => 0019_tag_data.py} (97%) rename netbox/extras/migrations/{0019_add_color_comments_changelog_to_tag.py => 0020_add_color_comments_changelog_to_tag.py} (95%) diff --git a/netbox/circuits/migrations/0015_custom_tag_models.py b/netbox/circuits/migrations/0015_custom_tag_models.py index e4926b923..d00eb36d7 100644 --- a/netbox/circuits/migrations/0015_custom_tag_models.py +++ b/netbox/circuits/migrations/0015_custom_tag_models.py @@ -8,7 +8,7 @@ class Migration(migrations.Migration): dependencies = [ ('circuits', '0014_circuittermination_description'), - ('extras', '0017_tag_taggeditem'), + ('extras', '0018_tag_taggeditem'), ] operations = [ diff --git a/netbox/dcim/migrations/0070_custom_tag_models.py b/netbox/dcim/migrations/0070_custom_tag_models.py index 255b9c3ff..e3b54b9cc 100644 --- a/netbox/dcim/migrations/0070_custom_tag_models.py +++ b/netbox/dcim/migrations/0070_custom_tag_models.py @@ -8,7 +8,7 @@ class Migration(migrations.Migration): dependencies = [ ('dcim', '0069_deprecate_nullablecharfield'), - ('extras', '0017_tag_taggeditem'), + ('extras', '0018_tag_taggeditem'), ] operations = [ diff --git a/netbox/extras/migrations/0017_tag_taggeditem.py b/netbox/extras/migrations/0018_tag_taggeditem.py similarity index 96% rename from netbox/extras/migrations/0017_tag_taggeditem.py rename to netbox/extras/migrations/0018_tag_taggeditem.py index 91f47240b..11a3a50a5 100644 --- a/netbox/extras/migrations/0017_tag_taggeditem.py +++ b/netbox/extras/migrations/0018_tag_taggeditem.py @@ -9,7 +9,7 @@ class Migration(migrations.Migration): dependencies = [ ('contenttypes', '0002_remove_content_type_name'), - ('extras', '0016_exporttemplate_add_cable'), + ('extras', '0017_exporttemplate_mime_type_length'), ] operations = [ diff --git a/netbox/extras/migrations/0018_tag_data.py b/netbox/extras/migrations/0019_tag_data.py similarity index 97% rename from netbox/extras/migrations/0018_tag_data.py rename to netbox/extras/migrations/0019_tag_data.py index 86d024a66..af4641daa 100644 --- a/netbox/extras/migrations/0018_tag_data.py +++ b/netbox/extras/migrations/0019_tag_data.py @@ -48,7 +48,7 @@ def delete_taggit_tags(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('extras', '0017_tag_taggeditem'), + ('extras', '0018_tag_taggeditem'), ('circuits', '0015_custom_tag_models'), ('dcim', '0070_custom_tag_models'), ('ipam', '0025_custom_tag_models'), diff --git a/netbox/extras/migrations/0019_add_color_comments_changelog_to_tag.py b/netbox/extras/migrations/0020_add_color_comments_changelog_to_tag.py similarity index 95% rename from netbox/extras/migrations/0019_add_color_comments_changelog_to_tag.py rename to netbox/extras/migrations/0020_add_color_comments_changelog_to_tag.py index 8a81d85ed..33171db98 100644 --- a/netbox/extras/migrations/0019_add_color_comments_changelog_to_tag.py +++ b/netbox/extras/migrations/0020_add_color_comments_changelog_to_tag.py @@ -7,7 +7,7 @@ import utilities.fields class Migration(migrations.Migration): dependencies = [ - ('extras', '0018_tag_data'), + ('extras', '0019_tag_data'), ] operations = [ diff --git a/netbox/ipam/migrations/0025_custom_tag_models.py b/netbox/ipam/migrations/0025_custom_tag_models.py index 89075f3ab..a47335b58 100644 --- a/netbox/ipam/migrations/0025_custom_tag_models.py +++ b/netbox/ipam/migrations/0025_custom_tag_models.py @@ -8,7 +8,7 @@ class Migration(migrations.Migration): dependencies = [ ('ipam', '0024_vrf_allow_null_rd'), - ('extras', '0017_tag_taggeditem'), + ('extras', '0018_tag_taggeditem'), ] operations = [ diff --git a/netbox/secrets/migrations/0006_custom_tag_models.py b/netbox/secrets/migrations/0006_custom_tag_models.py index 2f196ad72..8e5fc1b80 100644 --- a/netbox/secrets/migrations/0006_custom_tag_models.py +++ b/netbox/secrets/migrations/0006_custom_tag_models.py @@ -8,7 +8,7 @@ class Migration(migrations.Migration): dependencies = [ ('secrets', '0005_change_logging'), - ('extras', '0017_tag_taggeditem'), + ('extras', '0018_tag_taggeditem'), ] operations = [ diff --git a/netbox/tenancy/migrations/0006_custom_tag_models.py b/netbox/tenancy/migrations/0006_custom_tag_models.py index 9104398ad..92e333520 100644 --- a/netbox/tenancy/migrations/0006_custom_tag_models.py +++ b/netbox/tenancy/migrations/0006_custom_tag_models.py @@ -8,7 +8,7 @@ class Migration(migrations.Migration): dependencies = [ ('tenancy', '0005_change_logging'), - ('extras', '0017_tag_taggeditem'), + ('extras', '0018_tag_taggeditem'), ] operations = [ diff --git a/netbox/virtualization/migrations/0009_custom_tag_models.py b/netbox/virtualization/migrations/0009_custom_tag_models.py index 883d844f7..3f70d5469 100644 --- a/netbox/virtualization/migrations/0009_custom_tag_models.py +++ b/netbox/virtualization/migrations/0009_custom_tag_models.py @@ -8,7 +8,7 @@ class Migration(migrations.Migration): dependencies = [ ('virtualization', '0008_virtualmachine_local_context_data'), - ('extras', '0017_tag_taggeditem'), + ('extras', '0018_tag_taggeditem'), ] operations = [ From d9c8c0cbc10a92e3a1b104e959cf20a8b9eb3bb7 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 8 Mar 2019 10:25:09 -0500 Subject: [PATCH 015/140] Fixes #2968: Correct API documentation for SerializerMethodFields --- CHANGELOG.md | 4 ++++ netbox/dcim/api/serializers.py | 7 +++++++ netbox/extras/api/serializers.py | 3 +++ netbox/netbox/settings.py | 1 + netbox/virtualization/api/serializers.py | 2 ++ 5 files changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e6c3d361..fbbba1907 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,10 @@ to now use "Extras | Tag." * [#2791](https://github.com/digitalocean/netbox/issues/2791) - Add a `comment` field for tags * [#2926](https://github.com/digitalocean/netbox/issues/2926) - Add changelog to the Tag model +## Bug Fixes + +* [#2968](https://github.com/digitalocean/netbox/issues/2968) - Correct API documentation for SerializerMethodFields + --- v2.5.8 (FUTURE) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 969308ba1..d18c59be5 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -1,3 +1,4 @@ +from drf_yasg.utils import swagger_serializer_method from rest_framework import serializers from rest_framework.validators import UniqueTogetherValidator from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField @@ -35,6 +36,7 @@ class ConnectedEndpointSerializer(ValidatedModelSerializer): ) return None + @swagger_serializer_method(serializer_or_field=serializers.DictField) def get_connected_endpoint(self, obj): """ Return the appropriate serializer for the type of connected object. @@ -312,6 +314,7 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer): return data + @swagger_serializer_method(serializer_or_field=NestedDeviceSerializer) def get_parent_device(self, obj): try: device_bay = obj.parent_bay @@ -334,6 +337,7 @@ class DeviceWithConfigContextSerializer(DeviceSerializer): 'custom_fields', 'config_context', 'created', 'last_updated', ] + @swagger_serializer_method(serializer_or_field=serializers.DictField) def get_config_context(self, obj): return obj.get_config_context() @@ -531,9 +535,11 @@ class CableSerializer(ValidatedModelSerializer): return data + @swagger_serializer_method(serializer_or_field=serializers.DictField) def get_termination_a(self, obj): return self._get_termination(obj, 'a') + @swagger_serializer_method(serializer_or_field=serializers.DictField) def get_termination_b(self, obj): return self._get_termination(obj, 'b') @@ -564,6 +570,7 @@ class InterfaceConnectionSerializer(ValidatedModelSerializer): model = Interface fields = ['interface_a', 'interface_b', 'connection_status'] + @swagger_serializer_method(serializer_or_field=NestedInterfaceSerializer) def get_interface_a(self, obj): context = {'request': self.context['request']} return NestedInterfaceSerializer(instance=obj, context=context).data diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 9d24589b0..6533b71ff 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -1,4 +1,5 @@ from django.core.exceptions import ObjectDoesNotExist +from drf_yasg.utils import swagger_serializer_method from rest_framework import serializers from dcim.api.nested_serializers import ( @@ -112,6 +113,7 @@ class ImageAttachmentSerializer(ValidatedModelSerializer): return data + @swagger_serializer_method(serializer_or_field=serializers.DictField) def get_parent(self, obj): # Static mapping of models to their nested serializers @@ -215,6 +217,7 @@ class ObjectChangeSerializer(serializers.ModelSerializer): 'id', 'time', 'user', 'user_name', 'request_id', 'action', 'content_type', 'changed_object', 'object_data', ] + @swagger_serializer_method(serializer_or_field=serializers.DictField) def get_changed_object(self, obj): """ Serialize a nested representation of the changed object. diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 08ded2b63..08bd63588 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -308,6 +308,7 @@ SWAGGER_SETTINGS = { 'drf_yasg.inspectors.ChoiceFieldInspector', 'drf_yasg.inspectors.FileFieldInspector', 'drf_yasg.inspectors.DictFieldInspector', + 'drf_yasg.inspectors.SerializerMethodFieldInspector', 'drf_yasg.inspectors.SimpleFieldInspector', 'drf_yasg.inspectors.StringDefaultFieldInspector', ], diff --git a/netbox/virtualization/api/serializers.py b/netbox/virtualization/api/serializers.py index 1b06dab3b..86953cadc 100644 --- a/netbox/virtualization/api/serializers.py +++ b/netbox/virtualization/api/serializers.py @@ -1,3 +1,4 @@ +from drf_yasg.utils import swagger_serializer_method from rest_framework import serializers from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField @@ -80,6 +81,7 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer): 'config_context', 'created', 'last_updated', ] + @swagger_serializer_method(serializer_or_field=serializers.DictField) def get_config_context(self, obj): return obj.get_config_context() From 5b753923b61748c947877bd0db5569c906a3e895 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 11 Mar 2019 22:40:52 -0400 Subject: [PATCH 016/140] Initial work on power modeling (WIP) --- netbox/dcim/constants.py | 30 ++++ netbox/dcim/forms.py | 170 ++++++++++++++++++++- netbox/dcim/migrations/0072_powerfeeds.py | 74 +++++++++ netbox/dcim/models.py | 131 ++++++++++++++++ netbox/dcim/tables.py | 50 +++++- netbox/dcim/urls.py | 22 ++- netbox/dcim/views.py | 115 +++++++++++++- netbox/templates/dcim/powerfeed.html | 111 ++++++++++++++ netbox/templates/dcim/powerfeed_edit.html | 45 ++++++ netbox/templates/dcim/powerfeed_list.html | 22 +++ netbox/templates/dcim/powerpanel_list.html | 18 +++ netbox/templates/inc/nav_menu.html | 23 +++ 12 files changed, 804 insertions(+), 7 deletions(-) create mode 100644 netbox/dcim/migrations/0072_powerfeeds.py create mode 100644 netbox/templates/dcim/powerfeed.html create mode 100644 netbox/templates/dcim/powerfeed_edit.html create mode 100644 netbox/templates/dcim/powerfeed_list.html create mode 100644 netbox/templates/dcim/powerpanel_list.html diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 0b81e68bf..e68493af6 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -443,3 +443,33 @@ RACK_DIMENSION_UNIT_CHOICES = ( (LENGTH_UNIT_MILLIMETER, 'Millimeters'), (LENGTH_UNIT_INCH, 'Inches'), ) + +# Power feeds +POWERFEED_TYPE_PRIMARY = 1 +POWERFEED_TYPE_REDUNDANT = 2 +POWERFEED_TYPE_CHOICES = ( + (POWERFEED_TYPE_PRIMARY, 'AC'), + (POWERFEED_TYPE_REDUNDANT, 'DC'), +) +POWERFEED_SUPPLY_AC = 1 +POWERFEED_SUPPLY_DC = 2 +POWERFEED_SUPPLY_CHOICES = ( + (POWERFEED_SUPPLY_AC, 'AC'), + (POWERFEED_SUPPLY_DC, 'DC'), +) +POWERFEED_PHASE_SINGLE = 1 +POWERFEED_PHASE_3PHASE = 3 +POWERFEED_PHASE_CHOICES = ( + (POWERFEED_PHASE_SINGLE, 'Single phase'), + (POWERFEED_PHASE_3PHASE, 'Three-phase'), +) +POWERFEED_STATUS_OFFLINE = 0 +POWERFEED_STATUS_ACTIVE = 1 +POWERFEED_STATUS_PLANNED = 2 +POWERFEED_STATUS_FAILED = 4 +POWERFEED_STATUS_CHOICES = ( + (POWERFEED_STATUS_ACTIVE, 'Active'), + (POWERFEED_STATUS_OFFLINE, 'Offline'), + (POWERFEED_STATUS_PLANNED, 'Planned'), + (POWERFEED_STATUS_FAILED, 'Failed'), +) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index ab3da181f..8e429137a 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -26,8 +26,8 @@ from .constants import * from .models import ( Cable, DeviceBay, DeviceBayTemplate, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, Manufacturer, - InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, - RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis + InventoryItem, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort, PowerPortTemplate, + Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis, ) DEVICE_BY_PK_RE = r'{\d+\}' @@ -3156,3 +3156,169 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm): to_field_name='slug', null_label='-- None --', ) + + +# +# Power panels +# + +class PowerPanelForm(BootstrapMixin, forms.ModelForm): + rackgroup = ChainedModelChoiceField( + queryset=RackGroup.objects.all(), + chains=( + ('site', 'site'), + ), + required=False, + widget=APISelect( + api_url='/api/dcim/rack-groups/', + ) + ) + + class Meta: + model = PowerPanel + fields = [ + 'site', 'rackgroup', 'name', + ] + widgets = { + 'site': APISelect( + api_url="/api/dcim/sites/", + filter_for={ + 'rackgroup': 'site_id', + } + ), + } + + +class PowerPanelCSVForm(forms.ModelForm): + site = forms.ModelChoiceField( + queryset=Site.objects.all(), + to_field_name='name', + help_text='Name of parent site', + error_messages={ + 'invalid_choice': 'Site not found.', + } + ) + group_name = forms.CharField( + help_text='Name of rack group', + required=False + ) + + class Meta: + model = PowerPanel + fields = PowerPanel.csv_headers + + +# +# Power feeds +# + +class PowerFeedForm(BootstrapMixin, CustomFieldForm): + tags = TagField( + required=False + ) + + class Meta: + model = PowerFeed + fields = [ + 'powerpanel', 'rack', 'name', 'type', 'status', 'supply', 'voltage', 'amperage', 'phase', 'max_utilization', + 'comments', 'tags', + ] + widgets = { + 'site': APISelect( + api_url="/api/dcim/sites/", + filter_for={ + 'rackgroup': 'site_id', + } + ), + 'type': StaticSelect2(), + 'status': StaticSelect2(), + 'supply': StaticSelect2(), + 'phase': StaticSelect2(), + } + + +class PowerFeedCSVForm(forms.ModelForm): + type = CSVChoiceField( + choices=POWERFEED_TYPE_CHOICES, + required=False, + help_text='Primary or redundant' + ) + status = CSVChoiceField( + choices=POWERFEED_STATUS_CHOICES, + required=False, + help_text='Operational status' + ) + supply = CSVChoiceField( + choices=POWERFEED_SUPPLY_CHOICES, + required=False, + help_text='AC/DC' + ) + + class Meta: + model = PowerFeed + fields = PowerFeed.csv_headers + + +class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=PowerFeed.objects.all(), + widget=forms.MultipleHiddenInput + ) + powerpanel = forms.ModelChoiceField( + queryset=PowerPanel.objects.all(), + required=False, + widget=APISelect( + api_url="/api/dcim/sites", + filter_for={ + 'rackgroup': 'site_id', + } + ) + ) + rackgroup = forms.ModelChoiceField( + queryset=RackGroup.objects.all(), + required=False, + widget=APISelect( + api_url="/api/dcim/rack-groups", + ) + ) + type = forms.ChoiceField( + choices=add_blank_choice(POWERFEED_TYPE_CHOICES), + required=False, + initial='', + widget=StaticSelect2() + ) + status = forms.ChoiceField( + choices=add_blank_choice(POWERFEED_STATUS_CHOICES), + required=False, + initial='', + widget=StaticSelect2() + ) + supply = forms.ChoiceField( + choices=add_blank_choice(POWERFEED_SUPPLY_CHOICES), + required=False, + initial='', + widget=StaticSelect2() + ) + voltage = forms.IntegerField( + required=False + ) + amperage = forms.IntegerField( + required=False + ) + phase = forms.ChoiceField( + choices=add_blank_choice(POWERFEED_PHASE_CHOICES), + required=False, + initial='', + widget=StaticSelect2() + ) + max_utilization = forms.IntegerField( + required=False + ) + comments = forms.CharField( + required=False + ) + + class Meta: + nullable_fields = [ + 'rackgroup', 'comments', + ] diff --git a/netbox/dcim/migrations/0072_powerfeeds.py b/netbox/dcim/migrations/0072_powerfeeds.py new file mode 100644 index 000000000..ddfbb8ec1 --- /dev/null +++ b/netbox/dcim/migrations/0072_powerfeeds.py @@ -0,0 +1,74 @@ +# Generated by Django 2.1.7 on 2019-03-12 02:29 + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import taggit.managers + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0020_add_color_comments_changelog_to_tag'), + ('dcim', '0071_device_components_add_description'), + ] + + operations = [ + migrations.CreateModel( + name='PowerFeed', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)), + ('created', models.DateField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('name', models.CharField(max_length=50)), + ('type', models.PositiveSmallIntegerField(default=1)), + ('status', models.PositiveSmallIntegerField(default=1)), + ('supply', models.PositiveSmallIntegerField(default=1)), + ('voltage', models.PositiveSmallIntegerField(default=120, validators=[django.core.validators.MinValueValidator(1)])), + ('amperage', models.PositiveSmallIntegerField(default=20, validators=[django.core.validators.MinValueValidator(1)])), + ('phase', models.PositiveSmallIntegerField(default=1)), + ('max_utilization', models.PositiveSmallIntegerField(default=80, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)])), + ('comments', models.TextField(blank=True)), + ], + options={ + 'ordering': ['powerpanel', 'name'], + }, + ), + migrations.CreateModel( + name='PowerPanel', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)), + ('created', models.DateField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('name', models.CharField(max_length=50)), + ('rackgroup', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.RackGroup')), + ('site', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='dcim.Site')), + ], + options={ + 'ordering': ['site', 'name'], + }, + ), + migrations.AddField( + model_name='powerfeed', + name='powerpanel', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='powerfeeds', to='dcim.PowerPanel'), + ), + migrations.AddField( + model_name='powerfeed', + name='rack', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.Rack'), + ), + migrations.AddField( + model_name='powerfeed', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AlterUniqueTogether( + name='powerpanel', + unique_together={('site', 'name')}, + ), + migrations.AlterUniqueTogether( + name='powerfeed', + unique_together={('powerpanel', 'name')}, + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index cbfce0b91..ad7e198fb 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -2668,3 +2668,134 @@ class Cable(ChangeLoggedModel): b_endpoint = b_path[-1][2] return a_endpoint, b_endpoint, path_status + + +# +# Power +# + +class PowerPanel(ChangeLoggedModel): + """ + A distribution point for electrical power; e.g. a data center RPP. + """ + site = models.ForeignKey( + to='Site', + on_delete=models.PROTECT + ) + rackgroup = models.ForeignKey( + to='RackGroup', + on_delete=models.PROTECT, + blank=True, + null=True + ) + name = models.CharField( + max_length=50 + ) + + csv_headers = ['site', 'rackgroup', 'name'] + + class Meta: + ordering = ['site', 'name'] + unique_together = ['site', 'name'] + + def __str__(self): + return self.name + + def get_absolute_url(self): + return reverse('dcim:powerpanel', args=[self.pk]) + + def to_csv(self): + return ( + self.site.name, + self.rackgroup.name if self.rackgroup else None, + self.name, + ) + + +class PowerFeed(ChangeLoggedModel, CustomFieldModel): + """ + An electrical circuit delivered from a PowerPanel. + """ + powerpanel = models.ForeignKey( + to='PowerPanel', + on_delete=models.PROTECT, + related_name='powerfeeds' + ) + rack = models.ForeignKey( + to='Rack', + on_delete=models.PROTECT, + blank=True, + null=True + ) + name = models.CharField( + max_length=50 + ) + type = models.PositiveSmallIntegerField( + choices=POWERFEED_TYPE_CHOICES, + default=POWERFEED_TYPE_PRIMARY + ) + status = models.PositiveSmallIntegerField( + choices=POWERFEED_STATUS_CHOICES, + default=POWERFEED_STATUS_ACTIVE + ) + supply = models.PositiveSmallIntegerField( + choices=POWERFEED_SUPPLY_CHOICES, + default=POWERFEED_SUPPLY_AC + ) + voltage = models.PositiveSmallIntegerField( + validators=[MinValueValidator(1)], + default=120 + ) + amperage = models.PositiveSmallIntegerField( + validators=[MinValueValidator(1)], + default=20 + ) + phase = models.PositiveSmallIntegerField( + choices=POWERFEED_PHASE_CHOICES, + default=POWERFEED_PHASE_SINGLE + ) + max_utilization = models.PositiveSmallIntegerField( + validators=[MinValueValidator(1), MaxValueValidator(100)], + default=80, + help_text="Maximum permissible draw (percentage)" + ) + comments = models.TextField( + blank=True + ) + custom_field_values = GenericRelation( + to='extras.CustomFieldValue', + content_type_field='obj_type', + object_id_field='obj_id' + ) + + tags = TaggableManager(through=TaggedItem) + + csv_headers = [ + 'powerpanel', 'rack', 'name', 'type', 'status', 'supply', 'voltage', 'amperage', 'phase', 'max_utilization', + 'comments', + ] + + class Meta: + ordering = ['powerpanel', 'name'] + unique_together = ['powerpanel', 'name'] + + def __str__(self): + return self.name + + def get_absolute_url(self): + return reverse('dcim:powerfeed', args=[self.pk]) + + def to_csv(self): + return ( + self.powerpanel.name, + self.rack.name if self.rack else None, + self.name, + self.get_type_display(), + self.get_status_display(), + self.get_supply_display(), + self.voltage, + self.amperage, + self.get_phase_display(), + self.max_utilization, + self.comments, + ) diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 436b9053d..8106d997a 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -6,8 +6,9 @@ from utilities.tables import BaseTable, BooleanColumn, ColorColumn, ToggleColumn from .models import ( Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, - InventoryItem, Manufacturer, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, - RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis, + InventoryItem, Manufacturer, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort, + PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, + VirtualChassis, ) REGION_LINK = """ @@ -786,3 +787,48 @@ class VirtualChassisTable(BaseTable): class Meta(BaseTable.Meta): model = VirtualChassis fields = ('pk', 'master', 'domain', 'member_count', 'actions') + + +# +# Power panels +# + +class PowerPanelTable(BaseTable): + pk = ToggleColumn() + name = tables.LinkColumn() + powerfeed_count = tables.Column( + verbose_name='Feeds' + ) + actions = tables.TemplateColumn( + template_code=RACKROLE_ACTIONS, + attrs={ + 'td': {'class': 'text-right noprint'} + }, + verbose_name='' + ) + + class Meta(BaseTable.Meta): + model = PowerPanel + fields = ('pk', 'name', 'site', 'rackgroup', 'powerfeed_count', 'actions') + + +# +# Power feeds +# + +class PowerFeedTable(BaseTable): + pk = ToggleColumn() + name = tables.LinkColumn() + powerpanel = tables.LinkColumn( + viewname='dcim:powerpanel', + args=[Accessor('powerpanel.pk')], + + ) + rack = tables.LinkColumn( + viewname='dcim:rack', + accessor=Accessor('rack.pk') + ) + + class Meta(BaseTable.Meta): + model = PowerFeed + fields = ('pk', 'name', 'powerpanel', 'rack', 'type', 'status', 'supply', 'voltage', 'amperage', 'phase') diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 21d620af1..a087a0ae9 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -6,7 +6,8 @@ from secrets.views import secret_add from . import views from .models import ( Cable, ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, FrontPort, Interface, Manufacturer, Platform, - PowerPort, PowerOutlet, Rack, RackGroup, RackReservation, RackRole, RearPort, Region, Site, VirtualChassis, + PowerFeed, PowerPanel, PowerPort, PowerOutlet, Rack, RackGroup, RackReservation, RackRole, RearPort, Region, Site, + VirtualChassis, ) app_name = 'dcim' @@ -279,4 +280,23 @@ urlpatterns = [ url(r'^virtual-chassis/(?P\d+)/add-member/$', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'), url(r'^virtual-chassis-members/(?P\d+)/delete/$', views.VirtualChassisRemoveMemberView.as_view(), name='virtualchassis_remove_member'), + # Power panels + url(r'^power-panels/$', views.PowerPanelListView.as_view(), name='powerpanel_list'), + url(r'^power-panels/add/$', views.PowerPanelCreateView.as_view(), name='powerpanel_add'), + url(r'^power-panels/import/$', views.PowerPanelBulkImportView.as_view(), name='powerpanel_import'), + url(r'^power-panels/delete/$', views.PowerPanelBulkDeleteView.as_view(), name='powerpanel_bulk_delete'), + url(r'^power-panels/(?P\d+)/edit/$', views.PowerPanelEditView.as_view(), name='powerpanel_edit'), + url(r'^power-panels/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='powerpanel_changelog', kwargs={'model': PowerPanel}), + + # Racks + url(r'^power-feeds/$', views.PowerFeedListView.as_view(), name='powerfeed_list'), + url(r'^power-feeds/add/$', views.PowerFeedEditView.as_view(), name='powerfeed_add'), + url(r'^power-feeds/import/$', views.PowerFeedBulkImportView.as_view(), name='powerfeed_import'), + url(r'^power-feeds/edit/$', views.PowerFeedBulkEditView.as_view(), name='powerfeed_bulk_edit'), + url(r'^power-feeds/delete/$', views.PowerFeedBulkDeleteView.as_view(), name='powerfeed_bulk_delete'), + url(r'^power-feeds/(?P\d+)/$', views.PowerFeedView.as_view(), name='powerfeed'), + url(r'^power-feeds/(?P\d+)/edit/$', views.PowerFeedEditView.as_view(), name='powerfeed_edit'), + url(r'^power-feeds/(?P\d+)/delete/$', views.PowerFeedDeleteView.as_view(), name='powerfeed_delete'), + url(r'^power-feeds/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='powerfeed_changelog', kwargs={'model': PowerFeed}), + ] diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 27f90a3a2..958975e16 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -30,8 +30,9 @@ from . import filters, forms, tables from .models import ( Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, - InventoryItem, Manufacturer, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, - RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis, + InventoryItem, Manufacturer, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort, + PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, + VirtualChassis, ) @@ -2114,3 +2115,113 @@ class VirtualChassisRemoveMemberView(PermissionRequiredMixin, GetReturnURLMixin, 'form': form, 'return_url': self.get_return_url(request, device), }) + + +# +# Power panels +# + +class PowerPanelListView(ObjectListView): + queryset = PowerPanel.objects.select_related( + 'site', 'rackgroup' + ).annotate( + rack_count=Count('powerfeeds') + ) + table = tables.PowerPanelTable + template_name = 'dcim/powerpanel_list.html' + + +class PowerPanelCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'dcim.add_powerpanel' + model = PowerPanel + model_form = forms.PowerPanelForm + default_return_url = 'dcim:powerpanel_list' + + +class PowerPanelEditView(PowerPanelCreateView): + permission_required = 'dcim.change_powerpanel' + + +class PowerPanelBulkImportView(PermissionRequiredMixin, BulkImportView): + permission_required = 'dcim.add_powerpanel' + model_form = forms.PowerPanelCSVForm + table = tables.PowerPanelTable + default_return_url = 'dcim:powerpanel_list' + + +class PowerPanelBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): + permission_required = 'dcim.delete_powerpanel' + queryset = PowerPanel.objects.select_related( + 'site', 'rack_group' + ).annotate( + rack_count=Count('powerfeeds') + ) + table = tables.PowerPanelTable + default_return_url = 'dcim:powerpanel_list' + + +# +# Power feeds +# + +class PowerFeedListView(ObjectListView): + queryset = PowerFeed.objects.select_related( + 'powerpanel', 'rack' + ) + # filter = filters.PowerFeedFilter + # filter_form = forms.PowerFeedFilterForm + table = tables.PowerFeedTable + template_name = 'dcim/powerfeed_list.html' + + +class PowerFeedView(View): + + def get(self, request, pk): + + powerfeed = get_object_or_404(PowerFeed.objects.select_related('panel', 'rack'), pk=pk) + + return render(request, 'dcim/powerfeed.html', { + 'powerfeed': powerfeed, + }) + + +class PowerFeedCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'dcim.add_powerfeed' + model = PowerFeed + model_form = forms.PowerFeedForm + template_name = 'dcim/powerfeed_edit.html' + default_return_url = 'dcim:powerfeed_list' + + +class PowerFeedEditView(PowerFeedCreateView): + permission_required = 'dcim.change_powerfeed' + + +class PowerFeedDeleteView(PermissionRequiredMixin, ObjectDeleteView): + permission_required = 'dcim.delete_powerfeed' + model = PowerFeed + default_return_url = 'dcim:powerfeed_list' + + +class PowerFeedBulkImportView(PermissionRequiredMixin, BulkImportView): + permission_required = 'dcim.add_powerfeed' + model_form = forms.PowerFeedCSVForm + table = tables.PowerFeedTable + default_return_url = 'dcim:powerfeed_list' + + +class PowerFeedBulkEditView(PermissionRequiredMixin, BulkEditView): + permission_required = 'dcim.change_powerfeed' + queryset = PowerFeed.objects.select_related('powerpanel', 'rack') + # filter = filters.PowerFeedFilter + table = tables.PowerFeedTable + form = forms.PowerFeedBulkEditForm + default_return_url = 'dcim:powerfeed_list' + + +class PowerFeedBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): + permission_required = 'dcim.delete_powerfeed' + queryset = PowerFeed.objects.select_related('powerpanel', 'rack') + # filter = filters.PowerFeedFilter + table = tables.PowerFeedTable + default_return_url = 'dcim:powerfeed_list' diff --git a/netbox/templates/dcim/powerfeed.html b/netbox/templates/dcim/powerfeed.html new file mode 100644 index 000000000..9dc4843f4 --- /dev/null +++ b/netbox/templates/dcim/powerfeed.html @@ -0,0 +1,111 @@ +{% extends '_base.html' %} +{% load static %} +{% load tz %} +{% load helpers %} + +{% block header %} +
+
+ +
+
+
+
+ + + + +
+
+
+
+
+ {% if perms.dcim.change_powerfeed %} + + + Edit this power feed + + {% endif %} + {% if perms.dcim.delete_powerfeed %} + + + Delete this power feed + + {% endif %} +
+

{% block title %}{{ powerfeed }}{% endblock %}

+ {% include 'inc/created_updated.html' with obj=powerfeed %} +{% endblock %} + +{% block content %} +
+
+
+
+ Power Feed +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Power Panel + {{ powerfeed.powerpanel }} +
Rack + {% if powerfeed.rack %} + {{ powerfeed.rack }} + {% else %} + None + {% endif %} +
Type + {{ powerfeed.get_type_display }} +
Status + {{ powerfeed.get_status_display }} +
Supply{{ powerfeed.get_supply_display }}
Voltage{{ powerfeed.voltage }}V
Amperage{{ powerfeed.amperage }}A
Phase{{ powerfeed.get_phase_display }}
Max Utilization{{ powerfeed.max_utilization }}%
+
+
+
+
+
+{% endblock %} diff --git a/netbox/templates/dcim/powerfeed_edit.html b/netbox/templates/dcim/powerfeed_edit.html new file mode 100644 index 000000000..709112f93 --- /dev/null +++ b/netbox/templates/dcim/powerfeed_edit.html @@ -0,0 +1,45 @@ +{% extends 'utilities/obj_edit.html' %} +{% load form_helpers %} + +{% block form %} +
+
Power Feed
+
+ {% render_field form.powerpanel %} + {% render_field form.rack %} + {% render_field form.name %} + {% render_field form.type %} + {% render_field form.status %} +
+
+
+
Characteristics
+
+ {% render_field form.supply %} + {% render_field form.voltage %} + {% render_field form.amperage %} + {% render_field form.phase %} + {% render_field form.max_utilization %} +
+
+ {% if form.custom_fields %} +
+
Custom Fields
+
+ {% render_custom_fields form %} +
+
+ {% endif %} +
+
Tags
+
+ {% render_field form.tags %} +
+
+
+
Comments
+
+ {% render_field form.comments %} +
+
+{% endblock %} diff --git a/netbox/templates/dcim/powerfeed_list.html b/netbox/templates/dcim/powerfeed_list.html new file mode 100644 index 000000000..cfe2c989c --- /dev/null +++ b/netbox/templates/dcim/powerfeed_list.html @@ -0,0 +1,22 @@ +{% extends '_base.html' %} +{% load buttons %} + +{% block content %} +
+ {% if perms.dcim.add_powerfeed %} + {% add_button 'dcim:powerfeed_add' %} + {% import_button 'dcim:powerfeed_import' %} + {% endif %} + {% export_button content_type %} +
+

{% block title %}Power Feeds{% endblock %}

+
+
+ {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:powerfeed_bulk_edit' bulk_delete_url='dcim:powerfeed_bulk_delete' %} +
+
+ {% include 'inc/search_panel.html' %} + {% include 'inc/tags_panel.html' %} +
+
+{% endblock %} diff --git a/netbox/templates/dcim/powerpanel_list.html b/netbox/templates/dcim/powerpanel_list.html new file mode 100644 index 000000000..b58469c12 --- /dev/null +++ b/netbox/templates/dcim/powerpanel_list.html @@ -0,0 +1,18 @@ +{% extends '_base.html' %} +{% load buttons %} + +{% block content %} +
+ {% if perms.dcim.add_powerpanel %} + {% add_button 'dcim:powerpanel_add' %} + {% import_button 'dcim:powerpanel_import' %} + {% endif %} + {% export_button content_type %} +
+

{% block title %}Power Panels{% endblock %}

+
+
+ {% include 'utilities/obj_table.html' with bulk_delete_url='dcim:powerpanel_bulk_delete' %} +
+
+{% endblock %} diff --git a/netbox/templates/inc/nav_menu.html b/netbox/templates/inc/nav_menu.html index 5f8f371d3..a08eb96e0 100644 --- a/netbox/templates/inc/nav_menu.html +++ b/netbox/templates/inc/nav_menu.html @@ -368,6 +368,29 @@ + {% if request.user.is_authenticated %} - + {% if perms.extras.view_objectchange %} + + {% endif %} {% endblock %} diff --git a/netbox/templates/circuits/provider.html b/netbox/templates/circuits/provider.html index 3dd5d973f..13318492b 100644 --- a/netbox/templates/circuits/provider.html +++ b/netbox/templates/circuits/provider.html @@ -51,9 +51,11 @@ - + {% if perms.extras.view_objectchange %} + + {% endif %} {% endblock %} diff --git a/netbox/templates/dcim/cable.html b/netbox/templates/dcim/cable.html index 953e6caf4..07a7c49b9 100644 --- a/netbox/templates/dcim/cable.html +++ b/netbox/templates/dcim/cable.html @@ -27,9 +27,11 @@ - + {% if perms.extras.view_objectchange %} + + {% endif %} {% endblock %} diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index b9f283837..dc39959a5 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -86,12 +86,16 @@ {% include 'dcim/inc/device_napalm_tabs.html' %} {% endif %} {% endif %} - - + {% if perms.extras.view_configcontext %} + + {% endif %} + {% if perms.extras.view_objectchange %} + + {% endif %} {% endblock %} diff --git a/netbox/templates/dcim/devicetype.html b/netbox/templates/dcim/devicetype.html index 5b5410457..52edf5e09 100644 --- a/netbox/templates/dcim/devicetype.html +++ b/netbox/templates/dcim/devicetype.html @@ -50,9 +50,11 @@ - + {% if perms.extras.view_objectchange %} + + {% endif %} {% endblock %} diff --git a/netbox/templates/dcim/interface.html b/netbox/templates/dcim/interface.html index 3c3951417..8d12535c0 100644 --- a/netbox/templates/dcim/interface.html +++ b/netbox/templates/dcim/interface.html @@ -32,9 +32,11 @@ - + {% if perms.extras.view_objectchange %} + + {% endif %} {% endblock %} diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index e702405a2..f3083198c 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -47,9 +47,11 @@ - + {% if perms.extras.view_objectchange %} + + {% endif %} {% endblock %} diff --git a/netbox/templates/dcim/site.html b/netbox/templates/dcim/site.html index 0ef14ee1b..bf95e3050 100644 --- a/netbox/templates/dcim/site.html +++ b/netbox/templates/dcim/site.html @@ -56,9 +56,11 @@ - + {% if perms.extras.view_objectchange %} + + {% endif %} {% endblock %} diff --git a/netbox/templates/extras/tag.html b/netbox/templates/extras/tag.html index 3c37729e7..3011e2d3b 100644 --- a/netbox/templates/extras/tag.html +++ b/netbox/templates/extras/tag.html @@ -42,9 +42,11 @@ - + {% if perms.extras.view_objectchange %} + + {% endif %} {% endblock %} diff --git a/netbox/templates/ipam/aggregate.html b/netbox/templates/ipam/aggregate.html index 6fe79810f..d2bedf13d 100644 --- a/netbox/templates/ipam/aggregate.html +++ b/netbox/templates/ipam/aggregate.html @@ -43,9 +43,11 @@ - + {% if perms.extras.view_objectchange %} + + {% endif %} {% endblock %} diff --git a/netbox/templates/ipam/ipaddress.html b/netbox/templates/ipam/ipaddress.html index c13653e3c..f4e0b495f 100644 --- a/netbox/templates/ipam/ipaddress.html +++ b/netbox/templates/ipam/ipaddress.html @@ -45,9 +45,11 @@ - + {% if perms.extras.view_objectchange %} + + {% endif %} {% endblock %} diff --git a/netbox/templates/ipam/prefix.html b/netbox/templates/ipam/prefix.html index 281515278..72be2e491 100644 --- a/netbox/templates/ipam/prefix.html +++ b/netbox/templates/ipam/prefix.html @@ -59,12 +59,16 @@ - - + {% if perms.ipam.view_ipaddress %} + + {% endif %} + {% if perms.extras.view_objectchange %} + + {% endif %} {% endblock %} diff --git a/netbox/templates/ipam/vlan.html b/netbox/templates/ipam/vlan.html index 7798bf9e0..ae87b5e29 100644 --- a/netbox/templates/ipam/vlan.html +++ b/netbox/templates/ipam/vlan.html @@ -51,9 +51,11 @@ - + {% if perms.extras.view_objectchange %} + + {% endif %} {% endblock %} diff --git a/netbox/templates/ipam/vrf.html b/netbox/templates/ipam/vrf.html index fb81d7ee1..dbf426321 100644 --- a/netbox/templates/ipam/vrf.html +++ b/netbox/templates/ipam/vrf.html @@ -42,9 +42,11 @@ - + {% if perms.extras.view_objectchange %} + + {% endif %} {% endblock %} diff --git a/netbox/templates/secrets/secret.html b/netbox/templates/secrets/secret.html index 2825e6770..ce5ab68b4 100644 --- a/netbox/templates/secrets/secret.html +++ b/netbox/templates/secrets/secret.html @@ -33,9 +33,11 @@ - + {% if perms.extras.view_objectchange %} + + {% endif %} {% endblock %} diff --git a/netbox/templates/tenancy/tenant.html b/netbox/templates/tenancy/tenant.html index 2cb925303..80b40e05b 100644 --- a/netbox/templates/tenancy/tenant.html +++ b/netbox/templates/tenancy/tenant.html @@ -45,9 +45,11 @@ - + {% if perms.extras.view_objectchange %} + + {% endif %} {% endblock %} diff --git a/netbox/templates/virtualization/cluster.html b/netbox/templates/virtualization/cluster.html index d6a589775..513e122a1 100644 --- a/netbox/templates/virtualization/cluster.html +++ b/netbox/templates/virtualization/cluster.html @@ -45,9 +45,11 @@ - + {% if perms.extras.view_objectchange %} + + {% endif %} {% endblock %} diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html index b1a8a1505..cac90df42 100644 --- a/netbox/templates/virtualization/virtualmachine.html +++ b/netbox/templates/virtualization/virtualmachine.html @@ -44,12 +44,16 @@ - - + {% if perms.extras.view_configcontext %} + + {% endif %} + {% if perms.extras.view_objectchange %} + + {% endif %} {% endblock %} From 78c90ba24b321aa68f324de8f23c372689c161bc Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 12 Apr 2019 10:00:43 -0400 Subject: [PATCH 044/140] Changelog for #54 --- CHANGELOG.md | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c09f3f91..19fd2535a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,22 +1,17 @@ v2.6.0 (FUTURE) -## Changes +## New Features -### API Device/VM Config Context Included by Default ([#2350](https://github.com/digitalocean/netbox/issues/2350)) +### Power Panels and Feeds ([#54](https://github.com/digitalocean/netbox/issues/54)) -The rendered Config Context for Devices and VMs is now included by default in all API results (list and detail views). -Previously the rendered Config Context was only available in the detail view for objects. Users with large amounts of -context data may observe a performance drop when returning multiple objects. To combat this, in cases where the rendered -Config Context is not needed, the query parameter `?exclude=config_context` may be added to the request as to remove -the Config Context from being included in any results. +NetBox now includes support for power supply modeling, through two new models: power panels and power feeds. Power feeds +are terminated to power panels and otpionally associated with individual racks. Each power feed defines a supply type +(AC/DC), amperage, voltage, and phase. Power ports can be connected directly to power feeds. -### Tag Permissions Changed +Additionally, the power port model has been extended to include fields denoting maximum and allocated draw, in watts. +This allows a device (e.g. a PDU) to calculate its total load compared to its connected power feed. -NetBox now makes use of its own `Tag` model instead of the vanilla model which ships with django-taggit. This new model -lives in the `extras` app and thus any permissions that you may have configured using "Taggit | Tag" should be changed -to now use "Extras | Tag." - -### View Permissions +### View Permissions ([#323](https://github.com/digitalocean/netbox/issues/323)) Django 2.1 introduced the ability to enforce view-only permissions for different object types. NetBox now enforces these by default. You can grant view permission to a user or group by assigning the "can view" permission for the @@ -38,6 +33,22 @@ To exclude _all_ objects, effectively disabling view permissions, set: EXEMPT_VIEW_PERMISSIONS = ['*'] ``` +## Changes + +### API Device/VM Config Context Included by Default ([#2350](https://github.com/digitalocean/netbox/issues/2350)) + +The rendered Config Context for Devices and VMs is now included by default in all API results (list and detail views). +Previously the rendered Config Context was only available in the detail view for objects. Users with large amounts of +context data may observe a performance drop when returning multiple objects. To combat this, in cases where the rendered +Config Context is not needed, the query parameter `?exclude=config_context` may be added to the request as to remove +the Config Context from being included in any results. + +### Tag Permissions Changed + +NetBox now makes use of its own `Tag` model instead of the vanilla model which ships with django-taggit. This new model +lives in the `extras` app and thus any permissions that you may have configured using "Taggit | Tag" should be changed +to now use "Extras | Tag." + ## Enhancements * [#323](https://github.com/digitalocean/netbox/issues/323) - Enforce per-object type view permissions From 067c788df713b379959e46ba7b5de335447ea6c2 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 12 Apr 2019 10:16:34 -0400 Subject: [PATCH 045/140] Add changelog tabs for power panel, power feed & clean up nav links --- netbox/dcim/urls.py | 2 +- netbox/templates/dcim/powerfeed.html | 10 ++++++ netbox/templates/dcim/powerpanel.html | 10 ++++++ netbox/templates/inc/nav_menu.html | 49 +++++++++++++-------------- 4 files changed, 45 insertions(+), 26 deletions(-) diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 82e32dad2..220f4b405 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -290,7 +290,7 @@ urlpatterns = [ url(r'^power-panels/(?P\d+)/delete/$', views.PowerPanelDeleteView.as_view(), name='powerpanel_delete'), url(r'^power-panels/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='powerpanel_changelog', kwargs={'model': PowerPanel}), - # Racks + # Power feeds url(r'^power-feeds/$', views.PowerFeedListView.as_view(), name='powerfeed_list'), url(r'^power-feeds/add/$', views.PowerFeedEditView.as_view(), name='powerfeed_add'), url(r'^power-feeds/import/$', views.PowerFeedBulkImportView.as_view(), name='powerfeed_import'), diff --git a/netbox/templates/dcim/powerfeed.html b/netbox/templates/dcim/powerfeed.html index f2c16927b..18af53350 100644 --- a/netbox/templates/dcim/powerfeed.html +++ b/netbox/templates/dcim/powerfeed.html @@ -45,6 +45,16 @@

{% block title %}{{ powerfeed }}{% endblock %}

{% include 'inc/created_updated.html' with obj=powerfeed %} + {% endblock %} {% block content %} diff --git a/netbox/templates/dcim/powerpanel.html b/netbox/templates/dcim/powerpanel.html index 88f90ae89..282582061 100644 --- a/netbox/templates/dcim/powerpanel.html +++ b/netbox/templates/dcim/powerpanel.html @@ -44,6 +44,16 @@

{% block title %}{{ powerpanel }}{% endblock %}

{% include 'inc/created_updated.html' with obj=powerpanel %} + {% endblock %} {% block content %} diff --git a/netbox/templates/inc/nav_menu.html b/netbox/templates/inc/nav_menu.html index 8d6fa3439..76dc61ef3 100644 --- a/netbox/templates/inc/nav_menu.html +++ b/netbox/templates/inc/nav_menu.html @@ -371,6 +371,7 @@ - {% if request.user.is_authenticated %} - - {% endif %} + {% endif %}