|
|
@@ -0,0 +1,130 @@
|
|
|
+import inspect
|
|
|
+from importlib import import_module
|
|
|
+
|
|
|
+from django.test import RequestFactory
|
|
|
+
|
|
|
+from netbox.views import generic
|
|
|
+
|
|
|
+from .base import TestCase
|
|
|
+
|
|
|
+__all__ = (
|
|
|
+ "ModelTableTestCase",
|
|
|
+ "TableTestCases",
|
|
|
+)
|
|
|
+
|
|
|
+
|
|
|
+class ModelTableTestCase(TestCase):
|
|
|
+ """
|
|
|
+ Shared helpers for model-backed table tests.
|
|
|
+
|
|
|
+ Concrete subclasses should set `table` and may override `get_queryset()`
|
|
|
+ or `excluded_orderable_columns` as needed.
|
|
|
+ """
|
|
|
+ table = None
|
|
|
+ excluded_orderable_columns = frozenset({"actions"})
|
|
|
+
|
|
|
+ # Optional explicit override for odd cases
|
|
|
+ queryset_sources = None
|
|
|
+
|
|
|
+ # Only these view types are considered sortable queryset sources by default
|
|
|
+ queryset_source_view_classes = (generic.ObjectListView,)
|
|
|
+
|
|
|
+ @classmethod
|
|
|
+ def validate_table_test_case(cls):
|
|
|
+ if cls.table is None:
|
|
|
+ raise AssertionError(f"{cls.__name__} must define `table`")
|
|
|
+ if getattr(cls.table._meta, "model", None) is None:
|
|
|
+ raise AssertionError(f"{cls.__name__}.table must be model-backed")
|
|
|
+
|
|
|
+ def get_request(self):
|
|
|
+ request = RequestFactory().get("/")
|
|
|
+ request.user = self.user
|
|
|
+ return request
|
|
|
+
|
|
|
+ def get_table(self, queryset):
|
|
|
+ return self.table(queryset)
|
|
|
+
|
|
|
+ @classmethod
|
|
|
+ def is_queryset_source_view(cls, view):
|
|
|
+ model = cls.table._meta.model
|
|
|
+ app_label = model._meta.app_label
|
|
|
+
|
|
|
+ return (
|
|
|
+ inspect.isclass(view)
|
|
|
+ and view.__module__.startswith(f"{app_label}.views")
|
|
|
+ and getattr(view, "table", None) is cls.table
|
|
|
+ and getattr(view, "queryset", None) is not None
|
|
|
+ and issubclass(view, cls.queryset_source_view_classes)
|
|
|
+ )
|
|
|
+
|
|
|
+ @classmethod
|
|
|
+ def get_queryset_sources(cls):
|
|
|
+ """
|
|
|
+ Return iterable of (label, queryset) pairs to test.
|
|
|
+
|
|
|
+ By default, only discover list-style views that declare this table.
|
|
|
+ That keeps bulk edit/delete confirmation tables out of the ordering
|
|
|
+ smoke test.
|
|
|
+ """
|
|
|
+ if cls.queryset_sources is not None:
|
|
|
+ return tuple(cls.queryset_sources)
|
|
|
+
|
|
|
+ model = cls.table._meta.model
|
|
|
+ app_label = model._meta.app_label
|
|
|
+ module = import_module(f"{app_label}.views")
|
|
|
+
|
|
|
+ sources = []
|
|
|
+ for _, view in inspect.getmembers(module, inspect.isclass):
|
|
|
+ if not cls.is_queryset_source_view(view):
|
|
|
+ continue
|
|
|
+
|
|
|
+ queryset = view.queryset
|
|
|
+ if hasattr(queryset, "all"):
|
|
|
+ queryset = queryset.all()
|
|
|
+
|
|
|
+ sources.append((view.__name__, queryset))
|
|
|
+
|
|
|
+ if not sources:
|
|
|
+ raise AssertionError(
|
|
|
+ f"{cls.__name__} could not find any list-style queryset source for "
|
|
|
+ f"{cls.table.__module__}.{cls.table.__name__}; "
|
|
|
+ "set `queryset_sources` explicitly if needed."
|
|
|
+ )
|
|
|
+
|
|
|
+ return tuple(sources)
|
|
|
+
|
|
|
+ def iter_orderable_columns(self, queryset):
|
|
|
+ for column in self.get_table(queryset).columns:
|
|
|
+ if not column.orderable:
|
|
|
+ continue
|
|
|
+ if column.name in self.excluded_orderable_columns:
|
|
|
+ continue
|
|
|
+ yield column.name
|
|
|
+
|
|
|
+
|
|
|
+class TableTestCases:
|
|
|
+ """
|
|
|
+ Keep test_* methods nested to avoid unittest auto-discovering the reusable
|
|
|
+ base classes directly.
|
|
|
+ """
|
|
|
+
|
|
|
+ class OrderableColumnsTestCase(ModelTableTestCase):
|
|
|
+ @classmethod
|
|
|
+ def setUpClass(cls):
|
|
|
+ super().setUpClass()
|
|
|
+ cls.validate_table_test_case()
|
|
|
+
|
|
|
+ def test_every_orderable_column_renders(self):
|
|
|
+ request = self.get_request()
|
|
|
+
|
|
|
+ for source_name, queryset in self.get_queryset_sources():
|
|
|
+ for column_name in self.iter_orderable_columns(queryset):
|
|
|
+ for direction, prefix in (("asc", ""), ("desc", "-")):
|
|
|
+ with self.cleanupSubTest(
|
|
|
+ source=source_name,
|
|
|
+ column=column_name,
|
|
|
+ direction=direction,
|
|
|
+ ):
|
|
|
+ table = self.get_table(queryset)
|
|
|
+ table.order_by = f"{prefix}{column_name}"
|
|
|
+ table.as_html(request)
|