Explorar o código

Added API views & tests for tags

Jeremy Stretch %!s(int64=7) %!d(string=hai) anos
pai
achega
dc2f1d7c64

+ 13 - 0
netbox/extras/api/serializers.py

@@ -2,6 +2,7 @@ from __future__ import unicode_literals
 
 
 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.serializers import NestedDeviceSerializer, NestedRackSerializer, NestedSiteSerializer
 from dcim.api.serializers import NestedDeviceSerializer, NestedRackSerializer, NestedSiteSerializer
 from dcim.models import Device, Rack, Site
 from dcim.models import Device, Rack, Site
@@ -62,6 +63,18 @@ class TopologyMapSerializer(ValidatedModelSerializer):
         fields = ['id', 'name', 'slug', 'site', 'device_patterns', 'description']
         fields = ['id', 'name', 'slug', 'site', 'device_patterns', 'description']
 
 
 
 
+#
+# Tags
+#
+
+class TagSerializer(ValidatedModelSerializer):
+    tagged_items = serializers.IntegerField(read_only=True)
+
+    class Meta:
+        model = Tag
+        fields = ['id', 'name', 'slug', 'tagged_items']
+
+
 #
 #
 # Image attachments
 # Image attachments
 #
 #

+ 3 - 0
netbox/extras/api/urls.py

@@ -28,6 +28,9 @@ router.register(r'export-templates', views.ExportTemplateViewSet)
 # Topology maps
 # Topology maps
 router.register(r'topology-maps', views.TopologyMapViewSet)
 router.register(r'topology-maps', views.TopologyMapViewSet)
 
 
+# Tags
+router.register(r'tags', views.TagViewSet)
+
 # Image attachments
 # Image attachments
 router.register(r'image-attachments', views.ImageAttachmentViewSet)
 router.register(r'image-attachments', views.ImageAttachmentViewSet)
 
 

+ 12 - 0
netbox/extras/api/views.py

@@ -1,12 +1,14 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
+from django.db.models import Count
 from django.http import Http404, HttpResponse
 from django.http import Http404, HttpResponse
 from django.shortcuts import get_object_or_404
 from django.shortcuts import get_object_or_404
 from rest_framework.decorators import detail_route
 from rest_framework.decorators import detail_route
 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 CustomField, ExportTemplate, Graph, ImageAttachment, ReportResult, TopologyMap, UserAction
 from extras.models import CustomField, ExportTemplate, Graph, ImageAttachment, ReportResult, TopologyMap, UserAction
@@ -109,6 +111,16 @@ class TopologyMapViewSet(ModelViewSet):
         return response
         return response
 
 
 
 
+#
+# Tags
+#
+
+class TagViewSet(ModelViewSet):
+    queryset = Tag.objects.annotate(tagged_items=Count('taggit_taggeditem_items'))
+    serializer_class = serializers.TagSerializer
+    filter_class = filters.TagFilter
+
+
 #
 #
 # Image attachments
 # Image attachments
 #
 #

+ 21 - 0
netbox/extras/filters.py

@@ -3,6 +3,8 @@ from __future__ import unicode_literals
 import django_filters
 import django_filters
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
+from django.db.models import Q
+from taggit.models import Tag
 
 
 from dcim.models import Site
 from dcim.models import Site
 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
@@ -85,6 +87,25 @@ class ExportTemplateFilter(django_filters.FilterSet):
         fields = ['content_type', 'name']
         fields = ['content_type', 'name']
 
 
 
 
+class TagFilter(django_filters.FilterSet):
+    q = django_filters.CharFilter(
+        method='search',
+        label='Search',
+    )
+
+    class Meta:
+        model = Tag
+        fields = ['name', 'slug']
+
+    def search(self, queryset, name, value):
+        if not value.strip():
+            return queryset
+        return queryset.filter(
+            Q(name__icontains=value) |
+            Q(slug__icontains=value)
+        )
+
+
 class TopologyMapFilter(django_filters.FilterSet):
 class TopologyMapFilter(django_filters.FilterSet):
     site_id = django_filters.ModelMultipleChoiceFilter(
     site_id = django_filters.ModelMultipleChoiceFilter(
         name='site',
         name='site',

+ 94 - 0
netbox/extras/tests/test_api.py

@@ -5,6 +5,7 @@ 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 rest_framework.test import APITestCase
 from rest_framework.test import APITestCase
+from taggit.models import Tag
 
 
 from dcim.models import Device
 from dcim.models import Device
 from extras.constants import GRAPH_TYPE_SITE
 from extras.constants import GRAPH_TYPE_SITE
@@ -226,3 +227,96 @@ class ExportTemplateTest(HttpStatusMixin, APITestCase):
 
 
         self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
         self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
         self.assertEqual(ExportTemplate.objects.count(), 2)
         self.assertEqual(ExportTemplate.objects.count(), 2)
+
+
+class TagTest(HttpStatusMixin, APITestCase):
+
+    def setUp(self):
+
+        user = User.objects.create(username='testuser', is_superuser=True)
+        token = Token.objects.create(user=user)
+        self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
+
+        self.tag1 = Tag.objects.create(name='Test Tag 1', slug='test-tag-1')
+        self.tag2 = Tag.objects.create(name='Test Tag 2', slug='test-tag-2')
+        self.tag3 = Tag.objects.create(name='Test Tag 3', slug='test-tag-3')
+
+    def test_get_tag(self):
+
+        url = reverse('extras-api:tag-detail', kwargs={'pk': self.tag1.pk})
+        response = self.client.get(url, **self.header)
+
+        self.assertEqual(response.data['name'], self.tag1.name)
+
+    def test_list_tags(self):
+
+        url = reverse('extras-api:tag-list')
+        response = self.client.get(url, **self.header)
+
+        self.assertEqual(response.data['count'], 3)
+
+    def test_create_tag(self):
+
+        data = {
+            'name': 'Test Tag 4',
+            'slug': 'test-tag-4',
+        }
+
+        url = reverse('extras-api:tag-list')
+        response = self.client.post(url, data, format='json', **self.header)
+
+        self.assertHttpStatus(response, status.HTTP_201_CREATED)
+        self.assertEqual(Tag.objects.count(), 4)
+        tag4 = Tag.objects.get(pk=response.data['id'])
+        self.assertEqual(tag4.name, data['name'])
+        self.assertEqual(tag4.slug, data['slug'])
+
+    def test_create_tag_bulk(self):
+
+        data = [
+            {
+                'name': 'Test Tag 4',
+                'slug': 'test-tag-4',
+            },
+            {
+                'name': 'Test Tag 5',
+                'slug': 'test-tag-5',
+            },
+            {
+                'name': 'Test Tag 6',
+                'slug': 'test-tag-6',
+            },
+        ]
+
+        url = reverse('extras-api:tag-list')
+        response = self.client.post(url, data, format='json', **self.header)
+
+        self.assertHttpStatus(response, status.HTTP_201_CREATED)
+        self.assertEqual(Tag.objects.count(), 6)
+        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'])
+
+    def test_update_tag(self):
+
+        data = {
+            'name': 'Test Tag X',
+            'slug': 'test-tag-x',
+        }
+
+        url = reverse('extras-api:tag-detail', kwargs={'pk': self.tag1.pk})
+        response = self.client.put(url, data, format='json', **self.header)
+
+        self.assertHttpStatus(response, status.HTTP_200_OK)
+        self.assertEqual(Tag.objects.count(), 3)
+        tag1 = Tag.objects.get(pk=response.data['id'])
+        self.assertEqual(tag1.name, data['name'])
+        self.assertEqual(tag1.slug, data['slug'])
+
+    def test_delete_tag(self):
+
+        url = reverse('extras-api:tag-detail', kwargs={'pk': self.tag1.pk})
+        response = self.client.delete(url, **self.header)
+
+        self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
+        self.assertEqual(Tag.objects.count(), 2)