Просмотр исходного кода

Closes #4349: Drop support for embedded graphs

Jeremy Stretch 5 лет назад
Родитель
Сommit
ec66e1a5c0
36 измененных файлов с 33 добавлено и 595 удалено
  1. 0 30
      docs/additional-features/graphs.md
  2. 3 0
      docs/release-notes/version-2.10.md
  3. 0 1
      mkdocs.yml
  4. 0 15
      netbox/circuits/api/views.py
  5. 1 1
      netbox/circuits/models.py
  6. 0 24
      netbox/circuits/tests/test_api.py
  7. 1 4
      netbox/circuits/views.py
  8. 0 33
      netbox/dcim/api/views.py
  9. 1 1
      netbox/dcim/models/device_components.py
  10. 1 1
      netbox/dcim/models/devices.py
  11. 1 1
      netbox/dcim/models/sites.py
  12. 0 63
      netbox/dcim/tests/test_api.py
  13. 0 5
      netbox/dcim/views.py
  14. 1 40
      netbox/extras/admin.py
  15. 0 9
      netbox/extras/api/nested_serializers.py
  16. 1 38
      netbox/extras/api/serializers.py
  17. 0 3
      netbox/extras/api/urls.py
  18. 1 12
      netbox/extras/api/views.py
  19. 0 15
      netbox/extras/choices.py
  20. 0 1
      netbox/extras/constants.py
  21. 1 9
      netbox/extras/filters.py
  22. 16 0
      netbox/extras/migrations/0049_remove_graph.py
  23. 1 2
      netbox/extras/models/__init__.py
  24. 0 63
      netbox/extras/models/models.py
  25. 1 34
      netbox/extras/tests/test_api.py
  26. 1 38
      netbox/extras/tests/test_filters.py
  27. 1 44
      netbox/extras/tests/test_models.py
  28. 0 26
      netbox/project-static/js/graphs.js
  29. 0 10
      netbox/templates/circuits/provider.html
  30. 0 8
      netbox/templates/dcim/device.html
  31. 0 7
      netbox/templates/dcim/inc/interface.html
  32. 0 11
      netbox/templates/dcim/site.html
  33. 0 5
      netbox/templates/virtualization/inc/vminterface.html
  34. 0 15
      netbox/virtualization/api/views.py
  35. 1 1
      netbox/virtualization/models.py
  36. 0 25
      netbox/virtualization/tests/test_api.py

+ 0 - 30
docs/additional-features/graphs.md

@@ -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
-```

+ 3 - 0
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
 

+ 0 - 1
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'

+ 0 - 15
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

+ 1 - 1
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

+ 0 - 24
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

+ 1 - 4
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,
         })
 
 

+ 0 - 33
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')

+ 1 - 1
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.

+ 1 - 1
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,

+ 1 - 1
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

+ 0 - 63
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

+ 0 - 5
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(),
         })
 
 

+ 1 - 40
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': "<a href=\"https://jinja.palletsprojects.com\">Jinja2</a> 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
 #

+ 0 - 9
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')
 

+ 1 - 38
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
 #

+ 0 - 3
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)
 

+ 1 - 12
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
 #

+ 0 - 15
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
 #

+ 0 - 1
netbox/extras/constants.py

@@ -6,7 +6,6 @@ EXTRAS_FEATURES = [
     'custom_fields',
     'custom_links',
     'export_templates',
-    'graphs',
     'job_results',
     'webhooks'
 ]

+ 1 - 9
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:

+ 16 - 0
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',
+        ),
+    ]

+ 1 - 2
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',

+ 0 - 63
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
 #

+ 1 - 34
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']

+ 1 - 38
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

+ 1 - 44
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):

+ 0 - 26
netbox/project-static/js/graphs.js

@@ -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('<h4 class="text-center">' + graph.name + '</h4>');
-                    if (graph.embed_link) {
-                        modal_body.append('<a href="' + graph.embed_link + '"><img src="' + graph.embed_url + '" /></a>');
-                    } else {
-                        modal_body.append('<img src="' + graph.embed_url + '" />');
-                    }
-                }, i*500);
-            })
-        }
-    });
-});

+ 0 - 10
netbox/templates/circuits/provider.html

@@ -30,11 +30,6 @@
     </div>
     <div class="pull-right noprint">
         {% plugin_buttons provider %}
-        {% if show_graphs %}
-            <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ provider.name }}" data-url="{% url 'circuits-api:provider-graphs' pk=provider.pk %}" title="Show graphs">
-                <i class="fa fa-signal" aria-hidden="true"></i> Graphs
-            </button>
-        {% endif %}
         {% if perms.circuits.add_provider %}
             {% clone_button provider %}
         {% endif %}
@@ -138,14 +133,9 @@
     {% plugin_right_page provider %}
     </div>
 </div>
-{% include 'inc/modal.html' with name='graphs' title='Graphs' %}
 <div class="row">
     <div class="col-md-12">
         {% plugin_full_width_page provider %}
     </div>
 </div>
 {% endblock %}
-
-{% block javascript %}
-<script src="{% static 'js/graphs.js' %}?v{{ settings.VERSION }}"></script>
-{% endblock %}

+ 0 - 8
netbox/templates/dcim/device.html

@@ -38,12 +38,6 @@
     </div>
     <div class="pull-right noprint">
         {% plugin_buttons device %}
-        {% if show_graphs %}
-            <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }}" data-url="{% url 'dcim-api:device-graphs' pk=device.pk %}" title="Show graphs">
-                <i class="fa fa-signal" aria-hidden="true"></i>
-                Graphs
-            </button>
-        {% endif %}
         {% if perms.dcim.change_device %}
             <div class="btn-group">
                 <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@@ -958,7 +952,6 @@
             </div>
         </div>
     </div>
-{% 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() {
 });
 </script>
 <script src="{% static 'js/interface_toggles.js' %}?v{{ settings.VERSION }}"></script>
-<script src="{% static 'js/graphs.js' %}?v{{ settings.VERSION }}"></script>
 <script src="{% static 'js/secrets.js' %}?v{{ settings.VERSION }}"></script>
 {% endblock %}

+ 0 - 7
netbox/templates/dcim/inc/interface.html

@@ -138,13 +138,6 @@
 
     {# Buttons #}
     <td class="text-right text-nowrap noprint">
-        {% if show_interface_graphs %}
-            {% if iface.connected_endpoint %}
-                <button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }} - {{ iface.name }}" data-url="{% url 'dcim-api:interface-graphs' pk=iface.pk %}" title="Show graphs">
-                    <i class="glyphicon glyphicon-signal" aria-hidden="true"></i>
-                </button>
-            {% endif %}
-        {% endif %}
         {% if perms.ipam.add_ipaddress %}
             <a href="{% url 'ipam:ipaddress_add' %}?interface={{ iface.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-success" title="Add IP address">
                 <i class="glyphicon glyphicon-plus" aria-hidden="true"></i>

+ 0 - 11
netbox/templates/dcim/site.html

@@ -35,12 +35,6 @@
     </div>
     <div class="pull-right noprint">
         {% plugin_buttons site %}
-        {% if show_graphs %}
-            <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ site.name }}" data-url="{% url 'dcim-api:site-graphs' pk=site.pk %}" title="Show graphs">
-                <i class="fa fa-signal" aria-hidden="true"></i>
-                Graphs
-            </button>
-        {% endif %}
         {% if perms.dcim.add_site %}
             {% clone_button site %}
         {% endif %}
@@ -292,14 +286,9 @@
         {% plugin_right_page site %}
 	</div>
 </div>
-{% include 'inc/modal.html' with name='graphs' title='Graphs' %}
 <div class="row">
     <div class="col-md-12">
         {% plugin_full_width_page site %}
     </div>
 </div>
 {% endblock %}
-
-{% block javascript %}
-<script src="{% static 'js/graphs.js' %}?v{{ settings.VERSION }}"></script>
-{% endblock %}

+ 0 - 5
netbox/templates/virtualization/inc/vminterface.html

@@ -38,11 +38,6 @@
 
     {# Buttons #}
     <td class="text-right text-nowrap noprint">
-        {% if show_interface_graphs %}
-            <button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ virtualmachine.name }} - {{ iface.name }}" data-url="{% url 'virtualization-api:vminterface-graphs' pk=iface.pk %}" title="Show graphs">
-                <i class="glyphicon glyphicon-signal" aria-hidden="true"></i>
-            </button>
-        {% endif %}
         {% if perms.ipam.add_ipaddress %}
             <a href="{% url 'ipam:ipaddress_add' %}?vminterface={{ iface.pk }}&return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-xs btn-success" title="Add IP address">
                 <i class="glyphicon glyphicon-plus" aria-hidden="true"></i>

+ 0 - 15
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)

+ 1 - 1
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',

+ 0 - 25
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')