| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130 |
- 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)
|