From fc2bb724fa91e5d9de83269ad3999e71753c99af Mon Sep 17 00:00:00 2001 From: John Anderson Date: Wed, 20 Feb 2019 03:52:47 -0500 Subject: [PATCH] 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',