Coverage for netbox_data_import/tables.py: 100.00%

123 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-10-30 09:47 +0000

1# SPDX-License-Identifier: Apache-2.0 

2# Copyright (C) 2025 Marcin Zieba <marcinpsk@gmail.com> 

3import django_tables2 as tables 

4from netbox.tables import NetBoxTable, columns 

5from .cable_disclosure import POLICY_HIDDEN 

6from .models import ( 

7 CableClassMapping, 

8 ImportProfile, 

9 InferenceBackend, 

10 ColumnMapping, 

11 ClassRoleMapping, 

12 DeviceTypeMapping, 

13 ColumnTransformRule, 

14 ImportExecution, 

15 PortClassMapping, 

16) 

17 

18 

19_MAPPING_ACTIONS_TEMPLATE = """ 

20<a href="{% url edit_url record.pk %}" class="btn btn-sm btn-warning" aria-label="Edit {{ record }}"> 

21 <i class="mdi mdi-pencil"></i> 

22</a> 

23<a href="{% url delete_url record.pk %}" class="btn btn-sm btn-danger" aria-label="Delete {{ record }}"> 

24 <i class="mdi mdi-trash-can-outline"></i> 

25</a> 

26""" 

27_CABLE_MAPPING_ACTIONS_TEMPLATE = "{% if record.policy_viewable %}" + _MAPPING_ACTIONS_TEMPLATE + "{% endif %}" 

28 

29 

30def _mapping_actions_column(route_prefix: str) -> tables.TemplateColumn: 

31 """Return the edit and delete actions every inline mapping table shares.""" 

32 return tables.TemplateColumn( 

33 template_code=_MAPPING_ACTIONS_TEMPLATE, 

34 extra_context={ 

35 "edit_url": f"plugins:netbox_data_import:{route_prefix}_edit", 

36 "delete_url": f"plugins:netbox_data_import:{route_prefix}_delete", 

37 }, 

38 verbose_name="", 

39 orderable=False, 

40 ) 

41 

42 

43class ImportProfileTable(NetBoxTable): 

44 """Table for listing ImportProfile objects.""" 

45 

46 name = tables.Column(linkify=True) 

47 source_adapter = tables.Column() 

48 column_mappings = tables.Column( 

49 accessor="column_mappings.count", 

50 verbose_name="Columns", 

51 orderable=False, 

52 ) 

53 class_role_mappings = tables.Column( 

54 accessor="class_role_mappings.count", 

55 verbose_name="Class Mappings", 

56 orderable=False, 

57 ) 

58 device_type_mappings = tables.Column( 

59 accessor="device_type_mappings.count", 

60 verbose_name="DT Mappings", 

61 orderable=False, 

62 ) 

63 tags = columns.TagColumn(url_name="plugins:netbox_data_import:importprofile_list") 

64 actions = columns.ActionsColumn(actions=("edit", "delete")) 

65 

66 class Meta(NetBoxTable.Meta): 

67 model = ImportProfile 

68 fields = ( 

69 "pk", 

70 "name", 

71 "source_adapter", 

72 "column_mappings", 

73 "class_role_mappings", 

74 "device_type_mappings", 

75 "tags", 

76 "actions", 

77 ) 

78 default_columns = ( 

79 "name", 

80 "source_adapter", 

81 "column_mappings", 

82 "class_role_mappings", 

83 "device_type_mappings", 

84 "actions", 

85 ) 

86 

87 

88class ColumnMappingTable(tables.Table): 

89 """Table for displaying ColumnMapping objects inline on the profile detail page.""" 

90 

91 source_column = tables.Column() 

92 target_field = tables.Column(accessor="get_target_field_display", order_by="target_field") 

93 actions = _mapping_actions_column("columnmapping") 

94 

95 class Meta: 

96 model = ColumnMapping 

97 fields = ("source_column", "target_field", "actions") 

98 

99 

100class ClassRoleMappingTable(tables.Table): 

101 """Table for displaying ClassRoleMapping objects inline on the profile detail page.""" 

102 

103 source_class = tables.Column() 

104 creates_rack = tables.BooleanColumn() 

105 rack_type = tables.Column(verbose_name="Rack Type", accessor="rack_type", default="—") 

106 role_slug = tables.Column() 

107 ignore = tables.BooleanColumn() 

108 actions = _mapping_actions_column("classrolemapping") 

109 

110 class Meta: 

111 model = ClassRoleMapping 

112 fields = ("source_class", "creates_rack", "rack_type", "role_slug", "ignore", "actions") 

113 

114 

115class CableClassMappingTable(tables.Table): 

116 """Display CableClass target decisions inline on the profile detail page.""" 

117 

118 cable_class = tables.Column(verbose_name="CableClass") 

119 cable_type = tables.Column(accessor="cable_type_display", order_by="cable_type", verbose_name="Cable Type") 

120 cable_profile = tables.Column( 

121 accessor="cable_profile_display", order_by="cable_profile", verbose_name="Cable Profile" 

122 ) 

123 actions = tables.TemplateColumn( 

124 template_code=_CABLE_MAPPING_ACTIONS_TEMPLATE, 

125 extra_context={ 

126 "edit_url": "plugins:netbox_data_import:cableclassmapping_edit", 

127 "delete_url": "plugins:netbox_data_import:cableclassmapping_delete", 

128 }, 

129 verbose_name="", 

130 orderable=False, 

131 ) 

132 

133 def __init__(self, data, *, viewer): 

134 rows = list(data) 

135 self.visible_ids = set( 

136 CableClassMapping.objects.restrict(viewer, "view") 

137 .filter(pk__in=[row.pk for row in rows]) 

138 .values_list("pk", flat=True) 

139 ) 

140 for row in rows: 

141 row.policy_viewable = row.pk in self.visible_ids # type: ignore[attr-defined] 

142 super().__init__(rows) 

143 

144 def render_cable_type(self, record): 

145 """Render the decision only while this viewer may read its row.""" 

146 return record.cable_type_display() if record.pk in self.visible_ids else POLICY_HIDDEN 

147 

148 def render_cable_profile(self, record): 

149 """Render the decision only while this viewer may read its row.""" 

150 return record.cable_profile_display() if record.pk in self.visible_ids else POLICY_HIDDEN 

151 

152 class Meta: 

153 model = CableClassMapping 

154 fields = ("cable_class", "cable_type", "cable_profile", "actions") 

155 

156 

157class PortClassMappingTable(tables.Table): 

158 """Display PortClass label mappings inline on the profile detail page.""" 

159 

160 source_label = tables.Column(verbose_name="Source label") 

161 port_class = tables.Column(verbose_name="PortClass") 

162 actions = tables.TemplateColumn( 

163 template_code=_CABLE_MAPPING_ACTIONS_TEMPLATE, 

164 extra_context={ 

165 "edit_url": "plugins:netbox_data_import:portclassmapping_edit", 

166 "delete_url": "plugins:netbox_data_import:portclassmapping_delete", 

167 }, 

168 verbose_name="", 

169 orderable=False, 

170 ) 

171 

172 def __init__(self, data, *, viewer): 

173 rows = list(data) 

174 self.visible_ids = set( 

175 PortClassMapping.objects.restrict(viewer, "view") 

176 .filter(pk__in=[row.pk for row in rows]) 

177 .values_list("pk", flat=True) 

178 ) 

179 for row in rows: 

180 row.policy_viewable = row.pk in self.visible_ids # type: ignore[attr-defined] 

181 super().__init__(rows) 

182 

183 def render_port_class(self, record): 

184 """Render the decision only while this viewer may read its row.""" 

185 return record.port_class if record.pk in self.visible_ids else POLICY_HIDDEN 

186 

187 class Meta: 

188 model = PortClassMapping 

189 fields = ("source_label", "port_class", "actions") 

190 

191 

192class DeviceTypeMappingTable(tables.Table): 

193 """Table for displaying DeviceTypeMapping objects inline on the profile detail page.""" 

194 

195 source_make = tables.Column() 

196 source_model = tables.Column() 

197 netbox_manufacturer_slug = tables.Column() 

198 netbox_device_type_slug = tables.Column() 

199 actions = _mapping_actions_column("devicetypemapping") 

200 

201 class Meta: 

202 model = DeviceTypeMapping 

203 fields = ( 

204 "source_make", 

205 "source_model", 

206 "netbox_manufacturer_slug", 

207 "netbox_device_type_slug", 

208 "actions", 

209 ) 

210 

211 

212class ImportExecutionTable(NetBoxTable): 

213 """Table for listing Import Execution records in the history view.""" 

214 

215 created = tables.DateTimeColumn(format="Y-m-d H:i") 

216 profile = tables.Column(linkify=lambda record: record.profile.get_absolute_url() if record.profile else None) 

217 input_filename = tables.Column(verbose_name="File") 

218 site_name = tables.Column(verbose_name="Site") 

219 racks_created = tables.Column( 

220 accessor="result_counts", 

221 verbose_name="Racks", 

222 orderable=False, 

223 ) 

224 devices_created = tables.Column( 

225 accessor="result_counts", 

226 verbose_name="Devices", 

227 orderable=False, 

228 ) 

229 errors = tables.TemplateColumn( 

230 template_code=""" 

231 {% with c=record.result_counts %} 

232 {% if c.errors %}<span class="badge bg-danger">{{ c.errors }}</span>{% else %}—{% endif %} 

233 {% endwith %} 

234 """, 

235 verbose_name="Errors", 

236 orderable=False, 

237 ) 

238 

239 class Meta(NetBoxTable.Meta): 

240 model = ImportExecution 

241 fields = ( 

242 "pk", 

243 "created", 

244 "profile", 

245 "input_filename", 

246 "site_name", 

247 "racks_created", 

248 "devices_created", 

249 "errors", 

250 ) 

251 default_columns = ( 

252 "created", 

253 "profile", 

254 "input_filename", 

255 "site_name", 

256 "racks_created", 

257 "devices_created", 

258 "errors", 

259 ) 

260 

261 def render_profile(self, record): 

262 """Return the profile name, or a placeholder for deleted profiles.""" 

263 if record.profile: 

264 return record.profile.name 

265 return "(deleted)" 

266 

267 def render_racks_created(self, value): 

268 """Extract racks_created count from the JSON result_counts dict.""" 

269 value = value or {} 

270 if isinstance(value.get("created"), dict): 

271 return value["created"].get("rack", 0) 

272 return value.get("racks_created", 0) 

273 

274 def render_devices_created(self, value): 

275 """Extract devices_created count from the JSON result_counts dict.""" 

276 value = value or {} 

277 if isinstance(value.get("created"), dict): 

278 return value["created"].get("device", 0) 

279 return value.get("devices_created", 0) 

280 

281 

282class ColumnTransformRuleTable(tables.Table): 

283 """Table for displaying ColumnTransformRule objects inline on the profile detail page.""" 

284 

285 source_column = tables.Column() 

286 pattern = tables.Column() 

287 group_1_target = tables.Column() 

288 group_2_target = tables.Column() 

289 actions = _mapping_actions_column("columntransformrule") 

290 

291 class Meta: 

292 model = ColumnTransformRule 

293 fields = ("source_column", "pattern", "group_1_target", "group_2_target", "actions") 

294 

295 

296class InferenceBackendTable(NetBoxTable): 

297 """List AI backends with the decisions that select one.""" 

298 

299 backend_key = tables.Column(linkify=True) 

300 display_name = tables.Column() 

301 enabled = columns.BooleanColumn() 

302 tags = columns.TagColumn() 

303 

304 class Meta(NetBoxTable.Meta): 

305 model = InferenceBackend 

306 fields = ("pk", "backend_key", "display_name", "adapter_type", "api_root", "model", "enabled", "tags") 

307 default_columns = ("backend_key", "display_name", "adapter_type", "model", "enabled")