Coverage for utilities/testing/tables.py: 0%
68 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
1import inspect
2from importlib import import_module
4from django.test import RequestFactory
6from netbox.views import generic
8from .base import TestCase
10__all__ = (
11 "ModelTableTestCase",
12 "TableTestCases",
13)
16class ModelTableTestCase(TestCase):
17 """
18 Shared helpers for model-backed table ordering smoke tests.
20 Concrete subclasses should set `table` and may override `queryset_sources`,
21 `queryset_source_view_classes`, or `excluded_orderable_columns` as needed.
22 """
23 table = None
24 excluded_orderable_columns = frozenset({"actions"})
26 # Optional explicit override for odd cases
27 queryset_sources = None
29 # Only these view types are considered sortable queryset sources by default
30 queryset_source_view_classes = (generic.ObjectListView,)
32 @classmethod
33 def validate_table_test_case(cls):
34 """
35 Assert that the test case is correctly configured with a model-backed table.
37 Raises:
38 AssertionError: If ``table`` is not set or is not backed by a Django model.
39 """
40 if cls.table is None:
41 raise AssertionError(f"{cls.__name__} must define `table`")
42 if getattr(cls.table._meta, "model", None) is None:
43 raise AssertionError(f"{cls.__name__}.table must be model-backed")
45 def get_request(self):
46 """
47 Build a minimal ``GET`` request authenticated as the test user.
48 """
49 request = RequestFactory().get("/")
50 request.user = self.user
51 return request
53 def get_table(self, queryset):
54 """
55 Instantiate the table class under test with the given *queryset*.
56 """
57 return self.table(queryset)
59 @classmethod
60 def is_queryset_source_view(cls, view):
61 """
62 Return ``True`` if *view* is a list-style view class that declares
63 this test case's table and exposes a usable queryset.
64 """
65 model = cls.table._meta.model
66 app_label = model._meta.app_label
68 return (
69 inspect.isclass(view)
70 and view.__module__.startswith(f"{app_label}.views")
71 and getattr(view, "table", None) is cls.table
72 and getattr(view, "queryset", None) is not None
73 and issubclass(view, cls.queryset_source_view_classes)
74 )
76 @classmethod
77 def get_queryset_sources(cls):
78 """
79 Return iterable of (label, queryset) pairs to test.
81 The label is included in the subtest failure output.
83 By default, only discover list-style views that declare this table.
84 That keeps bulk edit/delete confirmation tables out of the ordering
85 smoke test.
86 """
87 if cls.queryset_sources is not None:
88 return tuple(cls.queryset_sources)
90 model = cls.table._meta.model
91 app_label = model._meta.app_label
92 module = import_module(f"{app_label}.views")
94 sources = []
95 for _, view in inspect.getmembers(module, inspect.isclass):
96 if not cls.is_queryset_source_view(view):
97 continue
99 queryset = view.queryset
100 if hasattr(queryset, "all"):
101 queryset = queryset.all()
103 sources.append((view.__name__, queryset))
105 if not sources:
106 raise AssertionError(
107 f"{cls.__name__} could not find any list-style queryset source for "
108 f"{cls.table.__module__}.{cls.table.__name__}; "
109 "set `queryset_sources` explicitly if needed."
110 )
112 return tuple(sources)
114 def iter_orderable_columns(self, queryset):
115 """
116 Yield the names of all orderable columns for *queryset*, excluding
117 any listed in ``excluded_orderable_columns``.
118 """
119 for column in self.get_table(queryset).columns:
120 if not column.orderable:
121 continue
122 if column.name in self.excluded_orderable_columns:
123 continue
124 yield column.name
127class TableTestCases:
128 """
129 Keep test_* methods nested to avoid unittest auto-discovering the reusable
130 base classes directly.
131 """
133 class StandardTableTestCase(ModelTableTestCase):
134 @classmethod
135 def setUpClass(cls):
136 super().setUpClass()
137 cls.validate_table_test_case()
139 def test_every_orderable_column_renders(self):
140 """
141 Verify that each declared ordering can be applied without error.
143 This is intentionally a smoke test. It validates ordering against
144 the configured queryset sources but does not create model
145 instances by default, so it complements rather than replaces
146 data-backed rendering tests for tables whose behavior depends on
147 populated querysets.
148 """
149 request = self.get_request()
151 for source_name, queryset in self.get_queryset_sources():
152 for column_name in self.iter_orderable_columns(queryset):
153 for direction, prefix in (("asc", ""), ("desc", "-")):
154 with self.subTest(source=source_name, column=column_name, direction=direction):
155 table = self.get_table(queryset)
156 table.order_by = f"{prefix}{column_name}"
157 table.as_html(request)