From bd573fd5cf3273456d1ac5faabbde85509cb8387 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Mon, 18 Feb 2019 21:37:00 -0500 Subject: [PATCH 01/10] 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 02/10] #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 03/10] 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 04/10] 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 05/10] 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 06/10] 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 07/10] 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 08/10] 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 09/10] 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 10/10] 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__)))