{% include 'panel_table.html' with table=items_table heading='Tagged Objects' %}
diff --git a/netbox/templates/extras/tag_edit.html b/netbox/templates/extras/tag_edit.html
new file mode 100644
index 000000000..800db1d26
--- /dev/null
+++ b/netbox/templates/extras/tag_edit.html
@@ -0,0 +1,19 @@
+{% extends 'utilities/obj_edit.html' %}
+{% load form_helpers %}
+
+{% block form %}
+
+
Tag
+
+ {% render_field form.name %}
+ {% render_field form.slug %}
+ {% render_field form.color %}
+
+
+
+
Comments
+
+ {% render_field form.comments %}
+
+
+{% endblock %}
diff --git a/netbox/templates/utilities/templatetags/tag.html b/netbox/templates/utilities/templatetags/tag.html
index 09b885d42..0be4e7ca5 100644
--- a/netbox/templates/utilities/templatetags/tag.html
+++ b/netbox/templates/utilities/templatetags/tag.html
@@ -1,5 +1,7 @@
+{% load helpers %}
+
{% if url_name %}
-
{{ tag }}
+
{{ tag }}
{% else %}
{{ tag }}
{% endif %}
diff --git a/netbox/tenancy/migrations/0006_custom_tag_models.py b/netbox/tenancy/migrations/0006_custom_tag_models.py
new file mode 100644
index 000000000..9104398ad
--- /dev/null
+++ b/netbox/tenancy/migrations/0006_custom_tag_models.py
@@ -0,0 +1,20 @@
+# Generated by Django 2.1.4 on 2019-02-20 06:56
+
+from django.db import migrations
+import taggit.managers
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('tenancy', '0005_change_logging'),
+ ('extras', '0017_tag_taggeditem'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='tenant',
+ name='tags',
+ field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
+ ),
+ ]
diff --git a/netbox/tenancy/models.py b/netbox/tenancy/models.py
index 045679b90..bc67804d6 100644
--- a/netbox/tenancy/models.py
+++ b/netbox/tenancy/models.py
@@ -3,7 +3,7 @@ from django.db import models
from django.urls import reverse
from taggit.managers import TaggableManager
-from extras.models import CustomFieldModel
+from extras.models import CustomFieldModel, TaggedItem
from utilities.models import ChangeLoggedModel
@@ -70,7 +70,7 @@ class Tenant(ChangeLoggedModel, CustomFieldModel):
object_id_field='obj_id'
)
- tags = TaggableManager()
+ tags = TaggableManager(through=TaggedItem)
csv_headers = ['name', 'slug', 'group', 'description', 'comments']
diff --git a/netbox/utilities/filters.py b/netbox/utilities/filters.py
index 40e687077..674aee639 100644
--- a/netbox/utilities/filters.py
+++ b/netbox/utilities/filters.py
@@ -1,7 +1,8 @@
import django_filters
from django.conf import settings
from django.db.models import Q
-from taggit.models import Tag
+
+from extras.models import Tag
class NumericInFilter(django_filters.BaseInFilter, django_filters.NumberFilter):
diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py
index f52f4ea9e..02441549f 100644
--- a/netbox/utilities/views.py
+++ b/netbox/utilities/views.py
@@ -157,7 +157,7 @@ class ObjectListView(View):
# Construct queryset for tags list
if hasattr(model, 'tags'):
- tags = model.tags.annotate(count=Count('taggit_taggeditem_items')).order_by('name')
+ tags = model.tags.annotate(count=Count('extras_taggeditem_items')).order_by('name')
else:
tags = None
diff --git a/netbox/virtualization/api/views.py b/netbox/virtualization/api/views.py
index 3b0c02b22..ce7ee4934 100644
--- a/netbox/virtualization/api/views.py
+++ b/netbox/virtualization/api/views.py
@@ -50,16 +50,23 @@ class VirtualMachineViewSet(CustomFieldModelViewSet):
def get_serializer_class(self):
"""
- Include rendered config context when retrieving a single VirtualMachine.
+ Select the specific serializer based on the request context.
+
+ If the `brief` query param equates to True, return the NestedVirtualMachineSerializer
+
+ If the `exclude` query param includes `config_context` as a value, return the VirtualMachineSerializer
+
+ Else, return the VirtualMachineWithConfigContextSerializer
"""
- if self.action == 'retrieve':
- return serializers.VirtualMachineWithConfigContextSerializer
request = self.get_serializer_context()['request']
if request.query_params.get('brief', False):
return serializers.NestedVirtualMachineSerializer
- return serializers.VirtualMachineSerializer
+ elif 'config_context' in request.query_params.get('exclude', []):
+ return serializers.VirtualMachineSerializer
+
+ return serializers.VirtualMachineWithConfigContextSerializer
class InterfaceViewSet(ModelViewSet):
diff --git a/netbox/virtualization/migrations/0009_custom_tag_models.py b/netbox/virtualization/migrations/0009_custom_tag_models.py
new file mode 100644
index 000000000..883d844f7
--- /dev/null
+++ b/netbox/virtualization/migrations/0009_custom_tag_models.py
@@ -0,0 +1,25 @@
+# Generated by Django 2.1.4 on 2019-02-20 06:56
+
+from django.db import migrations
+import taggit.managers
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('virtualization', '0008_virtualmachine_local_context_data'),
+ ('extras', '0017_tag_taggeditem'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='cluster',
+ name='tags',
+ field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
+ ),
+ migrations.AlterField(
+ model_name='virtualmachine',
+ name='tags',
+ field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
+ ),
+ ]
diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py
index ff9f39ee9..2ef782dfd 100644
--- a/netbox/virtualization/models.py
+++ b/netbox/virtualization/models.py
@@ -6,7 +6,7 @@ from django.urls import reverse
from taggit.managers import TaggableManager
from dcim.models import Device
-from extras.models import ConfigContextModel, CustomFieldModel
+from extras.models import ConfigContextModel, CustomFieldModel, TaggedItem
from utilities.models import ChangeLoggedModel
from .constants import DEVICE_STATUS_ACTIVE, VM_STATUS_CHOICES, VM_STATUS_CLASSES
@@ -119,7 +119,7 @@ class Cluster(ChangeLoggedModel, CustomFieldModel):
object_id_field='obj_id'
)
- tags = TaggableManager()
+ tags = TaggableManager(through=TaggedItem)
csv_headers = ['name', 'type', 'group', 'site', 'comments']
@@ -238,7 +238,7 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
object_id_field='obj_id'
)
- tags = TaggableManager()
+ tags = TaggableManager(through=TaggedItem)
csv_headers = [
'name', 'status', 'role', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',
diff --git a/netbox/virtualization/tests/test_api.py b/netbox/virtualization/tests/test_api.py
index 91792f8fb..d618ebe30 100644
--- a/netbox/virtualization/tests/test_api.py
+++ b/netbox/virtualization/tests/test_api.py
@@ -337,6 +337,14 @@ class VirtualMachineTest(APITestCase):
self.virtualmachine1 = VirtualMachine.objects.create(name='Test Virtual Machine 1', cluster=self.cluster1)
self.virtualmachine2 = VirtualMachine.objects.create(name='Test Virtual Machine 2', cluster=self.cluster1)
self.virtualmachine3 = VirtualMachine.objects.create(name='Test Virtual Machine 3', cluster=self.cluster1)
+ self.virtualmachine_with_context_data = VirtualMachine.objects.create(
+ name='VM with context data',
+ cluster=self.cluster1,
+ local_context_data={
+ 'A': 1,
+ 'B': 2
+ }
+ )
def test_get_virtualmachine(self):
@@ -350,7 +358,7 @@ class VirtualMachineTest(APITestCase):
url = reverse('virtualization-api:virtualmachine-list')
response = self.client.get(url, **self.header)
- self.assertEqual(response.data['count'], 3)
+ self.assertEqual(response.data['count'], 4)
def test_list_virtualmachines_brief(self):
@@ -373,7 +381,7 @@ class VirtualMachineTest(APITestCase):
response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
- self.assertEqual(VirtualMachine.objects.count(), 4)
+ self.assertEqual(VirtualMachine.objects.count(), 5)
virtualmachine4 = VirtualMachine.objects.get(pk=response.data['id'])
self.assertEqual(virtualmachine4.name, data['name'])
self.assertEqual(virtualmachine4.cluster.pk, data['cluster'])
@@ -388,7 +396,7 @@ class VirtualMachineTest(APITestCase):
response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
- self.assertEqual(VirtualMachine.objects.count(), 3)
+ self.assertEqual(VirtualMachine.objects.count(), 4)
def test_create_virtualmachine_bulk(self):
@@ -411,7 +419,7 @@ class VirtualMachineTest(APITestCase):
response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
- self.assertEqual(VirtualMachine.objects.count(), 6)
+ self.assertEqual(VirtualMachine.objects.count(), 7)
self.assertEqual(response.data[0]['name'], data[0]['name'])
self.assertEqual(response.data[1]['name'], data[1]['name'])
self.assertEqual(response.data[2]['name'], data[2]['name'])
@@ -438,7 +446,7 @@ class VirtualMachineTest(APITestCase):
response = self.client.put(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
- self.assertEqual(VirtualMachine.objects.count(), 3)
+ self.assertEqual(VirtualMachine.objects.count(), 4)
virtualmachine1 = VirtualMachine.objects.get(pk=response.data['id'])
self.assertEqual(virtualmachine1.name, data['name'])
self.assertEqual(virtualmachine1.cluster.pk, data['cluster'])
@@ -451,7 +459,22 @@ class VirtualMachineTest(APITestCase):
response = self.client.delete(url, **self.header)
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
- self.assertEqual(VirtualMachine.objects.count(), 2)
+ self.assertEqual(VirtualMachine.objects.count(), 3)
+
+ def test_config_context_included_by_default_in_list_view(self):
+
+ url = reverse('virtualization-api:virtualmachine-list')
+ url = '{}?id__in={}'.format(url, self.virtualmachine_with_context_data.pk)
+ response = self.client.get(url, **self.header)
+
+ self.assertEqual(response.data['results'][0].get('config_context', {}).get('A'), 1)
+
+ def test_config_context_excluded(self):
+
+ url = reverse('virtualization-api:virtualmachine-list') + '?exclude=config_context'
+ response = self.client.get(url, **self.header)
+
+ self.assertFalse('config_context' in response.data['results'][0])
class InterfaceTest(APITestCase):