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

1import inspect 

2from importlib import import_module 

3 

4from django.test import RequestFactory 

5 

6from netbox.views import generic 

7 

8from .base import TestCase 

9 

10__all__ = ( 

11 "ModelTableTestCase", 

12 "TableTestCases", 

13) 

14 

15 

16class ModelTableTestCase(TestCase): 

17 """ 

18 Shared helpers for model-backed table ordering smoke tests. 

19 

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"}) 

25 

26 # Optional explicit override for odd cases 

27 queryset_sources = None 

28 

29 # Only these view types are considered sortable queryset sources by default 

30 queryset_source_view_classes = (generic.ObjectListView,) 

31 

32 @classmethod 

33 def validate_table_test_case(cls): 

34 """ 

35 Assert that the test case is correctly configured with a model-backed table. 

36 

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

44 

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 

52 

53 def get_table(self, queryset): 

54 """ 

55 Instantiate the table class under test with the given *queryset*. 

56 """ 

57 return self.table(queryset) 

58 

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 

67 

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 ) 

75 

76 @classmethod 

77 def get_queryset_sources(cls): 

78 """ 

79 Return iterable of (label, queryset) pairs to test. 

80 

81 The label is included in the subtest failure output. 

82 

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) 

89 

90 model = cls.table._meta.model 

91 app_label = model._meta.app_label 

92 module = import_module(f"{app_label}.views") 

93 

94 sources = [] 

95 for _, view in inspect.getmembers(module, inspect.isclass): 

96 if not cls.is_queryset_source_view(view): 

97 continue 

98 

99 queryset = view.queryset 

100 if hasattr(queryset, "all"): 

101 queryset = queryset.all() 

102 

103 sources.append((view.__name__, queryset)) 

104 

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 ) 

111 

112 return tuple(sources) 

113 

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 

125 

126 

127class TableTestCases: 

128 """ 

129 Keep test_* methods nested to avoid unittest auto-discovering the reusable 

130 base classes directly. 

131 """ 

132 

133 class StandardTableTestCase(ModelTableTestCase): 

134 @classmethod 

135 def setUpClass(cls): 

136 super().setUpClass() 

137 cls.validate_table_test_case() 

138 

139 def test_every_orderable_column_renders(self): 

140 """ 

141 Verify that each declared ordering can be applied without error. 

142 

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

150 

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)