1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

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

This commit is contained in:
John Anderson
2019-02-20 03:52:47 -05:00
parent e521508de9
commit fc2bb724fa
32 changed files with 524 additions and 63 deletions

View File

@ -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)

View File

@ -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'),
),
]

View File

@ -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',

View File

@ -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'),
),
]

View File

@ -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']

View File

@ -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']
# #

View File

@ -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

View File

@ -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):

View File

@ -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'
] ]

View File

@ -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
)
]

View File

@ -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'
),
]

View File

@ -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
)
]

View File

@ -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),
),
]

View File

@ -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
)

View File

@ -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 utilities.tables import BaseTable, BooleanColumn, ColorColumn, ToggleColumn
from .models import ConfigContext, ObjectChange 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):

View File

@ -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

View File

@ -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):

View File

@ -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'
) )

View File

@ -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'),
),
]

View File

@ -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']

View File

@ -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.admin import TagAdmin, TaggedItemInline
from taggit.models import Tag
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:

View File

@ -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'),
),
]

View File

@ -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']

View File

@ -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' %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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'),
),
]

View File

@ -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']

View File

@ -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):

View File

@ -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

View File

@ -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'),
),
]

View File

@ -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',