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
« 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)
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 %}"
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 )
43class ImportProfileTable(NetBoxTable):
44 """Table for listing ImportProfile objects."""
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"))
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 )
88class ColumnMappingTable(tables.Table):
89 """Table for displaying ColumnMapping objects inline on the profile detail page."""
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")
95 class Meta:
96 model = ColumnMapping
97 fields = ("source_column", "target_field", "actions")
100class ClassRoleMappingTable(tables.Table):
101 """Table for displaying ClassRoleMapping objects inline on the profile detail page."""
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")
110 class Meta:
111 model = ClassRoleMapping
112 fields = ("source_class", "creates_rack", "rack_type", "role_slug", "ignore", "actions")
115class CableClassMappingTable(tables.Table):
116 """Display CableClass target decisions inline on the profile detail page."""
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 )
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)
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
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
152 class Meta:
153 model = CableClassMapping
154 fields = ("cable_class", "cable_type", "cable_profile", "actions")
157class PortClassMappingTable(tables.Table):
158 """Display PortClass label mappings inline on the profile detail page."""
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 )
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)
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
187 class Meta:
188 model = PortClassMapping
189 fields = ("source_label", "port_class", "actions")
192class DeviceTypeMappingTable(tables.Table):
193 """Table for displaying DeviceTypeMapping objects inline on the profile detail page."""
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")
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 )
212class ImportExecutionTable(NetBoxTable):
213 """Table for listing Import Execution records in the history view."""
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 )
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 )
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)"
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)
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)
282class ColumnTransformRuleTable(tables.Table):
283 """Table for displaying ColumnTransformRule objects inline on the profile detail page."""
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")
291 class Meta:
292 model = ColumnTransformRule
293 fields = ("source_column", "pattern", "group_1_target", "group_2_target", "actions")
296class InferenceBackendTable(NetBoxTable):
297 """List AI backends with the decisions that select one."""
299 backend_key = tables.Column(linkify=True)
300 display_name = tables.Column()
301 enabled = columns.BooleanColumn()
302 tags = columns.TagColumn()
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")