From ec66e1a5c08dac7b4e039f0e756f3621500fc2c0 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 21 Aug 2020 11:57:46 -0400 Subject: [PATCH] Closes #4349: Drop support for embedded graphs --- docs/additional-features/graphs.md | 30 --------- docs/release-notes/version-2.10.md | 3 + mkdocs.yml | 1 - netbox/circuits/api/views.py | 15 ----- netbox/circuits/models.py | 2 +- netbox/circuits/tests/test_api.py | 24 ------- netbox/circuits/views.py | 5 +- netbox/dcim/api/views.py | 33 ---------- netbox/dcim/models/device_components.py | 2 +- netbox/dcim/models/devices.py | 2 +- netbox/dcim/models/sites.py | 2 +- netbox/dcim/tests/test_api.py | 63 ------------------- netbox/dcim/views.py | 5 -- netbox/extras/admin.py | 41 +----------- netbox/extras/api/nested_serializers.py | 9 --- netbox/extras/api/serializers.py | 39 +----------- netbox/extras/api/urls.py | 3 - netbox/extras/api/views.py | 13 +--- netbox/extras/choices.py | 15 ----- netbox/extras/constants.py | 1 - netbox/extras/filters.py | 10 +-- netbox/extras/migrations/0049_remove_graph.py | 16 +++++ netbox/extras/models/__init__.py | 3 +- netbox/extras/models/models.py | 63 ------------------- netbox/extras/tests/test_api.py | 35 +---------- netbox/extras/tests/test_filters.py | 39 +----------- netbox/extras/tests/test_models.py | 45 +------------ netbox/project-static/js/graphs.js | 26 -------- netbox/templates/circuits/provider.html | 10 --- netbox/templates/dcim/device.html | 8 --- netbox/templates/dcim/inc/interface.html | 7 --- netbox/templates/dcim/site.html | 11 ---- .../virtualization/inc/vminterface.html | 5 -- netbox/virtualization/api/views.py | 15 ----- netbox/virtualization/models.py | 2 +- netbox/virtualization/tests/test_api.py | 25 -------- 36 files changed, 33 insertions(+), 595 deletions(-) delete mode 100644 docs/additional-features/graphs.md create mode 100644 netbox/extras/migrations/0049_remove_graph.py delete mode 100644 netbox/project-static/js/graphs.js diff --git a/docs/additional-features/graphs.md b/docs/additional-features/graphs.md deleted file mode 100644 index e3551b91f..000000000 --- a/docs/additional-features/graphs.md +++ /dev/null @@ -1,30 +0,0 @@ -# Graphs - -!!! warning - Native support for embedded graphs is due to be removed in NetBox v2.10. It will likely be superseded by a plugin providing similar functionality. - -NetBox does not have the ability to generate graphs natively, but this feature allows you to embed contextual graphs from an external resources (such as a monitoring system) inside the site, provider, and interface views. Each embedded graph must be defined with the following parameters: - -* **Type:** Site, device, provider, or interface. This determines in which view the graph will be displayed. -* **Weight:** Determines the order in which graphs are displayed (lower weights are displayed first). Graphs with equal weights will be ordered alphabetically by name. -* **Name:** The title to display above the graph. -* **Source URL:** The source of the image to be embedded. The associated object will be available as a template variable named `obj`. -* **Link URL (optional):** A URL to which the graph will be linked. The associated object will be available as a template variable named `obj`. - -Graph names and links can be rendered using Jinja2 or [Django's template language](https://docs.djangoproject.com/en/stable/ref/templates/language/). - -## Examples - -You only need to define one graph object for each graph you want to include when viewing an object. For example, if you want to include a graph of traffic through an interface over the past five minutes, your graph source might looks like this: - -``` -https://my.nms.local/graphs/?node={{ obj.device.name }}&interface={{ obj.name }}&duration=5m -``` - -You can define several graphs to provide multiple contexts when viewing an object. For example: - -``` -https://my.nms.local/graphs/?type=throughput&node={{ obj.device.name }}&interface={{ obj.name }}&duration=60m -https://my.nms.local/graphs/?type=throughput&node={{ obj.device.name }}&interface={{ obj.name }}&duration=24h -https://my.nms.local/graphs/?type=errors&node={{ obj.device.name }}&interface={{ obj.name }}&duration=60m -``` diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index d289c7a05..bc7c1a1a1 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -2,8 +2,11 @@ ## v2.10-beta1 (FUTURE) +**NOTE:** This release completely removes support for embedded graphs. + ### Other Changes +* [#4349](https://github.com/netbox-community/netbox/issues/4349) - Dropped support for embedded graphs * [#4360](https://github.com/netbox-community/netbox/issues/4360) - Remove support for the Django template language from export templates * [#4941](https://github.com/netbox-community/netbox/issues/4941) - `commit` argument is now required argument in a custom script's `run()` method diff --git a/mkdocs.yml b/mkdocs.yml index a94aa3cc4..bd8fc780d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -49,7 +49,6 @@ nav: - Custom Links: 'additional-features/custom-links.md' - Custom Scripts: 'additional-features/custom-scripts.md' - Export Templates: 'additional-features/export-templates.md' - - Graphs: 'additional-features/graphs.md' - NAPALM: 'additional-features/napalm.md' - Prometheus Metrics: 'additional-features/prometheus-metrics.md' - Reports: 'additional-features/reports.md' diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index 746ee02f6..cd73a614d 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -1,14 +1,9 @@ from django.db.models import Count, Prefetch -from django.shortcuts import get_object_or_404 -from rest_framework.decorators import action -from rest_framework.response import Response from rest_framework.routers import APIRootView from circuits import filters from circuits.models import Provider, CircuitTermination, CircuitType, Circuit -from extras.api.serializers import RenderedGraphSerializer from extras.api.views import CustomFieldModelViewSet -from extras.models import Graph from utilities.api import ModelViewSet from . import serializers @@ -32,16 +27,6 @@ class ProviderViewSet(CustomFieldModelViewSet): serializer_class = serializers.ProviderSerializer filterset_class = filters.ProviderFilterSet - @action(detail=True) - def graphs(self, request, pk): - """ - A convenience method for rendering graphs for a particular provider. - """ - provider = get_object_or_404(self.queryset, pk=pk) - queryset = Graph.objects.restrict(request.user).filter(type__model='provider') - serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': provider}) - return Response(serializer.data) - # # Circuit Types diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index cdec41d1f..14f555cc6 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -22,7 +22,7 @@ __all__ = ( ) -@extras_features('custom_fields', 'custom_links', 'graphs', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') class Provider(ChangeLoggedModel, CustomFieldModel): """ Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model diff --git a/netbox/circuits/tests/test_api.py b/netbox/circuits/tests/test_api.py index fa264aaa2..8a6289401 100644 --- a/netbox/circuits/tests/test_api.py +++ b/netbox/circuits/tests/test_api.py @@ -1,11 +1,8 @@ -from django.contrib.contenttypes.models import ContentType -from django.test import override_settings from django.urls import reverse from circuits.choices import * from circuits.models import Circuit, CircuitTermination, CircuitType, Provider from dcim.models import Site -from extras.models import Graph from utilities.testing import APITestCase, APIViewTestCases @@ -46,27 +43,6 @@ class ProviderTest(APIViewTestCases.APIViewTestCase): ) Provider.objects.bulk_create(providers) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) - def test_get_provider_graphs(self): - """ - Test retrieval of Graphs assigned to Providers. - """ - provider = self.model.objects.first() - ct = ContentType.objects.get(app_label='circuits', model='provider') - graphs = ( - Graph(type=ct, name='Graph 1', source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=1'), - Graph(type=ct, name='Graph 2', source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=2'), - Graph(type=ct, name='Graph 3', source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=3'), - ) - Graph.objects.bulk_create(graphs) - - self.add_permissions('circuits.view_provider') - url = reverse('circuits-api:provider-graphs', kwargs={'pk': provider.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=provider-1&foo=1') - class CircuitTypeTest(APIViewTestCases.APIViewTestCase): model = CircuitType diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index 4d02ef011..e5da5100f 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -1,11 +1,10 @@ from django.conf import settings from django.contrib import messages from django.db import transaction -from django.db.models import Count, Prefetch +from django.db.models import Count from django.shortcuts import get_object_or_404, redirect, render from django_tables2 import RequestConfig -from extras.models import Graph from utilities.forms import ConfirmationForm from utilities.paginator import EnhancedPaginator from utilities.views import ( @@ -38,7 +37,6 @@ class ProviderView(ObjectView): ).prefetch_related( 'type', 'tenant', 'terminations__site' ).annotate_sites() - show_graphs = Graph.objects.filter(type__model='provider').exists() circuits_table = tables.CircuitTable(circuits) circuits_table.columns.hide('provider') @@ -52,7 +50,6 @@ class ProviderView(ObjectView): return render(request, 'circuits/provider.html', { 'provider': provider, 'circuits_table': circuits_table, - 'show_graphs': show_graphs, }) diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index f5b37021d..0583d4e56 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -23,9 +23,7 @@ from dcim.models import ( PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis, ) -from extras.api.serializers import RenderedGraphSerializer from extras.api.views import CustomFieldModelViewSet -from extras.models import Graph from ipam.models import Prefix, VLAN from utilities.api import ( get_serializer_for_model, IsAuthenticatedOrLoginNotRequired, ModelViewSet, ServiceUnavailable, @@ -113,16 +111,6 @@ class SiteViewSet(CustomFieldModelViewSet): serializer_class = serializers.SiteSerializer filterset_class = filters.SiteFilterSet - @action(detail=True) - def graphs(self, request, pk): - """ - A convenience method for rendering graphs for a particular site. - """ - site = get_object_or_404(self.queryset, pk=pk) - queryset = Graph.objects.restrict(request.user).filter(type__model='site') - serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': site}) - return Response(serializer.data) - # # Rack groups @@ -363,17 +351,6 @@ class DeviceViewSet(CustomFieldModelViewSet): return serializers.DeviceWithConfigContextSerializer - @action(detail=True) - def graphs(self, request, pk): - """ - A convenience method for rendering graphs for a particular Device. - """ - device = get_object_or_404(self.queryset, pk=pk) - queryset = Graph.objects.restrict(request.user).filter(type__model='device') - serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': device}) - - return Response(serializer.data) - @swagger_auto_schema( manual_parameters=[ Parameter( @@ -527,16 +504,6 @@ class InterfaceViewSet(CableTraceMixin, ModelViewSet): serializer_class = serializers.InterfaceSerializer filterset_class = filters.InterfaceFilterSet - @action(detail=True) - def graphs(self, request, pk): - """ - A convenience method for rendering graphs for a particular interface. - """ - interface = get_object_or_404(self.queryset, pk=pk) - queryset = Graph.objects.restrict(request.user).filter(type__model='interface') - serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': interface}) - return Response(serializer.data) - class FrontPortViewSet(CableTraceMixin, ModelViewSet): queryset = FrontPort.objects.prefetch_related('device__device_type__manufacturer', 'rear_port', 'cable', 'tags') diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 92b0605e9..9bd7cdc8b 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -582,7 +582,7 @@ class BaseInterface(models.Model): abstract = True -@extras_features('graphs', 'export_templates', 'webhooks') +@extras_features('export_templates', 'webhooks') class Interface(CableTermination, ComponentModel, BaseInterface): """ A network interface within a Device. A physical Interface can connect to exactly one other Interface. diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 4189e0446..8bb56101e 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -450,7 +450,7 @@ class Platform(ChangeLoggedModel): ) -@extras_features('custom_fields', 'custom_links', 'graphs', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): """ A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType, diff --git a/netbox/dcim/models/sites.py b/netbox/dcim/models/sites.py index daf7055db..1ea083367 100644 --- a/netbox/dcim/models/sites.py +++ b/netbox/dcim/models/sites.py @@ -91,7 +91,7 @@ class Region(MPTTModel, ChangeLoggedModel): # Sites # -@extras_features('custom_fields', 'custom_links', 'graphs', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') class Site(ChangeLoggedModel, CustomFieldModel): """ A Site represents a geographic location within a network; typically a building or campus. The optional facility diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index c3ffecdff..22085fdbd 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -1,6 +1,4 @@ from django.contrib.auth.models import User -from django.contrib.contenttypes.models import ContentType -from django.test import override_settings from django.urls import reverse from rest_framework import status @@ -14,7 +12,6 @@ from dcim.models import ( Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis, ) from ipam.models import VLAN -from extras.models import Graph from utilities.testing import APITestCase, APIViewTestCases from virtualization.models import Cluster, ClusterType @@ -132,26 +129,6 @@ class SiteTest(APIViewTestCases.APIViewTestCase): }, ] - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) - def test_get_site_graphs(self): - """ - Test retrieval of Graphs assigned to Sites. - """ - ct = ContentType.objects.get_for_model(Site) - graphs = ( - Graph(type=ct, name='Graph 1', source='http://example.com/graphs.py?site={{ obj.slug }}&foo=1'), - Graph(type=ct, name='Graph 2', source='http://example.com/graphs.py?site={{ obj.slug }}&foo=2'), - Graph(type=ct, name='Graph 3', source='http://example.com/graphs.py?site={{ obj.slug }}&foo=3'), - ) - Graph.objects.bulk_create(graphs) - - self.add_permissions('dcim.view_site') - url = reverse('dcim-api:site-graphs', kwargs={'pk': Site.objects.first().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=site-1&foo=1') - class RackGroupTest(APIViewTestCases.APIViewTestCase): model = RackGroup @@ -902,26 +879,6 @@ class DeviceTest(APIViewTestCases.APIViewTestCase): }, ] - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) - def test_get_device_graphs(self): - """ - Test retrieval of Graphs assigned to Devices. - """ - ct = ContentType.objects.get_for_model(Device) - graphs = ( - Graph(type=ct, name='Graph 1', source='http://example.com/graphs.py?device={{ obj.name }}&foo=1'), - Graph(type=ct, name='Graph 2', source='http://example.com/graphs.py?device={{ obj.name }}&foo=2'), - Graph(type=ct, name='Graph 3', source='http://example.com/graphs.py?device={{ obj.name }}&foo=3'), - ) - Graph.objects.bulk_create(graphs) - - self.add_permissions('dcim.view_device') - url = reverse('dcim-api:device-graphs', kwargs={'pk': Device.objects.first().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?device=Device 1&foo=1') - def test_config_context_included_by_default_in_list_view(self): """ Check that config context data is included by default in the devices list. @@ -1159,26 +1116,6 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase }, ] - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) - def test_get_interface_graphs(self): - """ - Test retrieval of Graphs assigned to Devices. - """ - ct = ContentType.objects.get_for_model(Interface) - graphs = ( - Graph(type=ct, name='Graph 1', source='http://example.com/graphs.py?interface={{ obj.name }}&foo=1'), - Graph(type=ct, name='Graph 2', source='http://example.com/graphs.py?interface={{ obj.name }}&foo=2'), - Graph(type=ct, name='Graph 3', source='http://example.com/graphs.py?interface={{ obj.name }}&foo=3'), - ) - Graph.objects.bulk_create(graphs) - - self.add_permissions('dcim.view_interface') - url = reverse('dcim-api:interface-graphs', kwargs={'pk': Interface.objects.first().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=Interface 1&foo=1') - class FrontPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase): model = FrontPort diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index c016f6e54..c3cfa105f 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -14,7 +14,6 @@ from django.utils.safestring import mark_safe from django.views.generic import View from circuits.models import Circuit -from extras.models import Graph from extras.views import ObjectConfigContextView from ipam.models import IPAddress, Prefix, Service, VLAN from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable @@ -172,13 +171,11 @@ class SiteView(ObjectView): rack_groups = RackGroup.objects.restrict(request.user, 'view').filter(site=site).annotate( rack_count=Count('racks') ) - show_graphs = Graph.objects.filter(type__model='site').exists() return render(request, 'dcim/site.html', { 'site': site, 'stats': stats, 'rack_groups': rack_groups, - 'show_graphs': show_graphs, }) @@ -1082,8 +1079,6 @@ class DeviceView(ObjectView): 'secrets': secrets, 'vc_members': vc_members, 'related_devices': related_devices, - 'show_graphs': Graph.objects.filter(type__model='device').exists(), - 'show_interface_graphs': Graph.objects.filter(type__model='interface').exists(), }) diff --git a/netbox/extras/admin.py b/netbox/extras/admin.py index de6a9b470..5c95025cd 100644 --- a/netbox/extras/admin.py +++ b/netbox/extras/admin.py @@ -2,7 +2,7 @@ from django import forms from django.contrib import admin from utilities.forms import LaxURLField -from .models import CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, JobResult, Webhook +from .models import CustomField, CustomFieldChoice, CustomLink, ExportTemplate, JobResult, Webhook def order_content_types(field): @@ -150,45 +150,6 @@ class CustomLinkAdmin(admin.ModelAdmin): form = CustomLinkForm -# -# Graphs -# - -class GraphForm(forms.ModelForm): - - class Meta: - model = Graph - exclude = () - help_texts = { - 'template_language': "Jinja2 is strongly recommended for " - "new graphs." - } - widgets = { - 'source': forms.Textarea, - 'link': forms.Textarea, - } - - -@admin.register(Graph) -class GraphAdmin(admin.ModelAdmin): - fieldsets = ( - ('Graph', { - 'fields': ('type', 'name', 'weight') - }), - ('Templates', { - 'fields': ('template_language', 'source', 'link'), - 'classes': ('monospace',) - }) - ) - form = GraphForm - list_display = [ - 'name', 'type', 'weight', 'template_language', 'source', - ] - list_filter = [ - 'type', 'template_language', - ] - - # # Export templates # diff --git a/netbox/extras/api/nested_serializers.py b/netbox/extras/api/nested_serializers.py index 198a5d2f8..95c980768 100644 --- a/netbox/extras/api/nested_serializers.py +++ b/netbox/extras/api/nested_serializers.py @@ -7,7 +7,6 @@ from utilities.api import ChoiceField, WritableNestedSerializer __all__ = [ 'NestedConfigContextSerializer', 'NestedExportTemplateSerializer', - 'NestedGraphSerializer', 'NestedImageAttachmentSerializer', 'NestedJobResultSerializer', 'NestedTagSerializer', @@ -30,14 +29,6 @@ class NestedExportTemplateSerializer(WritableNestedSerializer): fields = ['id', 'url', 'name'] -class NestedGraphSerializer(WritableNestedSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:graph-detail') - - class Meta: - model = models.Graph - fields = ['id', 'url', 'name'] - - class NestedImageAttachmentSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail') diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index a186f772f..1942f6f25 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -10,7 +10,7 @@ from dcim.api.nested_serializers import ( from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site from extras.choices import * from extras.models import ( - ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, JobResult, Tag, + ConfigContext, ExportTemplate, ImageAttachment, ObjectChange, JobResult, Tag, ) from extras.utils import FeatureQuery from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer @@ -25,43 +25,6 @@ from virtualization.models import Cluster, ClusterGroup from .nested_serializers import * -# -# Graphs -# - -class GraphSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:graph-detail') - type = ContentTypeField( - queryset=ContentType.objects.filter(FeatureQuery('graphs').get_query()), - ) - - class Meta: - model = Graph - fields = ['id', 'url', 'type', 'weight', 'name', 'template_language', 'source', 'link'] - - -class RenderedGraphSerializer(serializers.ModelSerializer): - embed_url = serializers.SerializerMethodField( - read_only=True - ) - embed_link = serializers.SerializerMethodField( - read_only=True - ) - type = ContentTypeField( - read_only=True - ) - - 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']) - - def get_embed_link(self, obj): - return obj.embed_link(self.context['graphed_object']) - - # # Export templates # diff --git a/netbox/extras/api/urls.py b/netbox/extras/api/urls.py index 9c50c9a45..70e5bc9da 100644 --- a/netbox/extras/api/urls.py +++ b/netbox/extras/api/urls.py @@ -8,9 +8,6 @@ router.APIRootView = views.ExtrasRootView # Custom field choices router.register('_custom_field_choices', views.CustomFieldChoicesViewSet, basename='custom-field-choice') -# Graphs -router.register('graphs', views.GraphViewSet) - # Export templates router.register('export-templates', views.ExportTemplateViewSet) diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index 289a51c83..5fa26a0d7 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -15,7 +15,7 @@ from rq import Worker from extras import filters from extras.choices import JobResultStatusChoices from extras.models import ( - ConfigContext, CustomFieldChoice, ExportTemplate, Graph, ImageAttachment, ObjectChange, JobResult, Tag, + ConfigContext, CustomFieldChoice, ExportTemplate, ImageAttachment, ObjectChange, JobResult, Tag, ) from extras.reports import get_report, get_reports, run_report from extras.scripts import get_script, get_scripts, run_script @@ -98,17 +98,6 @@ class CustomFieldModelViewSet(ModelViewSet): return super().get_queryset().prefetch_related('custom_field_values__field') -# -# Graphs -# - -class GraphViewSet(ModelViewSet): - metadata_class = ContentTypeMetadata - queryset = Graph.objects.all() - serializer_class = serializers.GraphSerializer - filterset_class = filters.GraphFilterSet - - # # Export templates # diff --git a/netbox/extras/choices.py b/netbox/extras/choices.py index b14748135..7b4b1665a 100644 --- a/netbox/extras/choices.py +++ b/netbox/extras/choices.py @@ -79,21 +79,6 @@ class ObjectChangeActionChoices(ChoiceSet): ) -# -# ExportTemplates -# - -class TemplateLanguageChoices(ChoiceSet): - - LANGUAGE_JINJA2 = 'jinja2' - LANGUAGE_DJANGO = 'django' - - CHOICES = ( - (LANGUAGE_JINJA2, 'Jinja2'), - (LANGUAGE_DJANGO, 'Django (Legacy)'), - ) - - # # Log Levels for Reports and Scripts # diff --git a/netbox/extras/constants.py b/netbox/extras/constants.py index a506d5867..190e68c36 100644 --- a/netbox/extras/constants.py +++ b/netbox/extras/constants.py @@ -6,7 +6,6 @@ EXTRAS_FEATURES = [ 'custom_fields', 'custom_links', 'export_templates', - 'graphs', 'job_results', 'webhooks' ] diff --git a/netbox/extras/filters.py b/netbox/extras/filters.py index 288be10e2..73811c063 100644 --- a/netbox/extras/filters.py +++ b/netbox/extras/filters.py @@ -7,7 +7,7 @@ from tenancy.models import Tenant, TenantGroup from utilities.filters import BaseFilterSet from virtualization.models import Cluster, ClusterGroup from .choices import * -from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, JobResult, Tag +from .models import ConfigContext, CustomField, ExportTemplate, ObjectChange, JobResult, Tag __all__ = ( @@ -16,7 +16,6 @@ __all__ = ( 'CustomFieldFilter', 'CustomFieldFilterSet', 'ExportTemplateFilterSet', - 'GraphFilterSet', 'LocalConfigContextFilterSet', 'ObjectChangeFilterSet', 'TagFilterSet', @@ -90,13 +89,6 @@ class CustomFieldFilterSet(django_filters.FilterSet): self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(field_name=cf.name, custom_field=cf) -class GraphFilterSet(BaseFilterSet): - - class Meta: - model = Graph - fields = ['id', 'type', 'name', 'template_language'] - - class ExportTemplateFilterSet(BaseFilterSet): class Meta: diff --git a/netbox/extras/migrations/0049_remove_graph.py b/netbox/extras/migrations/0049_remove_graph.py new file mode 100644 index 000000000..c884c8f82 --- /dev/null +++ b/netbox/extras/migrations/0049_remove_graph.py @@ -0,0 +1,16 @@ +# Generated by Django 3.1 on 2020-08-21 15:47 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0048_exporttemplate_remove_template_language'), + ] + + operations = [ + migrations.DeleteModel( + name='Graph', + ), + ] diff --git a/netbox/extras/models/__init__.py b/netbox/extras/models/__init__.py index e52058157..9437fe01f 100644 --- a/netbox/extras/models/__init__.py +++ b/netbox/extras/models/__init__.py @@ -1,7 +1,7 @@ from .change_logging import ChangeLoggedModel, ObjectChange from .customfields import CustomField, CustomFieldChoice, CustomFieldModel, CustomFieldValue from .models import ( - ConfigContext, ConfigContextModel, CustomLink, ExportTemplate, Graph, ImageAttachment, JobResult, Report, Script, + ConfigContext, ConfigContextModel, CustomLink, ExportTemplate, ImageAttachment, JobResult, Report, Script, Webhook, ) from .tags import Tag, TaggedItem @@ -16,7 +16,6 @@ __all__ = ( 'CustomFieldValue', 'CustomLink', 'ExportTemplate', - 'Graph', 'ImageAttachment', 'JobResult', 'ObjectChange', diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 40ba64a67..8c88bb1da 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -203,69 +203,6 @@ class CustomLink(models.Model): return self.name -# -# Graphs -# - -class Graph(models.Model): - type = models.ForeignKey( - to=ContentType, - on_delete=models.CASCADE, - limit_choices_to=FeatureQuery('graphs') - ) - weight = models.PositiveSmallIntegerField( - default=1000 - ) - name = models.CharField( - max_length=100, - verbose_name='Name' - ) - template_language = models.CharField( - max_length=50, - choices=TemplateLanguageChoices, - default=TemplateLanguageChoices.LANGUAGE_JINJA2 - ) - source = models.CharField( - max_length=500, - verbose_name='Source URL' - ) - link = models.URLField( - blank=True, - verbose_name='Link URL' - ) - - objects = RestrictedQuerySet.as_manager() - - class Meta: - ordering = ('type', 'weight', 'name', 'pk') # (type, weight, name) may be non-unique - - def __str__(self): - return self.name - - def embed_url(self, obj): - context = {'obj': obj} - - if self.template_language == TemplateLanguageChoices.LANGUAGE_DJANGO: - template = Template(self.source) - return template.render(Context(context)) - - elif self.template_language == TemplateLanguageChoices.LANGUAGE_JINJA2: - return render_jinja2(self.source, context) - - def embed_link(self, obj): - if self.link is None: - return '' - - context = {'obj': obj} - - if self.template_language == TemplateLanguageChoices.LANGUAGE_DJANGO: - template = Template(self.link) - return template.render(Context(context)) - - elif self.template_language == TemplateLanguageChoices.LANGUAGE_JINJA2: - return render_jinja2(self.link, context) - - # # Export templates # diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index c768534a2..f66fea2ac 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -10,7 +10,7 @@ from rq import Worker from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, RackGroup, RackRole, Site from extras.api.views import ReportViewSet, ScriptViewSet -from extras.models import ConfigContext, ExportTemplate, Graph, ImageAttachment, Tag +from extras.models import ConfigContext, ExportTemplate, ImageAttachment, Tag from extras.reports import Report from extras.scripts import BooleanVar, IntegerVar, Script, StringVar from utilities.testing import APITestCase, APIViewTestCases @@ -29,39 +29,6 @@ class AppTest(APITestCase): self.assertEqual(response.status_code, 200) -class GraphTest(APIViewTestCases.APIViewTestCase): - model = Graph - brief_fields = ['id', 'name', 'url'] - create_data = [ - { - 'type': 'dcim.site', - 'name': 'Graph 4', - 'source': 'http://example.com/graphs.py?site={{ obj.name }}&foo=4', - }, - { - 'type': 'dcim.site', - 'name': 'Graph 5', - 'source': 'http://example.com/graphs.py?site={{ obj.name }}&foo=5', - }, - { - 'type': 'dcim.site', - 'name': 'Graph 6', - 'source': 'http://example.com/graphs.py?site={{ obj.name }}&foo=6', - }, - ] - - @classmethod - def setUpTestData(cls): - ct = ContentType.objects.get_for_model(Site) - - graphs = ( - Graph(type=ct, name='Graph 1', source='http://example.com/graphs.py?site={{ obj.name }}&foo=1'), - Graph(type=ct, name='Graph 2', source='http://example.com/graphs.py?site={{ obj.name }}&foo=2'), - Graph(type=ct, name='Graph 3', source='http://example.com/graphs.py?site={{ obj.name }}&foo=3'), - ) - Graph.objects.bulk_create(graphs) - - class ExportTemplateTest(APIViewTestCases.APIViewTestCase): model = ExportTemplate brief_fields = ['id', 'name', 'url'] diff --git a/netbox/extras/tests/test_filters.py b/netbox/extras/tests/test_filters.py index deb41d628..cc702f07b 100644 --- a/netbox/extras/tests/test_filters.py +++ b/netbox/extras/tests/test_filters.py @@ -2,49 +2,12 @@ from django.contrib.contenttypes.models import ContentType from django.test import TestCase from dcim.models import DeviceRole, Platform, Region, Site -from extras.choices import * from extras.filters import * -from extras.utils import FeatureQuery -from extras.models import ConfigContext, ExportTemplate, Graph, Tag +from extras.models import ConfigContext, ExportTemplate, Tag from tenancy.models import Tenant, TenantGroup from virtualization.models import Cluster, ClusterGroup, ClusterType -class GraphTestCase(TestCase): - queryset = Graph.objects.all() - filterset = GraphFilterSet - - @classmethod - def setUpTestData(cls): - - # Get the first three available types - content_types = ContentType.objects.filter(FeatureQuery('graphs').get_query())[:3] - - graphs = ( - Graph(name='Graph 1', type=content_types[0], template_language=TemplateLanguageChoices.LANGUAGE_DJANGO, source='http://example.com/1'), - Graph(name='Graph 2', type=content_types[1], template_language=TemplateLanguageChoices.LANGUAGE_JINJA2, source='http://example.com/2'), - Graph(name='Graph 3', type=content_types[2], template_language=TemplateLanguageChoices.LANGUAGE_JINJA2, source='http://example.com/3'), - ) - Graph.objects.bulk_create(graphs) - - def test_id(self): - params = {'id': self.queryset.values_list('pk', flat=True)[:2]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - - def test_name(self): - params = {'name': ['Graph 1', 'Graph 2']} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - - def test_type(self): - content_type = ContentType.objects.filter(FeatureQuery('graphs').get_query()).first() - params = {'type': content_type.pk} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - - def test_template_language(self): - params = {'template_language': TemplateLanguageChoices.LANGUAGE_JINJA2} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - - class ExportTemplateTestCase(TestCase): queryset = ExportTemplate.objects.all() filterset = ExportTemplateFilterSet diff --git a/netbox/extras/tests/test_models.py b/netbox/extras/tests/test_models.py index 22e6e1f8f..6a02872f8 100644 --- a/netbox/extras/tests/test_models.py +++ b/netbox/extras/tests/test_models.py @@ -1,49 +1,6 @@ -from django.contrib.contenttypes.models import ContentType from django.test import TestCase -from dcim.models import Site -from extras.choices import TemplateLanguageChoices -from extras.models import Graph, Tag - - -class GraphTest(TestCase): - - def setUp(self): - - self.site = Site(name='Site 1', slug='site-1') - - def test_graph_render_django(self): - - # Using the pluralize filter as a sanity check (it's only available in Django) - TEMPLATE_TEXT = "{{ obj.name|lower }} thing{{ 2|pluralize }}" - RENDERED_TEXT = "site 1 things" - - graph = Graph( - type=ContentType.objects.get(app_label='dcim', model='site'), - name='Graph 1', - template_language=TemplateLanguageChoices.LANGUAGE_DJANGO, - source=TEMPLATE_TEXT, - link=TEMPLATE_TEXT - ) - - self.assertEqual(graph.embed_url(self.site), RENDERED_TEXT) - self.assertEqual(graph.embed_link(self.site), RENDERED_TEXT) - - def test_graph_render_jinja2(self): - - TEMPLATE_TEXT = "{{ [obj.name, obj.slug]|join(',') }}" - RENDERED_TEXT = "Site 1,site-1" - - graph = Graph( - type=ContentType.objects.get(app_label='dcim', model='site'), - name='Graph 1', - template_language=TemplateLanguageChoices.LANGUAGE_JINJA2, - source=TEMPLATE_TEXT, - link=TEMPLATE_TEXT - ) - - self.assertEqual(graph.embed_url(self.site), RENDERED_TEXT) - self.assertEqual(graph.embed_link(self.site), RENDERED_TEXT) +from extras.models import Tag class TagTest(TestCase): diff --git a/netbox/project-static/js/graphs.js b/netbox/project-static/js/graphs.js deleted file mode 100644 index 4405c2903..000000000 --- a/netbox/project-static/js/graphs.js +++ /dev/null @@ -1,26 +0,0 @@ -$('#graphs_modal').on('show.bs.modal', function (event) { - var button = $(event.relatedTarget); - var obj = button.data('obj'); - var url = button.data('url'); - var modal_title = $(this).find('.modal-title'); - var modal_body = $(this).find('.modal-body'); - modal_title.text(obj); - modal_body.empty(); - $.ajax({ - url: url, - dataType: 'json', - success: function(json) { - $.each(json, function(i, graph) { - // Build in a 500ms delay per graph to avoid hammering the server - setTimeout(function() { - modal_body.append('

' + graph.name + '

'); - if (graph.embed_link) { - modal_body.append(''); - } else { - modal_body.append(''); - } - }, i*500); - }) - } - }); -}); diff --git a/netbox/templates/circuits/provider.html b/netbox/templates/circuits/provider.html index 42c322ce2..e0598d872 100644 --- a/netbox/templates/circuits/provider.html +++ b/netbox/templates/circuits/provider.html @@ -30,11 +30,6 @@
{% plugin_buttons provider %} - {% if show_graphs %} - - {% endif %} {% if perms.circuits.add_provider %} {% clone_button provider %} {% endif %} @@ -138,14 +133,9 @@ {% plugin_right_page provider %}
-{% include 'inc/modal.html' with name='graphs' title='Graphs' %}
{% plugin_full_width_page provider %}
{% endblock %} - -{% block javascript %} - -{% endblock %} diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index e97893c30..09f6eab40 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -38,12 +38,6 @@
{% plugin_buttons device %} - {% if show_graphs %} - - {% endif %} {% if perms.dcim.change_device %}
-{% include 'inc/modal.html' with name='graphs' title='Graphs' %} {% include 'secrets/inc/private_key_modal.html' %} {% endblock %} @@ -1012,6 +1005,5 @@ $(".cable-toggle").click(function() { }); - {% endblock %} diff --git a/netbox/templates/dcim/inc/interface.html b/netbox/templates/dcim/inc/interface.html index ce66d9da2..a317dc937 100644 --- a/netbox/templates/dcim/inc/interface.html +++ b/netbox/templates/dcim/inc/interface.html @@ -138,13 +138,6 @@ {# Buttons #} - {% if show_interface_graphs %} - {% if iface.connected_endpoint %} - - {% endif %} - {% endif %} {% if perms.ipam.add_ipaddress %} diff --git a/netbox/templates/dcim/site.html b/netbox/templates/dcim/site.html index d6c21bf92..a0fbd59ec 100644 --- a/netbox/templates/dcim/site.html +++ b/netbox/templates/dcim/site.html @@ -35,12 +35,6 @@
{% plugin_buttons site %} - {% if show_graphs %} - - {% endif %} {% if perms.dcim.add_site %} {% clone_button site %} {% endif %} @@ -292,14 +286,9 @@ {% plugin_right_page site %}
-{% include 'inc/modal.html' with name='graphs' title='Graphs' %}
{% plugin_full_width_page site %}
{% endblock %} - -{% block javascript %} - -{% endblock %} diff --git a/netbox/templates/virtualization/inc/vminterface.html b/netbox/templates/virtualization/inc/vminterface.html index 5410fba7a..0f672b729 100644 --- a/netbox/templates/virtualization/inc/vminterface.html +++ b/netbox/templates/virtualization/inc/vminterface.html @@ -38,11 +38,6 @@ {# Buttons #} - {% if show_interface_graphs %} - - {% endif %} {% if perms.ipam.add_ipaddress %}
diff --git a/netbox/virtualization/api/views.py b/netbox/virtualization/api/views.py index 1bf41c2b7..9d210459e 100644 --- a/netbox/virtualization/api/views.py +++ b/netbox/virtualization/api/views.py @@ -1,13 +1,8 @@ from django.db.models import Count -from django.shortcuts import get_object_or_404 -from rest_framework.decorators import action -from rest_framework.response import Response from rest_framework.routers import APIRootView from dcim.models import Device -from extras.api.serializers import RenderedGraphSerializer from extras.api.views import CustomFieldModelViewSet -from extras.models import Graph from utilities.api import ModelViewSet from utilities.utils import get_subquery from virtualization import filters @@ -91,13 +86,3 @@ class VMInterfaceViewSet(ModelViewSet): ) serializer_class = serializers.VMInterfaceSerializer filterset_class = filters.VMInterfaceFilterSet - - @action(detail=True) - def graphs(self, request, pk): - """ - A convenience method for rendering graphs for a particular VM interface. - """ - vminterface = get_object_or_404(self.queryset, pk=pk) - queryset = Graph.objects.restrict(request.user).filter(type__model='vminterface') - serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': vminterface}) - return Response(serializer.data) diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index f787aef0e..fb61c5b9e 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -381,7 +381,7 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): # Interfaces # -@extras_features('graphs', 'export_templates', 'webhooks') +@extras_features('export_templates', 'webhooks') class VMInterface(BaseInterface): virtual_machine = models.ForeignKey( to='virtualization.VirtualMachine', diff --git a/netbox/virtualization/tests/test_api.py b/netbox/virtualization/tests/test_api.py index 6ddcdf2ef..28d4bbb99 100644 --- a/netbox/virtualization/tests/test_api.py +++ b/netbox/virtualization/tests/test_api.py @@ -1,10 +1,7 @@ -from django.contrib.contenttypes.models import ContentType -from django.test import override_settings from django.urls import reverse from rest_framework import status from dcim.choices import InterfaceModeChoices -from extras.models import Graph from ipam.models import VLAN from utilities.testing import APITestCase, APIViewTestCases from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface @@ -244,25 +241,3 @@ class VMInterfaceTest(APIViewTestCases.APIViewTestCase): 'untagged_vlan': vlans[2].pk, }, ] - - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) - def test_get_vminterface_graphs(self): - """ - Test retrieval of Graphs assigned to VM interfaces. - """ - ct = ContentType.objects.get_for_model(VMInterface) - graphs = ( - Graph(type=ct, name='Graph 1', source='http://example.com/graphs.py?interface={{ obj.name }}&foo=1'), - Graph(type=ct, name='Graph 2', source='http://example.com/graphs.py?interface={{ obj.name }}&foo=2'), - Graph(type=ct, name='Graph 3', source='http://example.com/graphs.py?interface={{ obj.name }}&foo=3'), - ) - Graph.objects.bulk_create(graphs) - - self.add_permissions('virtualization.view_vminterface') - url = reverse('virtualization-api:vminterface-graphs', kwargs={ - 'pk': VMInterface.objects.first().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=Interface 1&foo=1')