diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index c8aa7b010..582de3ab1 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -7,7 +7,7 @@ from rest_framework.viewsets import ModelViewSet from circuits import filters from circuits.models import Provider, CircuitTermination, CircuitType, Circuit from extras.models import Graph, GRAPH_TYPE_PROVIDER -from extras.api.serializers import GraphSerializer +from extras.api.serializers import RenderedGraphSerializer from extras.api.views import CustomFieldModelViewSet from utilities.api import WritableSerializerMixin from . import serializers @@ -25,9 +25,12 @@ class ProviderViewSet(WritableSerializerMixin, CustomFieldModelViewSet): @detail_route() def graphs(self, request, pk=None): + """ + A convenience method for rendering graphs for a particular provider. + """ provider = get_object_or_404(Provider, pk=pk) queryset = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER) - serializer = GraphSerializer(queryset, many=True, context={'graphed_object': provider}) + serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': provider}) return Response(serializer.data) diff --git a/netbox/circuits/tests/test_api.py b/netbox/circuits/tests/test_api.py index a6e5fbd57..7bd3d8040 100644 --- a/netbox/circuits/tests/test_api.py +++ b/netbox/circuits/tests/test_api.py @@ -5,6 +5,7 @@ from django.contrib.auth.models import User from django.urls import reverse from dcim.models import Site +from extras.models import Graph, GRAPH_TYPE_PROVIDER from circuits.models import Circuit, CircuitTermination, CircuitType, Provider, TERM_SIDE_A, TERM_SIDE_Z from users.models import Token from utilities.tests import HttpStatusMixin @@ -29,6 +30,27 @@ class ProviderTest(HttpStatusMixin, APITestCase): self.assertEqual(response.data['name'], self.provider1.name) + def test_get_provider_graphs(self): + + self.graph1 = Graph.objects.create( + type=GRAPH_TYPE_PROVIDER, name='Test Graph 1', + source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=1' + ) + self.graph2 = Graph.objects.create( + type=GRAPH_TYPE_PROVIDER, name='Test Graph 2', + source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=2' + ) + self.graph3 = Graph.objects.create( + type=GRAPH_TYPE_PROVIDER, name='Test Graph 3', + source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=3' + ) + + url = reverse('circuits-api:provider-graphs', kwargs={'pk': self.provider1.pk}) + response = self.client.get(url, **self.header) + + self.assertEqual(len(response.data), 3) + self.assertEqual(response.data[0]['embed_url'], 'http://example.com/graphs.py?provider=test-provider-1&foo=1') + def test_list_providers(self): url = reverse('circuits-api:provider-list') diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index ac4c4cb56..38728679a 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -15,7 +15,7 @@ from dcim.models import ( ) from dcim import filters from extras.api.renderers import BINDZoneRenderer, FlatJSONRenderer -from extras.api.serializers import GraphSerializer +from extras.api.serializers import RenderedGraphSerializer from extras.api.views import CustomFieldModelViewSet from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE from utilities.api import ServiceUnavailable, WritableSerializerMixin @@ -45,9 +45,12 @@ class SiteViewSet(WritableSerializerMixin, CustomFieldModelViewSet): @detail_route() def graphs(self, request, pk=None): + """ + A convenience method for rendering graphs for a particular site. + """ site = get_object_or_404(Site, pk=pk) queryset = Graph.objects.filter(type=GRAPH_TYPE_SITE) - serializer = GraphSerializer(queryset, many=True, context={'graphed_object': site}) + serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': site}) return Response(serializer.data) @@ -278,9 +281,12 @@ class InterfaceViewSet(WritableSerializerMixin, ModelViewSet): @detail_route() def graphs(self, request, pk=None): + """ + A convenience method for rendering graphs for a particular interface. + """ interface = get_object_or_404(Interface, pk=pk) queryset = Graph.objects.filter(type=GRAPH_TYPE_INTERFACE) - serializer = GraphSerializer(queryset, many=True, context={'graphed_object': interface}) + serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': interface}) return Response(serializer.data) diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 74751d8be..628973cdd 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -10,6 +10,7 @@ from dcim.models import ( Manufacturer, Module, Platform, PowerPort, PowerPortTemplate, PowerOutlet, PowerOutletTemplate, Rack, RackGroup, RackReservation, RackRole, Region, Site, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT, ) +from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE from users.models import Token from utilities.tests import HttpStatusMixin @@ -102,6 +103,27 @@ class SiteTest(HttpStatusMixin, APITestCase): self.assertEqual(response.data['name'], self.site1.name) + def test_get_site_graphs(self): + + self.graph1 = Graph.objects.create( + type=GRAPH_TYPE_SITE, name='Test Graph 1', + source='http://example.com/graphs.py?site={{ obj.slug }}&foo=1' + ) + self.graph2 = Graph.objects.create( + type=GRAPH_TYPE_SITE, name='Test Graph 2', + source='http://example.com/graphs.py?site={{ obj.slug }}&foo=2' + ) + self.graph3 = Graph.objects.create( + type=GRAPH_TYPE_SITE, name='Test Graph 3', + source='http://example.com/graphs.py?site={{ obj.slug }}&foo=3' + ) + + url = reverse('dcim-api:site-graphs', kwargs={'pk': self.site1.pk}) + response = self.client.get(url, **self.header) + + self.assertEqual(len(response.data), 3) + self.assertEqual(response.data[0]['embed_url'], 'http://example.com/graphs.py?site=test-site-1&foo=1') + def test_list_sites(self): url = reverse('dcim-api:site-list') @@ -1655,6 +1677,27 @@ class InterfaceTest(HttpStatusMixin, APITestCase): self.assertEqual(response.data['name'], self.interface1.name) + def test_get_interface_graphs(self): + + self.graph1 = Graph.objects.create( + type=GRAPH_TYPE_INTERFACE, name='Test Graph 1', + source='http://example.com/graphs.py?interface={{ obj.name }}&foo=1' + ) + self.graph2 = Graph.objects.create( + type=GRAPH_TYPE_INTERFACE, name='Test Graph 2', + source='http://example.com/graphs.py?interface={{ obj.name }}&foo=2' + ) + self.graph3 = Graph.objects.create( + type=GRAPH_TYPE_INTERFACE, name='Test Graph 3', + source='http://example.com/graphs.py?interface={{ obj.name }}&foo=3' + ) + + url = reverse('dcim-api:interface-graphs', kwargs={'pk': self.interface1.pk}) + response = self.client.get(url, **self.header) + + self.assertEqual(len(response.data), 3) + self.assertEqual(response.data[0]['embed_url'], 'http://example.com/graphs.py?interface=Test Interface 1&foo=1') + def test_list_interfaces(self): url = reverse('dcim-api:interface-list') diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 5348dcc10..a7d081c5c 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -1,7 +1,7 @@ from rest_framework import serializers from dcim.api.serializers import NestedSiteSerializer -from extras.models import ACTION_CHOICES, Graph, TopologyMap, UserAction +from extras.models import ACTION_CHOICES, Graph, GRAPH_TYPE_CHOICES, TopologyMap, UserAction from users.api.serializers import NestedUserSerializer from utilities.api import ChoiceFieldSerializer @@ -11,12 +11,28 @@ from utilities.api import ChoiceFieldSerializer # class GraphSerializer(serializers.ModelSerializer): - embed_url = serializers.SerializerMethodField() - embed_link = serializers.SerializerMethodField() + type = ChoiceFieldSerializer(choices=GRAPH_TYPE_CHOICES) class Meta: model = Graph - fields = ['name', 'embed_url', 'embed_link'] + fields = ['id', 'type', 'weight', 'name', 'source', 'link'] + + +class WritableGraphSerializer(serializers.ModelSerializer): + + class Meta: + model = Graph + fields = ['id', 'type', 'weight', 'name', 'source', 'link'] + + +class RenderedGraphSerializer(serializers.ModelSerializer): + embed_url = serializers.SerializerMethodField() + embed_link = serializers.SerializerMethodField() + type = ChoiceFieldSerializer(choices=GRAPH_TYPE_CHOICES) + + class Meta: + model = Graph + fields = ['id', 'type', 'weight', 'name', 'embed_url', 'embed_link'] def get_embed_url(self, obj): return obj.embed_url(self.context['graphed_object']) diff --git a/netbox/extras/api/urls.py b/netbox/extras/api/urls.py index 141b4b7e4..ced4035c1 100644 --- a/netbox/extras/api/urls.py +++ b/netbox/extras/api/urls.py @@ -5,6 +5,9 @@ from . import views router = routers.DefaultRouter() +# Graphs +router.register(r'graphs', views.GraphViewSet) + # Topology maps router.register(r'topology-maps', views.TopologyMapViewSet) diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index 93bc724ec..5b9ea5afb 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -6,7 +6,7 @@ from django.http import HttpResponse from django.shortcuts import get_object_or_404 from extras import filters -from extras.models import TopologyMap, UserAction +from extras.models import Graph, TopologyMap, UserAction from utilities.api import WritableSerializerMixin from . import serializers @@ -41,6 +41,13 @@ class CustomFieldModelViewSet(ModelViewSet): return super(CustomFieldModelViewSet, self).get_queryset().prefetch_related('custom_field_values__field') +class GraphViewSet(WritableSerializerMixin, ModelViewSet): + queryset = Graph.objects.all() + serializer_class = serializers.GraphSerializer + write_serializer_class = serializers.WritableGraphSerializer + filter_class = filters.GraphFilter + + class TopologyMapViewSet(WritableSerializerMixin, ModelViewSet): queryset = TopologyMap.objects.select_related('site') serializer_class = serializers.TopologyMapSerializer diff --git a/netbox/extras/filters.py b/netbox/extras/filters.py index 609a0789a..740296c9e 100644 --- a/netbox/extras/filters.py +++ b/netbox/extras/filters.py @@ -4,7 +4,7 @@ from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from dcim.models import Site -from .models import CF_TYPE_SELECT, CustomField, TopologyMap, UserAction +from .models import CF_TYPE_SELECT, CustomField, Graph, TopologyMap, UserAction class CustomFieldFilter(django_filters.Filter): @@ -48,6 +48,13 @@ class CustomFieldFilterSet(django_filters.FilterSet): self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(name=cf.name, cf_type=cf.type) +class GraphFilter(django_filters.FilterSet): + + class Meta: + model = Graph + fields = ['type', 'name'] + + class TopologyMapFilter(django_filters.FilterSet): site_id = django_filters.ModelMultipleChoiceFilter( name='site', diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py new file mode 100644 index 000000000..8594e5faa --- /dev/null +++ b/netbox/extras/tests/test_api.py @@ -0,0 +1,86 @@ +from rest_framework import status +from rest_framework.test import APITestCase + +from django.contrib.auth.models import User +from django.urls import reverse + +from extras.models import Graph, GRAPH_TYPE_SITE +from users.models import Token +from utilities.tests import HttpStatusMixin + + +class GraphTest(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.graph1 = Graph.objects.create( + type=GRAPH_TYPE_SITE, name='Test Graph 1', source='http://example.com/graphs.py?site={{ obj.name }}&foo=1' + ) + self.graph2 = Graph.objects.create( + type=GRAPH_TYPE_SITE, name='Test Graph 2', source='http://example.com/graphs.py?site={{ obj.name }}&foo=2' + ) + self.graph3 = Graph.objects.create( + type=GRAPH_TYPE_SITE, name='Test Graph 3', source='http://example.com/graphs.py?site={{ obj.name }}&foo=3' + ) + + def test_get_graph(self): + + url = reverse('extras-api:graph-detail', kwargs={'pk': self.graph1.pk}) + response = self.client.get(url, **self.header) + + self.assertEqual(response.data['name'], self.graph1.name) + + def test_list_graphs(self): + + url = reverse('extras-api:graph-list') + response = self.client.get(url, **self.header) + + self.assertEqual(response.data['count'], 3) + + def test_create_graph(self): + + data = { + 'type': GRAPH_TYPE_SITE, + 'name': 'Test Graph 4', + 'source': 'http://example.com/graphs.py?site={{ obj.name }}&foo=4', + } + + url = reverse('extras-api:graph-list') + response = self.client.post(url, data, **self.header) + + self.assertHttpStatus(response, status.HTTP_201_CREATED) + self.assertEqual(Graph.objects.count(), 4) + graph4 = Graph.objects.get(pk=response.data['id']) + self.assertEqual(graph4.type, data['type']) + self.assertEqual(graph4.name, data['name']) + self.assertEqual(graph4.source, data['source']) + + def test_update_graph(self): + + data = { + 'type': GRAPH_TYPE_SITE, + 'name': 'Test Graph X', + 'source': 'http://example.com/graphs.py?site={{ obj.name }}&foo=99', + } + + url = reverse('extras-api:graph-detail', kwargs={'pk': self.graph1.pk}) + response = self.client.put(url, data, **self.header) + + self.assertHttpStatus(response, status.HTTP_200_OK) + self.assertEqual(Graph.objects.count(), 3) + graph1 = Graph.objects.get(pk=response.data['id']) + self.assertEqual(graph1.type, data['type']) + self.assertEqual(graph1.name, data['name']) + self.assertEqual(graph1.source, data['source']) + + def test_delete_graph(self): + + url = reverse('extras-api:graph-detail', kwargs={'pk': self.graph1.pk}) + response = self.client.delete(url, **self.header) + + self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT) + self.assertEqual(Graph.objects.count(), 2)