Просмотр исходного кода

initial pass on migrating to custom tag model with color and comments fields

John Anderson 7 лет назад
Родитель
Сommit
fc2bb724fa
32 измененных файлов с 524 добавлено и 63 удалено
  1. 5 0
      CHANGELOG.md
  2. 25 0
      netbox/circuits/migrations/0015_custom_tag_models.py
  3. 3 3
      netbox/circuits/models.py
  4. 85 0
      netbox/dcim/migrations/0070_custom_tag_models.py
  5. 15 15
      netbox/dcim/models.py
  6. 2 2
      netbox/extras/api/serializers.py
  7. 2 2
      netbox/extras/api/views.py
  8. 1 2
      netbox/extras/filters.py
  9. 4 4
      netbox/extras/forms.py
  10. 46 0
      netbox/extras/migrations/0017_tag_taggeditem.py
  11. 46 0
      netbox/extras/migrations/0018_rename_tag_tables.py
  12. 52 0
      netbox/extras/migrations/0019_delete_taggit_models.py
  13. 24 0
      netbox/extras/migrations/0020_add_color_comments_to_tag.py
  14. 22 0
      netbox/extras/models.py
  15. 4 4
      netbox/extras/tables.py
  16. 1 2
      netbox/extras/tests/test_api.py
  17. 1 2
      netbox/extras/tests/test_views.py
  18. 7 7
      netbox/extras/views.py
  19. 45 0
      netbox/ipam/migrations/0025_custom_tag_models.py
  20. 7 7
      netbox/ipam/models.py
  21. 12 3
      netbox/netbox/admin.py
  22. 20 0
      netbox/secrets/migrations/0006_custom_tag_models.py
  23. 2 2
      netbox/secrets/models.py
  24. 18 0
      netbox/templates/extras/tag.html
  25. 19 0
      netbox/templates/extras/tag_edit.html
  26. 3 1
      netbox/templates/utilities/templatetags/tag.html
  27. 20 0
      netbox/tenancy/migrations/0006_custom_tag_models.py
  28. 2 2
      netbox/tenancy/models.py
  29. 2 1
      netbox/utilities/filters.py
  30. 1 1
      netbox/utilities/views.py
  31. 25 0
      netbox/virtualization/migrations/0009_custom_tag_models.py
  32. 3 3
      netbox/virtualization/models.py

+ 5 - 0
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
 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.
 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)
 v2.5.7 (FUTURE)

+ 25 - 0
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'),
+        ),
+    ]

+ 3 - 3
netbox/circuits/models.py

@@ -6,7 +6,7 @@ from taggit.managers import TaggableManager
 from dcim.constants import CONNECTION_STATUS_CHOICES, STATUS_CLASSES
 from dcim.constants import CONNECTION_STATUS_CHOICES, STATUS_CLASSES
 from dcim.fields import ASNField
 from dcim.fields import ASNField
 from dcim.models import CableTermination
 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.models import ChangeLoggedModel
 from utilities.utils import serialize_object
 from utilities.utils import serialize_object
 from .constants import CIRCUIT_STATUS_ACTIVE, CIRCUIT_STATUS_CHOICES, TERM_SIDE_CHOICES
 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'
         object_id_field='obj_id'
     )
     )
 
 
-    tags = TaggableManager()
+    tags = TaggableManager(through=TaggedItem)
 
 
     csv_headers = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
     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'
         object_id_field='obj_id'
     )
     )
 
 
-    tags = TaggableManager()
+    tags = TaggableManager(through=TaggedItem)
 
 
     csv_headers = [
     csv_headers = [
         'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
         'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',

+ 85 - 0
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'),
+        ),
+    ]

+ 15 - 15
netbox/dcim/models.py

@@ -15,7 +15,7 @@ from mptt.models import MPTTModel, TreeForeignKey
 from taggit.managers import TaggableManager
 from taggit.managers import TaggableManager
 from timezone_field import TimeZoneField
 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.fields import ColorField
 from utilities.managers import NaturalOrderingManager
 from utilities.managers import NaturalOrderingManager
 from utilities.models import ChangeLoggedModel
 from utilities.models import ChangeLoggedModel
@@ -319,7 +319,7 @@ class Site(ChangeLoggedModel, CustomFieldModel):
     )
     )
 
 
     objects = NaturalOrderingManager()
     objects = NaturalOrderingManager()
-    tags = TaggableManager()
+    tags = TaggableManager(through=TaggedItem)
 
 
     csv_headers = [
     csv_headers = [
         'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address',
         'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address',
@@ -566,7 +566,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
     )
     )
 
 
     objects = NaturalOrderingManager()
     objects = NaturalOrderingManager()
-    tags = TaggableManager()
+    tags = TaggableManager(through=TaggedItem)
 
 
     csv_headers = [
     csv_headers = [
         'site', 'group_name', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag', 'width',
         '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'
         object_id_field='obj_id'
     )
     )
 
 
-    tags = TaggableManager()
+    tags = TaggableManager(through=TaggedItem)
 
 
     csv_headers = [
     csv_headers = [
         'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'comments',
         'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'comments',
@@ -1455,7 +1455,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
     )
     )
 
 
     objects = NaturalOrderingManager()
     objects = NaturalOrderingManager()
-    tags = TaggableManager()
+    tags = TaggableManager(through=TaggedItem)
 
 
     csv_headers = [
     csv_headers = [
         'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
         'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
@@ -1743,7 +1743,7 @@ class ConsolePort(CableTermination, ComponentModel):
     )
     )
 
 
     objects = DeviceComponentManager()
     objects = DeviceComponentManager()
-    tags = TaggableManager()
+    tags = TaggableManager(through=TaggedItem)
 
 
     csv_headers = ['device', 'name']
     csv_headers = ['device', 'name']
 
 
@@ -1786,7 +1786,7 @@ class ConsoleServerPort(CableTermination, ComponentModel):
     )
     )
 
 
     objects = DeviceComponentManager()
     objects = DeviceComponentManager()
-    tags = TaggableManager()
+    tags = TaggableManager(through=TaggedItem)
 
 
     csv_headers = ['device', 'name']
     csv_headers = ['device', 'name']
 
 
@@ -1835,7 +1835,7 @@ class PowerPort(CableTermination, ComponentModel):
     )
     )
 
 
     objects = DeviceComponentManager()
     objects = DeviceComponentManager()
-    tags = TaggableManager()
+    tags = TaggableManager(through=TaggedItem)
 
 
     csv_headers = ['device', 'name']
     csv_headers = ['device', 'name']
 
 
@@ -1878,7 +1878,7 @@ class PowerOutlet(CableTermination, ComponentModel):
     )
     )
 
 
     objects = DeviceComponentManager()
     objects = DeviceComponentManager()
-    tags = TaggableManager()
+    tags = TaggableManager(through=TaggedItem)
 
 
     csv_headers = ['device', 'name']
     csv_headers = ['device', 'name']
 
 
@@ -1998,7 +1998,7 @@ class Interface(CableTermination, ComponentModel):
     )
     )
 
 
     objects = InterfaceManager()
     objects = InterfaceManager()
-    tags = TaggableManager()
+    tags = TaggableManager(through=TaggedItem)
 
 
     csv_headers = [
     csv_headers = [
         'device', 'virtual_machine', 'name', 'lag', 'form_factor', 'enabled', 'mac_address', 'mtu', 'mgmt_only',
         'device', 'virtual_machine', 'name', 'lag', 'form_factor', 'enabled', 'mac_address', 'mtu', 'mgmt_only',
@@ -2199,7 +2199,7 @@ class FrontPort(CableTermination, ComponentModel):
     )
     )
 
 
     objects = DeviceComponentManager()
     objects = DeviceComponentManager()
-    tags = TaggableManager()
+    tags = TaggableManager(through=TaggedItem)
 
 
     csv_headers = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'description']
     csv_headers = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'description']
 
 
@@ -2265,7 +2265,7 @@ class RearPort(CableTermination, ComponentModel):
     )
     )
 
 
     objects = DeviceComponentManager()
     objects = DeviceComponentManager()
-    tags = TaggableManager()
+    tags = TaggableManager(through=TaggedItem)
 
 
     csv_headers = ['device', 'name', 'type', 'positions', 'description']
     csv_headers = ['device', 'name', 'type', 'positions', 'description']
 
 
@@ -2312,7 +2312,7 @@ class DeviceBay(ComponentModel):
     )
     )
 
 
     objects = DeviceComponentManager()
     objects = DeviceComponentManager()
-    tags = TaggableManager()
+    tags = TaggableManager(through=TaggedItem)
 
 
     csv_headers = ['device', 'name', 'installed_device']
     csv_headers = ['device', 'name', 'installed_device']
 
 
@@ -2405,7 +2405,7 @@ class InventoryItem(ComponentModel):
         blank=True
         blank=True
     )
     )
 
 
-    tags = TaggableManager()
+    tags = TaggableManager(through=TaggedItem)
 
 
     csv_headers = [
     csv_headers = [
         'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description',
         'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description',
@@ -2452,7 +2452,7 @@ class VirtualChassis(ChangeLoggedModel):
         blank=True
         blank=True
     )
     )
 
 
-    tags = TaggableManager()
+    tags = TaggableManager(through=TaggedItem)
 
 
     csv_headers = ['master', 'domain']
     csv_headers = ['master', 'domain']
 
 

+ 2 - 2
netbox/extras/api/serializers.py

@@ -1,6 +1,5 @@
 from django.core.exceptions import ObjectDoesNotExist
 from django.core.exceptions import ObjectDoesNotExist
 from rest_framework import serializers
 from rest_framework import serializers
-from taggit.models import Tag
 
 
 from dcim.api.nested_serializers import (
 from dcim.api.nested_serializers import (
     NestedDeviceSerializer, NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedRackSerializer,
     NestedDeviceSerializer, NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedRackSerializer,
@@ -10,6 +9,7 @@ from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site
 from extras.constants import *
 from extras.constants import *
 from extras.models import (
 from extras.models import (
     ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
     ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
+    Tag
 )
 )
 from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
 from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
 from tenancy.models import Tenant, TenantGroup
 from tenancy.models import Tenant, TenantGroup
@@ -80,7 +80,7 @@ class TagSerializer(ValidatedModelSerializer):
 
 
     class Meta:
     class Meta:
         model = Tag
         model = Tag
-        fields = ['id', 'name', 'slug', 'tagged_items']
+        fields = ['id', 'name', 'slug', 'color', 'comments', 'tagged_items']
 
 
 
 
 #
 #

+ 2 - 2
netbox/extras/api/views.py

@@ -6,11 +6,11 @@ from rest_framework.decorators import action
 from rest_framework.exceptions import PermissionDenied
 from rest_framework.exceptions import PermissionDenied
 from rest_framework.response import Response
 from rest_framework.response import Response
 from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
 from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
-from taggit.models import Tag
 
 
 from extras import filters
 from extras import filters
 from extras.models import (
 from extras.models import (
     ConfigContext, CustomField, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
     ConfigContext, CustomField, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
+    Tag
 )
 )
 from extras.reports import get_report, get_reports
 from extras.reports import get_report, get_reports
 from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet
 from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet
@@ -115,7 +115,7 @@ class TopologyMapViewSet(ModelViewSet):
 #
 #
 
 
 class TagViewSet(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
     serializer_class = serializers.TagSerializer
     filterset_class = filters.TagFilter
     filterset_class = filters.TagFilter
 
 

+ 1 - 2
netbox/extras/filters.py

@@ -1,12 +1,11 @@
 import django_filters
 import django_filters
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 from django.db.models import Q
 from django.db.models import Q
-from taggit.models import Tag
 
 
 from dcim.models import DeviceRole, Platform, Region, Site
 from dcim.models import DeviceRole, Platform, Region, Site
 from tenancy.models import Tenant, TenantGroup
 from tenancy.models import Tenant, TenantGroup
 from .constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_TYPE_BOOLEAN, CF_TYPE_SELECT
 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):
 class CustomFieldFilter(django_filters.Filter):

+ 4 - 4
netbox/extras/forms.py

@@ -6,19 +6,18 @@ from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ObjectDoesNotExist
 from django.core.exceptions import ObjectDoesNotExist
 from mptt.forms import TreeNodeMultipleChoiceField
 from mptt.forms import TreeNodeMultipleChoiceField
 from taggit.forms import TagField
 from taggit.forms import TagField
-from taggit.models import Tag
 
 
 from dcim.models import DeviceRole, Platform, Region, Site
 from dcim.models import DeviceRole, Platform, Region, Site
 from tenancy.models import Tenant, TenantGroup
 from tenancy.models import Tenant, TenantGroup
 from utilities.forms import (
 from utilities.forms import (
     add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ContentTypeSelect,
     add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ContentTypeSelect,
-    FilterChoiceField, FilterTreeNodeMultipleChoiceField, LaxURLField, JSONField, SlugField,
+    FilterChoiceField, FilterTreeNodeMultipleChoiceField, LaxURLField, JSONField, SlugField, CommentField
 )
 )
 from .constants import (
 from .constants import (
     CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL,
     CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL,
     OBJECTCHANGE_ACTION_CHOICES,
     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):
 class TagForm(BootstrapMixin, forms.ModelForm):
     slug = SlugField()
     slug = SlugField()
+    comments = CommentField()
 
 
     class Meta:
     class Meta:
         model = Tag
         model = Tag
         fields = [
         fields = [
-            'name', 'slug',
+            'name', 'slug', 'color', 'comments'
         ]
         ]
 
 
 
 

+ 46 - 0
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
+        )
+    ]

+ 46 - 0
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'
+        ),
+    ]

+ 52 - 0
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
+        )
+    ]

+ 24 - 0
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),
+        ),
+    ]

+ 22 - 0
netbox/extras/models.py

@@ -12,8 +12,10 @@ from django.db.models import F, Q
 from django.http import HttpResponse
 from django.http import HttpResponse
 from django.template import Template, Context
 from django.template import Template, Context
 from django.urls import reverse
 from django.urls import reverse
+from taggit.models import TagBase, GenericTaggedItemBase
 
 
 from dcim.constants import CONNECTION_STATUS_CONNECTED
 from dcim.constants import CONNECTION_STATUS_CONNECTED
+from utilities.fields import ColorField
 from utilities.utils import deepmerge, foreground_color
 from utilities.utils import deepmerge, foreground_color
 from .constants import *
 from .constants import *
 from .querysets import ConfigContextQuerySet
 from .querysets import ConfigContextQuerySet
@@ -860,3 +862,23 @@ class ObjectChange(models.Model):
             self.object_repr,
             self.object_repr,
             self.object_data,
             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
+    )

+ 4 - 4
netbox/extras/tables.py

@@ -1,9 +1,8 @@
 import django_tables2 as tables
 import django_tables2 as tables
 from django_tables2.utils import Accessor
 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 = """
 TAG_ACTIONS = """
 {% if perms.taggit.change_tag %}
 {% if perms.taggit.change_tag %}
@@ -71,10 +70,11 @@ class TagTable(BaseTable):
         attrs={'td': {'class': 'text-right'}},
         attrs={'td': {'class': 'text-right'}},
         verbose_name=''
         verbose_name=''
     )
     )
+    color = ColorColumn()
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = Tag
         model = Tag
-        fields = ('pk', 'name', 'items', 'slug', 'actions')
+        fields = ('pk', 'name', 'items', 'slug', 'color', 'actions')
 
 
 
 
 class TaggedItemTable(BaseTable):
 class TaggedItemTable(BaseTable):

+ 1 - 2
netbox/extras/tests/test_api.py

@@ -1,11 +1,10 @@
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 from django.urls import reverse
 from django.urls import reverse
 from rest_framework import status
 from rest_framework import status
-from taggit.models import Tag
 
 
 from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Platform, Region, Site
 from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Platform, Region, Site
 from extras.constants import GRAPH_TYPE_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 tenancy.models import Tenant, TenantGroup
 from utilities.testing import APITestCase
 from utilities.testing import APITestCase
 
 

+ 1 - 2
netbox/extras/tests/test_views.py

@@ -4,10 +4,9 @@ import uuid
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
 from django.test import Client, TestCase
 from django.test import Client, TestCase
 from django.urls import reverse
 from django.urls import reverse
-from taggit.models import Tag
 
 
 from dcim.models import Site
 from dcim.models import Site
-from extras.models import ConfigContext, ObjectChange
+from extras.models import ConfigContext, ObjectChange, Tag
 
 
 
 
 class TagTestCase(TestCase):
 class TagTestCase(TestCase):

+ 7 - 7
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.utils.safestring import mark_safe
 from django.views.generic import View
 from django.views.generic import View
 from django_tables2 import RequestConfig
 from django_tables2 import RequestConfig
-from taggit.models import Tag, TaggedItem
 
 
 from utilities.forms import ConfirmationForm
 from utilities.forms import ConfirmationForm
 from utilities.paginator import EnhancedPaginator
 from utilities.paginator import EnhancedPaginator
@@ -19,7 +18,7 @@ from .forms import (
     ConfigContextForm, ConfigContextBulkEditForm, ConfigContextFilterForm, ImageAttachmentForm, ObjectChangeFilterForm,
     ConfigContextForm, ConfigContextBulkEditForm, ConfigContextFilterForm, ImageAttachmentForm, ObjectChangeFilterForm,
     TagFilterForm, TagForm,
     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 .reports import get_report, get_reports
 from .tables import ConfigContextTable, ObjectChangeTable, TagTable, TaggedItemTable
 from .tables import ConfigContextTable, ObjectChangeTable, TagTable, TaggedItemTable
 
 
@@ -30,7 +29,7 @@ from .tables import ConfigContextTable, ObjectChangeTable, TagTable, TaggedItemT
 
 
 class TagListView(ObjectListView):
 class TagListView(ObjectListView):
     queryset = Tag.objects.annotate(
     queryset = Tag.objects.annotate(
-        items=Count('taggit_taggeditem_items')
+        items=Count('extras_taggeditem_items')
     ).order_by(
     ).order_by(
         'name'
         'name'
     )
     )
@@ -69,22 +68,23 @@ class TagView(View):
 
 
 
 
 class TagEditView(PermissionRequiredMixin, ObjectEditView):
 class TagEditView(PermissionRequiredMixin, ObjectEditView):
-    permission_required = 'taggit.change_tag'
+    permission_required = 'extras.change_tag'
     model = Tag
     model = Tag
     model_form = TagForm
     model_form = TagForm
     default_return_url = 'extras:tag_list'
     default_return_url = 'extras:tag_list'
+    template_name = 'extras/tag_edit.html'
 
 
 
 
 class TagDeleteView(PermissionRequiredMixin, ObjectDeleteView):
 class TagDeleteView(PermissionRequiredMixin, ObjectDeleteView):
-    permission_required = 'taggit.delete_tag'
+    permission_required = 'extras.delete_tag'
     model = Tag
     model = Tag
     default_return_url = 'extras:tag_list'
     default_return_url = 'extras:tag_list'
 
 
 
 
 class TagBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 class TagBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
-    permission_required = 'taggit.delete_tag'
+    permission_required = 'extras.delete_tag'
     queryset = Tag.objects.annotate(
     queryset = Tag.objects.annotate(
-        items=Count('taggit_taggeditem_items')
+        items=Count('extras_taggeditem_items')
     ).order_by(
     ).order_by(
         'name'
         'name'
     )
     )

+ 45 - 0
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'),
+        ),
+    ]

+ 7 - 7
netbox/ipam/models.py

@@ -10,7 +10,7 @@ from django.urls import reverse
 from taggit.managers import TaggableManager
 from taggit.managers import TaggableManager
 
 
 from dcim.models import Interface
 from dcim.models import Interface
-from extras.models import CustomFieldModel
+from extras.models import CustomFieldModel, TaggedItem
 from utilities.models import ChangeLoggedModel
 from utilities.models import ChangeLoggedModel
 from .constants import *
 from .constants import *
 from .fields import IPNetworkField, IPAddressField
 from .fields import IPNetworkField, IPAddressField
@@ -55,7 +55,7 @@ class VRF(ChangeLoggedModel, CustomFieldModel):
         object_id_field='obj_id'
         object_id_field='obj_id'
     )
     )
 
 
-    tags = TaggableManager()
+    tags = TaggableManager(through=TaggedItem)
 
 
     csv_headers = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
     csv_headers = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
 
 
@@ -154,7 +154,7 @@ class Aggregate(ChangeLoggedModel, CustomFieldModel):
         object_id_field='obj_id'
         object_id_field='obj_id'
     )
     )
 
 
-    tags = TaggableManager()
+    tags = TaggableManager(through=TaggedItem)
 
 
     csv_headers = ['prefix', 'rir', 'date_added', 'description']
     csv_headers = ['prefix', 'rir', 'date_added', 'description']
 
 
@@ -324,7 +324,7 @@ class Prefix(ChangeLoggedModel, CustomFieldModel):
     )
     )
 
 
     objects = PrefixQuerySet.as_manager()
     objects = PrefixQuerySet.as_manager()
-    tags = TaggableManager()
+    tags = TaggableManager(through=TaggedItem)
 
 
     csv_headers = [
     csv_headers = [
         'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan_vid', 'status', 'role', 'is_pool', 'description',
         'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan_vid', 'status', 'role', 'is_pool', 'description',
@@ -583,7 +583,7 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
     )
     )
 
 
     objects = IPAddressManager()
     objects = IPAddressManager()
-    tags = TaggableManager()
+    tags = TaggableManager(through=TaggedItem)
 
 
     csv_headers = [
     csv_headers = [
         'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface_name', 'is_primary',
         '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'
         object_id_field='obj_id'
     )
     )
 
 
-    tags = TaggableManager()
+    tags = TaggableManager(through=TaggedItem)
 
 
     csv_headers = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']
     csv_headers = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']
 
 
@@ -892,7 +892,7 @@ class Service(ChangeLoggedModel, CustomFieldModel):
         object_id_field='obj_id'
         object_id_field='obj_id'
     )
     )
 
 
-    tags = TaggableManager()
+    tags = TaggableManager(through=TaggedItem)
 
 
     csv_headers = ['device', 'virtual_machine', 'name', 'protocol', 'description']
     csv_headers = ['device', 'virtual_machine', 'name', 'protocol', 'description']
 
 

+ 12 - 3
netbox/netbox/admin.py

@@ -2,8 +2,17 @@ from django.conf import settings
 from django.contrib.admin import AdminSite
 from django.contrib.admin import AdminSite
 from django.contrib.auth.admin import GroupAdmin, UserAdmin
 from django.contrib.auth.admin import GroupAdmin, UserAdmin
 from django.contrib.auth.models import Group, User
 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):
 class NetBoxAdminSite(AdminSite):
@@ -20,7 +29,7 @@ admin_site = NetBoxAdminSite(name='admin')
 # Register external models
 # Register external models
 admin_site.register(Group, GroupAdmin)
 admin_site.register(Group, GroupAdmin)
 admin_site.register(User, UserAdmin)
 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)
 # Modify the template to include an RQ link if django_rq is installed (see RQ_SHOW_ADMIN_LINK)
 if settings.WEBHOOKS_ENABLED:
 if settings.WEBHOOKS_ENABLED:

+ 20 - 0
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'),
+        ),
+    ]

+ 2 - 2
netbox/secrets/models.py

@@ -14,7 +14,7 @@ from django.urls import reverse
 from django.utils.encoding import force_bytes
 from django.utils.encoding import force_bytes
 from taggit.managers import TaggableManager
 from taggit.managers import TaggableManager
 
 
-from extras.models import CustomFieldModel
+from extras.models import CustomFieldModel, TaggedItem
 from utilities.models import ChangeLoggedModel
 from utilities.models import ChangeLoggedModel
 from .exceptions import InvalidKey
 from .exceptions import InvalidKey
 from .hashers import SecretValidationHasher
 from .hashers import SecretValidationHasher
@@ -345,7 +345,7 @@ class Secret(ChangeLoggedModel, CustomFieldModel):
         object_id_field='obj_id'
         object_id_field='obj_id'
     )
     )
 
 
-    tags = TaggableManager()
+    tags = TaggableManager(through=TaggedItem)
 
 
     plaintext = None
     plaintext = None
     csv_headers = ['device', 'role', 'name', 'plaintext']
     csv_headers = ['device', 'role', 'name', 'plaintext']

+ 18 - 0
netbox/templates/extras/tag.html

@@ -59,8 +59,26 @@
                             {{ items_count }}
                             {{ items_count }}
                         </td>
                         </td>
                     </tr>
                     </tr>
+                    <tr>
+                        <td>Color</td>
+                        <td>
+                            <span class="label color-block" style="background-color: #{{ tag.color }}">&nbsp;</span>
+                        </td>
+                    </tr>
                 </table>
                 </table>
             </div>
             </div>
+            <div class="panel panel-default">
+                <div class="panel-heading">
+                    <strong>Comments</strong>
+                </div>
+                <div class="panel-body rendered-markdown">
+                    {% if tag.comments %}
+                        {{ tag.comments|gfm }}
+                    {% else %}
+                        <span class="text-muted">None</span>
+                    {% endif %}
+                </div>
+            </div>
         </div>
         </div>
         <div class="col-md-6">
         <div class="col-md-6">
             {% include 'panel_table.html' with table=items_table heading='Tagged Objects' %}
             {% include 'panel_table.html' with table=items_table heading='Tagged Objects' %}

+ 19 - 0
netbox/templates/extras/tag_edit.html

@@ -0,0 +1,19 @@
+{% extends 'utilities/obj_edit.html' %}
+{% load form_helpers %}
+
+{% block form %}
+    <div class="panel panel-default">
+        <div class="panel-heading"><strong>Tag</strong></div>
+        <div class="panel-body">
+            {% render_field form.name %}
+            {% render_field form.slug %}
+            {% render_field form.color %}
+        </div>
+    </div>
+    <div class="panel panel-default">
+        <div class="panel-heading"><strong>Comments</strong></div>
+        <div class="panel-body">
+            {% render_field form.comments %}
+        </div>
+    </div>
+{% endblock %}

+ 3 - 1
netbox/templates/utilities/templatetags/tag.html

@@ -1,5 +1,7 @@
+{% load helpers %}
+
 {% if url_name %}
 {% if url_name %}
-    <a href="{% url url_name %}?tag={{ tag.slug }}"><span class="label label-default">{{ tag }}</span></a>
+    <a href="{% url url_name %}?tag={{ tag.slug }}"><span class="label label-default" style="color: {{ tag.color|fgcolor }}; background-color: #{{ tag.color }}">{{ tag }}</span></a>
 {% else %}
 {% else %}
     <span class="label label-default">{{ tag }}</span>
     <span class="label label-default">{{ tag }}</span>
 {% endif %}
 {% endif %}

+ 20 - 0
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'),
+        ),
+    ]

+ 2 - 2
netbox/tenancy/models.py

@@ -3,7 +3,7 @@ from django.db import models
 from django.urls import reverse
 from django.urls import reverse
 from taggit.managers import TaggableManager
 from taggit.managers import TaggableManager
 
 
-from extras.models import CustomFieldModel
+from extras.models import CustomFieldModel, TaggedItem
 from utilities.models import ChangeLoggedModel
 from utilities.models import ChangeLoggedModel
 
 
 
 
@@ -70,7 +70,7 @@ class Tenant(ChangeLoggedModel, CustomFieldModel):
         object_id_field='obj_id'
         object_id_field='obj_id'
     )
     )
 
 
-    tags = TaggableManager()
+    tags = TaggableManager(through=TaggedItem)
 
 
     csv_headers = ['name', 'slug', 'group', 'description', 'comments']
     csv_headers = ['name', 'slug', 'group', 'description', 'comments']
 
 

+ 2 - 1
netbox/utilities/filters.py

@@ -1,7 +1,8 @@
 import django_filters
 import django_filters
 from django.conf import settings
 from django.conf import settings
 from django.db.models import Q
 from django.db.models import Q
-from taggit.models import Tag
+
+from extras.models import Tag
 
 
 
 
 class NumericInFilter(django_filters.BaseInFilter, django_filters.NumberFilter):
 class NumericInFilter(django_filters.BaseInFilter, django_filters.NumberFilter):

+ 1 - 1
netbox/utilities/views.py

@@ -157,7 +157,7 @@ class ObjectListView(View):
 
 
         # Construct queryset for tags list
         # Construct queryset for tags list
         if hasattr(model, 'tags'):
         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:
         else:
             tags = None
             tags = None
 
 

+ 25 - 0
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'),
+        ),
+    ]

+ 3 - 3
netbox/virtualization/models.py

@@ -6,7 +6,7 @@ from django.urls import reverse
 from taggit.managers import TaggableManager
 from taggit.managers import TaggableManager
 
 
 from dcim.models import Device
 from dcim.models import Device
-from extras.models import ConfigContextModel, CustomFieldModel
+from extras.models import ConfigContextModel, CustomFieldModel, TaggedItem
 from utilities.models import ChangeLoggedModel
 from utilities.models import ChangeLoggedModel
 from .constants import DEVICE_STATUS_ACTIVE, VM_STATUS_CHOICES, VM_STATUS_CLASSES
 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'
         object_id_field='obj_id'
     )
     )
 
 
-    tags = TaggableManager()
+    tags = TaggableManager(through=TaggedItem)
 
 
     csv_headers = ['name', 'type', 'group', 'site', 'comments']
     csv_headers = ['name', 'type', 'group', 'site', 'comments']
 
 
@@ -238,7 +238,7 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
         object_id_field='obj_id'
         object_id_field='obj_id'
     )
     )
 
 
-    tags = TaggableManager()
+    tags = TaggableManager(through=TaggedItem)
 
 
     csv_headers = [
     csv_headers = [
         'name', 'status', 'role', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',
         'name', 'status', 'role', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',