Description |
- {{ tag.description }}
+ {{ tag.description|placeholder }}
|
diff --git a/netbox/templates/inc/nav_menu.html b/netbox/templates/inc/nav_menu.html
index c862b1faa..db8442821 100644
--- a/netbox/templates/inc/nav_menu.html
+++ b/netbox/templates/inc/nav_menu.html
@@ -102,6 +102,12 @@
+ {% if perms.extras.add_tag %}
+
+ {% endif %}
Tags
diff --git a/netbox/tenancy/api/serializers.py b/netbox/tenancy/api/serializers.py
index 9c7a099e4..4454ac776 100644
--- a/netbox/tenancy/api/serializers.py
+++ b/netbox/tenancy/api/serializers.py
@@ -1,7 +1,7 @@
from rest_framework import serializers
-from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
from extras.api.customfields import CustomFieldModelSerializer
+from extras.api.serializers import TaggedObjectSerializer
from tenancy.models import Tenant, TenantGroup
from utilities.api import ValidatedModelSerializer
from .nested_serializers import *
@@ -20,9 +20,8 @@ class TenantGroupSerializer(ValidatedModelSerializer):
fields = ['id', 'name', 'slug', 'parent', 'description', 'tenant_count']
-class TenantSerializer(TaggitSerializer, CustomFieldModelSerializer):
+class TenantSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
group = NestedTenantGroupSerializer(required=False)
- tags = TagListSerializerField(required=False)
circuit_count = serializers.IntegerField(read_only=True)
device_count = serializers.IntegerField(read_only=True)
ipaddress_count = serializers.IntegerField(read_only=True)
diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py
index bf100f43a..5bd0657b6 100644
--- a/netbox/tenancy/forms.py
+++ b/netbox/tenancy/forms.py
@@ -2,8 +2,8 @@ from django import forms
from extras.forms import (
AddRemoveTagsForm, CustomFieldModelForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelCSVForm,
- TagField,
)
+from extras.models import Tag
from utilities.forms import (
APISelect, APISelectMultiple, BootstrapMixin, CommentField, CSVModelChoiceField, CSVModelForm,
DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField, TagFilterField,
@@ -57,7 +57,8 @@ class TenantForm(BootstrapMixin, CustomFieldModelForm):
required=False
)
comments = CommentField()
- tags = TagField(
+ tags = DynamicModelMultipleChoiceField(
+ queryset=Tag.objects.all(),
required=False
)
diff --git a/netbox/tenancy/tests/test_views.py b/netbox/tenancy/tests/test_views.py
index ca2c2633f..4e00b648c 100644
--- a/netbox/tenancy/tests/test_views.py
+++ b/netbox/tenancy/tests/test_views.py
@@ -55,7 +55,7 @@ class TenantTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'group': tenant_groups[1].pk,
'description': 'A new tenant',
'comments': 'Some comments',
- 'tags': 'Alpha,Bravo,Charlie',
+ 'tags': cls.create_tags('Alpha', 'Bravo', 'Charlie'),
}
cls.csv_data = (
diff --git a/netbox/utilities/custom_inspectors.py b/netbox/utilities/custom_inspectors.py
index 2cbe1cfc5..14463de23 100644
--- a/netbox/utilities/custom_inspectors.py
+++ b/netbox/utilities/custom_inspectors.py
@@ -1,10 +1,9 @@
from django.contrib.postgres.fields import JSONField
from drf_yasg import openapi
-from drf_yasg.inspectors import FieldInspector, NotHandled, PaginatorInspector, FilterInspector, SwaggerAutoSchema
+from drf_yasg.inspectors import FieldInspector, NotHandled, PaginatorInspector, SwaggerAutoSchema
from drf_yasg.utils import get_serializer_ref_name
from rest_framework.fields import ChoiceField
from rest_framework.relations import ManyRelatedField
-from taggit_serializer.serializers import TagListSerializerField
from dcim.api.serializers import InterfaceSerializer as DeviceInterfaceSerializer
from extras.api.customfields import CustomFieldsSerializer
@@ -56,19 +55,6 @@ class SerializedPKRelatedFieldInspector(FieldInspector):
return NotHandled
-class TagListFieldInspector(FieldInspector):
- def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
- SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
- if isinstance(field, TagListSerializerField):
- child_schema = self.probe_field_inspectors(field.child, ChildSwaggerType, use_references)
- return SwaggerType(
- type=openapi.TYPE_ARRAY,
- items=child_schema,
- )
-
- return NotHandled
-
-
class CustomChoiceFieldInspector(FieldInspector):
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
# this returns a callable which extracts title, description and other stuff
diff --git a/netbox/utilities/querysets.py b/netbox/utilities/querysets.py
index 558e926a9..04bc4c542 100644
--- a/netbox/utilities/querysets.py
+++ b/netbox/utilities/querysets.py
@@ -12,6 +12,9 @@ class DummyQuerySet:
def __init__(self, queryset):
self._cache = [obj for obj in queryset.all()]
+ def __iter__(self):
+ return iter(self._cache)
+
def all(self):
return self._cache
diff --git a/netbox/utilities/testing/utils.py b/netbox/utilities/testing/utils.py
index fd8c70f05..d763012f0 100644
--- a/netbox/utilities/testing/utils.py
+++ b/netbox/utilities/testing/utils.py
@@ -14,7 +14,14 @@ def post_data(data):
if value is None:
ret[key] = ''
elif type(value) in (list, tuple):
- ret[key] = value
+ if value and hasattr(value[0], 'pk'):
+ # Value is a list of instances
+ ret[key] = [v.pk for v in value]
+ else:
+ ret[key] = value
+ elif hasattr(value, 'pk'):
+ # Value is an instance
+ ret[key] = value.pk
else:
ret[key] = str(value)
diff --git a/netbox/utilities/testing/views.py b/netbox/utilities/testing/views.py
index b3b98605d..e3c1f0720 100644
--- a/netbox/utilities/testing/views.py
+++ b/netbox/utilities/testing/views.py
@@ -6,8 +6,10 @@ from django.db.models import ForeignKey, ManyToManyField
from django.forms.models import model_to_dict
from django.test import Client, TestCase as _TestCase, override_settings
from django.urls import reverse, NoReverseMatch
+from django.utils.text import slugify
from netaddr import IPNetwork
+from extras.models import Tag
from users.models import ObjectPermission
from utilities.permissions import resolve_permission_ct
from .utils import disable_warnings, post_data
@@ -49,7 +51,7 @@ class TestCase(_TestCase):
obj_perm.object_types.add(ct)
#
- # Convenience methods
+ # Custom assertions
#
def assertHttpStatus(self, response, expected_status):
@@ -75,7 +77,7 @@ class TestCase(_TestCase):
# TODO: Differentiate between tags assigned to the instance and a M2M field for tags (ex: ConfigContext)
if key == 'tags':
- model_dict[key] = ','.join(sorted([tag.name for tag in value]))
+ model_dict[key] = sorted(value)
# Convert ManyToManyField to list of instance PKs
elif model_dict[key] and type(value) in (list, tuple) and hasattr(value[0], 'pk'):
@@ -108,6 +110,19 @@ class TestCase(_TestCase):
self.assertDictEqual(model_dict, relevant_data)
+ #
+ # Convenience methods
+ #
+
+ @classmethod
+ def create_tags(cls, *names):
+ """
+ Create and return a Tag instance for each name given.
+ """
+ tags = [Tag(name=name, slug=slugify(name)) for name in names]
+ Tag.objects.bulk_create(tags)
+ return tags
+
#
# UI Tests
diff --git a/netbox/virtualization/api/serializers.py b/netbox/virtualization/api/serializers.py
index 3cca95b22..008c6dd88 100644
--- a/netbox/virtualization/api/serializers.py
+++ b/netbox/virtualization/api/serializers.py
@@ -1,11 +1,11 @@
from drf_yasg.utils import swagger_serializer_method
from rest_framework import serializers
-from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
from dcim.api.nested_serializers import NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer
-from dcim.choices import InterfaceModeChoices, InterfaceTypeChoices
+from dcim.choices import InterfaceModeChoices
from dcim.models import Interface
from extras.api.customfields import CustomFieldModelSerializer
+from extras.api.serializers import TaggedObjectSerializer
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
from ipam.models import VLAN
from tenancy.api.nested_serializers import NestedTenantSerializer
@@ -35,12 +35,11 @@ class ClusterGroupSerializer(ValidatedModelSerializer):
fields = ['id', 'name', 'slug', 'description', 'cluster_count']
-class ClusterSerializer(TaggitSerializer, CustomFieldModelSerializer):
+class ClusterSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
type = NestedClusterTypeSerializer()
group = NestedClusterGroupSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(required=False, allow_null=True)
site = NestedSiteSerializer(required=False, allow_null=True)
- tags = TagListSerializerField(required=False)
device_count = serializers.IntegerField(read_only=True)
virtualmachine_count = serializers.IntegerField(read_only=True)
@@ -56,7 +55,7 @@ class ClusterSerializer(TaggitSerializer, CustomFieldModelSerializer):
# Virtual machines
#
-class VirtualMachineSerializer(TaggitSerializer, CustomFieldModelSerializer):
+class VirtualMachineSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
status = ChoiceField(choices=VirtualMachineStatusChoices, required=False)
site = NestedSiteSerializer(read_only=True)
cluster = NestedClusterSerializer()
@@ -66,7 +65,6 @@ class VirtualMachineSerializer(TaggitSerializer, CustomFieldModelSerializer):
primary_ip = NestedIPAddressSerializer(read_only=True)
primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
- tags = TagListSerializerField(required=False)
class Meta:
model = VirtualMachine
@@ -97,7 +95,7 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer):
# VM interfaces
#
-class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
+class InterfaceSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
virtual_machine = NestedVirtualMachineSerializer()
type = ChoiceField(choices=VMInterfaceTypeChoices, default=VMInterfaceTypeChoices.TYPE_VIRTUAL, required=False)
mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
@@ -108,7 +106,6 @@ class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
required=False,
many=True
)
- tags = TagListSerializerField(required=False)
class Meta:
model = Interface
diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py
index 2f2ee4950..942368f19 100644
--- a/netbox/virtualization/forms.py
+++ b/netbox/virtualization/forms.py
@@ -7,8 +7,8 @@ from dcim.forms import INTERFACE_MODE_HELP_TEXT
from dcim.models import Device, DeviceRole, Interface, Platform, Rack, Region, Site
from extras.forms import (
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelForm, CustomFieldFilterForm,
- TagField,
)
+from extras.models import Tag
from ipam.models import IPAddress, VLAN
from tenancy.forms import TenancyFilterForm, TenancyForm
from tenancy.models import Tenant
@@ -83,7 +83,8 @@ class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
required=False
)
comments = CommentField()
- tags = TagField(
+ tags = DynamicModelMultipleChoiceField(
+ queryset=Tag.objects.all(),
required=False
)
@@ -312,13 +313,14 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
queryset=Platform.objects.all(),
required=False
)
- tags = TagField(
- required=False
- )
local_context_data = JSONField(
required=False,
label=''
)
+ tags = DynamicModelMultipleChoiceField(
+ queryset=Tag.objects.all(),
+ required=False
+ )
class Meta:
model = VirtualMachine
@@ -590,7 +592,8 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
},
)
)
- tags = TagField(
+ tags = DynamicModelMultipleChoiceField(
+ queryset=Tag.objects.all(),
required=False
)
@@ -697,7 +700,8 @@ class InterfaceCreateForm(BootstrapMixin, forms.Form):
},
)
)
- tags = TagField(
+ tags = DynamicModelMultipleChoiceField(
+ queryset=Tag.objects.all(),
required=False
)
diff --git a/netbox/virtualization/tests/test_views.py b/netbox/virtualization/tests/test_views.py
index b6f5be8b2..0ccf8a9b1 100644
--- a/netbox/virtualization/tests/test_views.py
+++ b/netbox/virtualization/tests/test_views.py
@@ -97,7 +97,7 @@ class ClusterTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'tenant': None,
'site': sites[1].pk,
'comments': 'Some comments',
- 'tags': 'Alpha,Bravo,Charlie',
+ 'tags': cls.create_tags('Alpha', 'Bravo', 'Charlie'),
}
cls.csv_data = (
@@ -161,7 +161,7 @@ class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'memory': 32768,
'disk': 4000,
'comments': 'Some comments',
- 'tags': 'Alpha,Bravo,Charlie',
+ 'tags': cls.create_tags('Alpha', 'Bravo', 'Charlie'),
'local_context_data': None,
}
@@ -228,6 +228,8 @@ class InterfaceTestCase(
)
VLAN.objects.bulk_create(vlans)
+ tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
+
cls.form_data = {
'virtual_machine': virtualmachines[1].pk,
'name': 'Interface X',
@@ -240,7 +242,7 @@ class InterfaceTestCase(
'mode': InterfaceModeChoices.MODE_TAGGED,
'untagged_vlan': vlans[0].pk,
'tagged_vlans': [v.pk for v in vlans[1:4]],
- 'tags': 'Alpha,Bravo,Charlie',
+ 'tags': tags,
}
cls.bulk_create_data = {
@@ -255,7 +257,7 @@ class InterfaceTestCase(
'mode': InterfaceModeChoices.MODE_TAGGED,
'untagged_vlan': vlans[0].pk,
'tagged_vlans': [v.pk for v in vlans[1:4]],
- 'tags': 'Alpha,Bravo,Charlie',
+ 'tags': tags,
}
cls.bulk_edit_data = {
diff --git a/requirements.txt b/requirements.txt
index 79e4fdd9f..eac5ca9d4 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -9,7 +9,6 @@ django-prometheus==2.0.0
django-rq==2.3.2
django-tables2==2.3.1
django-taggit==1.2.0
-django-taggit-serializer==0.1.7
django-timezone-field==4.0
djangorestframework==3.11.0
drf-yasg[validation]==1.17.1