2
0

tables.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. import inspect
  2. from importlib import import_module
  3. from django.test import RequestFactory
  4. from netbox.views import generic
  5. from .base import TestCase
  6. __all__ = (
  7. "ModelTableTestCase",
  8. "TableTestCases",
  9. )
  10. class ModelTableTestCase(TestCase):
  11. """
  12. Shared helpers for model-backed table tests.
  13. Concrete subclasses should set `table` and may override `get_queryset()`
  14. or `excluded_orderable_columns` as needed.
  15. """
  16. table = None
  17. excluded_orderable_columns = frozenset({"actions"})
  18. # Optional explicit override for odd cases
  19. queryset_sources = None
  20. # Only these view types are considered sortable queryset sources by default
  21. queryset_source_view_classes = (generic.ObjectListView,)
  22. @classmethod
  23. def validate_table_test_case(cls):
  24. if cls.table is None:
  25. raise AssertionError(f"{cls.__name__} must define `table`")
  26. if getattr(cls.table._meta, "model", None) is None:
  27. raise AssertionError(f"{cls.__name__}.table must be model-backed")
  28. def get_request(self):
  29. request = RequestFactory().get("/")
  30. request.user = self.user
  31. return request
  32. def get_table(self, queryset):
  33. return self.table(queryset)
  34. @classmethod
  35. def is_queryset_source_view(cls, view):
  36. model = cls.table._meta.model
  37. app_label = model._meta.app_label
  38. return (
  39. inspect.isclass(view)
  40. and view.__module__.startswith(f"{app_label}.views")
  41. and getattr(view, "table", None) is cls.table
  42. and getattr(view, "queryset", None) is not None
  43. and issubclass(view, cls.queryset_source_view_classes)
  44. )
  45. @classmethod
  46. def get_queryset_sources(cls):
  47. """
  48. Return iterable of (label, queryset) pairs to test.
  49. By default, only discover list-style views that declare this table.
  50. That keeps bulk edit/delete confirmation tables out of the ordering
  51. smoke test.
  52. """
  53. if cls.queryset_sources is not None:
  54. return tuple(cls.queryset_sources)
  55. model = cls.table._meta.model
  56. app_label = model._meta.app_label
  57. module = import_module(f"{app_label}.views")
  58. sources = []
  59. for _, view in inspect.getmembers(module, inspect.isclass):
  60. if not cls.is_queryset_source_view(view):
  61. continue
  62. queryset = view.queryset
  63. if hasattr(queryset, "all"):
  64. queryset = queryset.all()
  65. sources.append((view.__name__, queryset))
  66. if not sources:
  67. raise AssertionError(
  68. f"{cls.__name__} could not find any list-style queryset source for "
  69. f"{cls.table.__module__}.{cls.table.__name__}; "
  70. "set `queryset_sources` explicitly if needed."
  71. )
  72. return tuple(sources)
  73. def iter_orderable_columns(self, queryset):
  74. for column in self.get_table(queryset).columns:
  75. if not column.orderable:
  76. continue
  77. if column.name in self.excluded_orderable_columns:
  78. continue
  79. yield column.name
  80. class TableTestCases:
  81. """
  82. Keep test_* methods nested to avoid unittest auto-discovering the reusable
  83. base classes directly.
  84. """
  85. class OrderableColumnsTestCase(ModelTableTestCase):
  86. @classmethod
  87. def setUpClass(cls):
  88. super().setUpClass()
  89. cls.validate_table_test_case()
  90. def test_every_orderable_column_renders(self):
  91. request = self.get_request()
  92. for source_name, queryset in self.get_queryset_sources():
  93. for column_name in self.iter_orderable_columns(queryset):
  94. for direction, prefix in (("asc", ""), ("desc", "-")):
  95. with self.cleanupSubTest(
  96. source=source_name,
  97. column=column_name,
  98. direction=direction,
  99. ):
  100. table = self.get_table(queryset)
  101. table.order_by = f"{prefix}{column_name}"
  102. table.as_html(request)