Browse Source

Merge remote-tracking branch 'origin/feature' into 23472-plugin-export-formats

# Conflicts:
#	netbox/netbox/views/generic/bulk_views.py
Jeremy Stretch 3 days ago
parent
commit
97d70fbeb4

+ 3 - 0
docs/plugins/development/views.md

@@ -156,6 +156,9 @@ Below are the class definitions for NetBox's multi-object views. These views han
         - get_table
         - get_table
         - export_template
         - export_template
 
 
+!!! warning "Deprecation Warning"
+    The `export_table()` and `export_yaml()` methods of `ObjectListView` have been deprecated as of NetBox v4.8.0, and are scheduled for removal in NetBox v5.0.
+
 ::: netbox.views.generic.BulkImportView
 ::: netbox.views.generic.BulkImportView
     options:
     options:
       members:
       members:

+ 13 - 8
netbox/netbox/tests/test_exporters.py

@@ -292,16 +292,18 @@ class ExportViewTestCase(TestCase):
         """
         """
         Overriding the deprecated export_table() still applies to CSV exports, using the user's delimiter.
         Overriding the deprecated export_table() still applies to CSV exports, using the user's delimiter.
         """
         """
-        class CustomSiteListView(SiteListView):
-            def export_table(self, table, columns=None, filename=None, delimiter=None):
-                return super().export_table(table, columns, filename='custom.csv', delimiter=delimiter)
+        with self.assertWarns(DeprecationWarning):
+            class CustomSiteListView(SiteListView):
+                def export_table(self, table, columns=None, filename=None, delimiter=None):
+                    return super().export_table(table, columns, filename='custom.csv', delimiter=delimiter)
 
 
         self.user.config.set('csv_delimiter', 'semicolon', commit=True)
         self.user.config.set('csv_delimiter', 'semicolon', commit=True)
         for params in ({'export': 'table'}, {'export': ''}, {'export': 'table', 'export_format': 'csv'}):
         for params in ({'export': 'table'}, {'export': ''}, {'export': 'table', 'export_format': 'csv'}):
             with self.subTest(params=params):
             with self.subTest(params=params):
                 request = RequestFactory().get(self.url, params)
                 request = RequestFactory().get(self.url, params)
                 request.user = self.user
                 request.user = self.user
-                response = CustomSiteListView.as_view()(request)
+                with self.assertWarns(DeprecationWarning):
+                    response = CustomSiteListView.as_view()(request)
                 self.assertEqual(response['Content-Disposition'], 'attachment; filename="custom.csv"')
                 self.assertEqual(response['Content-Disposition'], 'attachment; filename="custom.csv"')
                 self.assertTrue(response.getvalue().decode().startswith('Name;Status'))
                 self.assertTrue(response.getvalue().decode().startswith('Name;Status'))
 
 
@@ -309,9 +311,10 @@ class ExportViewTestCase(TestCase):
         """
         """
         Overriding the deprecated export_yaml() still applies to YAML exports.
         Overriding the deprecated export_yaml() still applies to YAML exports.
         """
         """
-        class CustomDeviceTypeListView(DeviceTypeListView):
-            def export_yaml(self):
-                return 'custom'
+        with self.assertWarns(DeprecationWarning):
+            class CustomDeviceTypeListView(DeviceTypeListView):
+                def export_yaml(self):
+                    return 'custom'
 
 
         self.add_permissions('dcim.view_devicetype')
         self.add_permissions('dcim.view_devicetype')
         url = reverse('dcim:devicetype_list')
         url = reverse('dcim:devicetype_list')
@@ -331,4 +334,6 @@ class ExportViewTestCase(TestCase):
         DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
         DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
         view = DeviceTypeListView()
         view = DeviceTypeListView()
         view.queryset = DeviceType.objects.all()
         view.queryset = DeviceType.objects.all()
-        self.assertIn('model: Device Type 1', view.export_yaml())
+        with self.assertWarns(DeprecationWarning):
+            data = view.export_yaml()
+        self.assertIn('model: Device Type 1', data)

+ 34 - 0
netbox/netbox/tests/test_views.py

@@ -12,6 +12,7 @@ from django.utils import timezone
 from core.models import DataFile, DataSource
 from core.models import DataFile, DataSource
 from dcim.choices import DeviceStatusChoices, InterfaceTypeChoices, SiteStatusChoices
 from dcim.choices import DeviceStatusChoices, InterfaceTypeChoices, SiteStatusChoices
 from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer, Site, VirtualChassis
 from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer, Site, VirtualChassis
+from dcim.tables import SiteTable
 from extras.events import enqueue_event
 from extras.events import enqueue_event
 from extras.models import ImageAttachment
 from extras.models import ImageAttachment
 from extras.validators import CustomValidator
 from extras.validators import CustomValidator
@@ -21,6 +22,7 @@ from ipam.views import VLANGroupVLANsView
 from netbox.choices import CSVDelimiterChoices, ImportFormatChoices, ImportMethodChoices
 from netbox.choices import CSVDelimiterChoices, ImportFormatChoices, ImportMethodChoices
 from netbox.constants import EMPTY_TABLE_TEXT
 from netbox.constants import EMPTY_TABLE_TEXT
 from netbox.search.backends import search_backend
 from netbox.search.backends import search_backend
+from netbox.views import generic
 from users.models import User
 from users.models import User
 from utilities.testing import TestCase
 from utilities.testing import TestCase
 from utilities.views import get_action_url
 from utilities.views import get_action_url
@@ -219,6 +221,38 @@ class BulkActionButtonsTestCase(TestCase):
         self.assertIn('name="_edit"', content)
         self.assertIn('name="_edit"', content)
 
 
 
 
+# TODO: Remove in NetBox v5.0
+class ObjectListViewExportDeprecationTestCase(TestCase):
+    """
+    Verify that the deprecated export methods of ObjectListView raise deprecation warnings. (Ref: #23539)
+    """
+    def test_export_table(self):
+        view = generic.ObjectListView()
+        view.queryset = Site.objects.all()
+
+        with self.assertWarnsRegex(DeprecationWarning, r'export_table\(\)'):
+            view.export_table(SiteTable(Site.objects.all()))
+
+    def test_export_yaml(self):
+        view = generic.ObjectListView()
+        view.queryset = DeviceType.objects.all()
+
+        with self.assertWarnsRegex(DeprecationWarning, r'export_yaml\(\)'):
+            view.export_yaml()
+
+    def test_override_export_table(self):
+        with self.assertWarnsRegex(DeprecationWarning, r'export_table\(\)'):
+            class TestListView(generic.ObjectListView):
+                def export_table(self, table, columns=None, filename=None, delimiter=None):
+                    pass
+
+    def test_override_export_yaml(self):
+        with self.assertWarnsRegex(DeprecationWarning, r'export_yaml\(\)'):
+            class TestListView(generic.ObjectListView):
+                def export_yaml(self):
+                    pass
+
+
 class SearchViewTestCase(TestCase):
 class SearchViewTestCase(TestCase):
 
 
     @classmethod
     @classmethod

+ 23 - 0
netbox/netbox/views/generic/bulk_views.py

@@ -1,5 +1,6 @@
 import logging
 import logging
 import re
 import re
+import warnings
 from collections import Counter
 from collections import Counter
 from contextlib import contextmanager
 from contextlib import contextmanager
 from copy import deepcopy
 from copy import deepcopy
@@ -106,6 +107,18 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
     filterset_form = None
     filterset_form = None
     actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkRename, BulkDelete)
     actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkRename, BulkDelete)
 
 
+    def __init_subclass__(cls, **kwargs):
+        super().__init_subclass__(**kwargs)
+        # TODO: Remove in NetBox v5.0
+        for method in ('export_table', 'export_yaml'):
+            if method in cls.__dict__:
+                warnings.warn(
+                    f"{cls.__name__} overrides ObjectListView.{method}(), which is deprecated and will be removed in "
+                    f"NetBox v5.0.",
+                    DeprecationWarning,
+                    stacklevel=2,
+                )
+
     def get_required_permission(self):
     def get_required_permission(self):
         return get_permission_for_model(self.queryset.model, 'view')
         return get_permission_for_model(self.queryset.model, 'view')
 
 
@@ -176,6 +189,11 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
         """
         """
         Deprecated: Export the queryset of objects as concatenated YAML documents.
         Deprecated: Export the queryset of objects as concatenated YAML documents.
         """
         """
+        warnings.warn(
+            "ObjectListView.export_yaml() is deprecated and will be removed in NetBox v5.0.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
         return get_exporter('yaml').export(request=getattr(self, 'request', None), queryset=self.queryset)
         return get_exporter('yaml').export(request=getattr(self, 'request', None), queryset=self.queryset)
 
 
     # TODO: Remove in NetBox v5.0
     # TODO: Remove in NetBox v5.0
@@ -190,6 +208,11 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
                 from the queryset model name.
                 from the queryset model name.
             delimiter: The character used to separate columns (a comma is used by default)
             delimiter: The character used to separate columns (a comma is used by default)
         """
         """
+        warnings.warn(
+            "ObjectListView.export_table() is deprecated and will be removed in NetBox v5.0.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
         exporter = get_exporter('csv')
         exporter = get_exporter('csv')
         headers, rows = self.get_export_rows(table, columns)
         headers, rows = self.get_export_rows(table, columns)
         return self.get_export_response(exporter, exporter.write_rows(headers, rows, delimiter), filename)
         return self.get_export_response(exporter, exporter.write_rows(headers, rows, delimiter), filename)