ipfabric_netbox 4.3.2b9__py3-none-any.whl → 4.3.2b10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ipfabric_netbox might be problematic. Click here for more details.

Files changed (49) hide show
  1. ipfabric_netbox/__init__.py +2 -2
  2. ipfabric_netbox/api/serializers.py +112 -7
  3. ipfabric_netbox/api/urls.py +6 -0
  4. ipfabric_netbox/api/views.py +23 -0
  5. ipfabric_netbox/choices.py +72 -40
  6. ipfabric_netbox/data/endpoint.json +47 -0
  7. ipfabric_netbox/data/filters.json +51 -0
  8. ipfabric_netbox/data/transform_map.json +188 -174
  9. ipfabric_netbox/exceptions.py +7 -5
  10. ipfabric_netbox/filtersets.py +310 -41
  11. ipfabric_netbox/forms.py +324 -79
  12. ipfabric_netbox/graphql/__init__.py +6 -0
  13. ipfabric_netbox/graphql/enums.py +5 -5
  14. ipfabric_netbox/graphql/filters.py +56 -4
  15. ipfabric_netbox/graphql/schema.py +28 -0
  16. ipfabric_netbox/graphql/types.py +61 -1
  17. ipfabric_netbox/jobs.py +5 -1
  18. ipfabric_netbox/migrations/0022_prepare_for_filters.py +182 -0
  19. ipfabric_netbox/migrations/0023_populate_filters_data.py +279 -0
  20. ipfabric_netbox/migrations/0024_finish_filters.py +29 -0
  21. ipfabric_netbox/models.py +384 -12
  22. ipfabric_netbox/navigation.py +98 -24
  23. ipfabric_netbox/tables.py +194 -9
  24. ipfabric_netbox/templates/ipfabric_netbox/htmx_list.html +5 -0
  25. ipfabric_netbox/templates/ipfabric_netbox/inc/combined_expressions.html +59 -0
  26. ipfabric_netbox/templates/ipfabric_netbox/inc/combined_expressions_content.html +39 -0
  27. ipfabric_netbox/templates/ipfabric_netbox/inc/endpoint_filters_with_selector.html +54 -0
  28. ipfabric_netbox/templates/ipfabric_netbox/ipfabricendpoint.html +39 -0
  29. ipfabric_netbox/templates/ipfabric_netbox/ipfabricfilter.html +51 -0
  30. ipfabric_netbox/templates/ipfabric_netbox/ipfabricfilterexpression.html +39 -0
  31. ipfabric_netbox/templates/ipfabric_netbox/ipfabricfilterexpression_edit.html +150 -0
  32. ipfabric_netbox/templates/ipfabric_netbox/ipfabricsync.html +1 -1
  33. ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap.html +16 -2
  34. ipfabric_netbox/templatetags/ipfabric_netbox_helpers.py +65 -0
  35. ipfabric_netbox/tests/api/test_api.py +333 -13
  36. ipfabric_netbox/tests/test_filtersets.py +2592 -0
  37. ipfabric_netbox/tests/test_forms.py +1256 -74
  38. ipfabric_netbox/tests/test_models.py +242 -34
  39. ipfabric_netbox/tests/test_views.py +2030 -25
  40. ipfabric_netbox/urls.py +35 -0
  41. ipfabric_netbox/utilities/endpoint.py +30 -0
  42. ipfabric_netbox/utilities/filters.py +88 -0
  43. ipfabric_netbox/utilities/ipfutils.py +254 -316
  44. ipfabric_netbox/utilities/logging.py +7 -7
  45. ipfabric_netbox/utilities/transform_map.py +126 -0
  46. ipfabric_netbox/views.py +719 -5
  47. {ipfabric_netbox-4.3.2b9.dist-info → ipfabric_netbox-4.3.2b10.dist-info}/METADATA +3 -2
  48. {ipfabric_netbox-4.3.2b9.dist-info → ipfabric_netbox-4.3.2b10.dist-info}/RECORD +49 -33
  49. {ipfabric_netbox-4.3.2b9.dist-info → ipfabric_netbox-4.3.2b10.dist-info}/WHEEL +1 -1
ipfabric_netbox/views.py CHANGED
@@ -1,3 +1,5 @@
1
+ import json
2
+
1
3
  from core.choices import ObjectChangeActionChoices
2
4
  from dcim.models import Device
3
5
  from dcim.models import Site
@@ -37,7 +39,11 @@ from utilities.views import GetRelatedModelsMixin
37
39
  from utilities.views import register_model_view
38
40
  from utilities.views import ViewTab
39
41
 
42
+ from .choices import IPFabricSourceTypeChoices
40
43
  from .filtersets import IPFabricDataFilterSet
44
+ from .filtersets import IPFabricEndpointFilterSet
45
+ from .filtersets import IPFabricFilterExpressionFilterSet
46
+ from .filtersets import IPFabricFilterFilterSet
41
47
  from .filtersets import IPFabricIngestionChangeFilterSet
42
48
  from .filtersets import IPFabricIngestionFilterSet
43
49
  from .filtersets import IPFabricIngestionIssueFilterSet
@@ -46,6 +52,12 @@ from .filtersets import IPFabricSourceFilterSet
46
52
  from .filtersets import IPFabricSyncFilterSet
47
53
  from .filtersets import IPFabricTransformMapFilterSet
48
54
  from .filtersets import IPFabricTransformMapGroupFilterSet
55
+ from .forms import IPFabricFilterBulkEditForm
56
+ from .forms import IPFabricFilterBulkImportForm
57
+ from .forms import IPFabricFilterExpressionBulkEditForm
58
+ from .forms import IPFabricFilterExpressionBulkImportForm
59
+ from .forms import IPFabricFilterExpressionForm
60
+ from .forms import IPFabricFilterForm
49
61
  from .forms import IPFabricIngestionFilterForm
50
62
  from .forms import IPFabricIngestionMergeForm
51
63
  from .forms import IPFabricRelationshipFieldForm
@@ -65,6 +77,9 @@ from .forms import IPFabricTransformMapGroupBulkEditForm
65
77
  from .forms import IPFabricTransformMapGroupBulkImportForm
66
78
  from .forms import IPFabricTransformMapGroupForm
67
79
  from .models import IPFabricData
80
+ from .models import IPFabricEndpoint
81
+ from .models import IPFabricFilter
82
+ from .models import IPFabricFilterExpression
68
83
  from .models import IPFabricIngestion
69
84
  from .models import IPFabricIngestionIssue
70
85
  from .models import IPFabricRelationshipField
@@ -76,6 +91,9 @@ from .models import IPFabricTransformMap
76
91
  from .models import IPFabricTransformMapGroup
77
92
  from .tables import DeviceIPFTable
78
93
  from .tables import IPFabricDataTable
94
+ from .tables import IPFabricEndpointTable
95
+ from .tables import IPFabricFilterExpressionTable
96
+ from .tables import IPFabricFilterTable
79
97
  from .tables import IPFabricIngestionChangesTable
80
98
  from .tables import IPFabricIngestionIssuesTable
81
99
  from .tables import IPFabricIngestionTable
@@ -86,11 +104,221 @@ from .tables import IPFabricSyncTable
86
104
  from .tables import IPFabricTransformFieldTable
87
105
  from .tables import IPFabricTransformMapGroupTable
88
106
  from .tables import IPFabricTransformMapTable
107
+ from .utilities.filters import get_filter_expression_test_candidates
89
108
  from .utilities.ipfutils import IPFabric
90
109
  from .utilities.transform_map import build_transform_maps
91
110
  from .utilities.transform_map import get_transform_map
92
111
 
93
112
 
113
+ # region - Base Classes for Combined Expressions Views
114
+
115
+
116
+ class CombinedExpressionsBaseView(LoginRequiredMixin, View):
117
+ """Base view for displaying combined filter expressions in an HTMX modal.
118
+
119
+ This base class provides common functionality for views that display
120
+ combined/merged filter expressions. Subclasses must implement
121
+ get_context_data() to provide view-specific context.
122
+
123
+ The template (inc/combined_expressions.html) supports three context types:
124
+ - "filter": Display merged expressions for a single IPFabricFilter
125
+ - "endpoint": Display combined filters for an endpoint within a sync
126
+ - "endpoint_all": Display combined filters for an endpoint across all syncs
127
+ """
128
+
129
+ template_name = "ipfabric_netbox/inc/combined_expressions.html"
130
+
131
+ def get_context_data(self, request, **kwargs):
132
+ """Override this method to provide view-specific context.
133
+
134
+ Must return a dict with standardized keys:
135
+ - object: The main object being displayed (IPFabricFilter or IPFabricEndpoint, or None)
136
+ - combined_expressions: The expressions (list for filters, dict for endpoints)
137
+ - context_type: One of "filter", "endpoint", or "endpoint_all"
138
+ - is_empty: Boolean indicating if there are no expressions
139
+ - sync: Optional IPFabricSync object (for endpoint context type only)
140
+
141
+ Args:
142
+ request: The HTTP request object
143
+ **kwargs: URL parameters passed to the view
144
+
145
+ Returns:
146
+ dict: Context dictionary for template rendering
147
+ """
148
+ raise NotImplementedError(
149
+ "Subclasses must implement get_context_data()"
150
+ ) # pragma: no cover
151
+
152
+ def add_no_cache_headers(self, response):
153
+ """Add cache control headers to prevent browser caching.
154
+
155
+ Args:
156
+ response: HttpResponse object
157
+
158
+ Returns:
159
+ HttpResponse: The response with cache headers added
160
+ """
161
+ response["Cache-Control"] = "no-cache, no-store, must-revalidate"
162
+ response["Pragma"] = "no-cache"
163
+ response["Expires"] = "0"
164
+ return response
165
+
166
+ def get(self, request, **kwargs):
167
+ """Handle GET requests by rendering the template with context.
168
+
169
+ Args:
170
+ request: The HTTP request object
171
+ **kwargs: URL parameters
172
+
173
+ Returns:
174
+ HttpResponse: Rendered template with no-cache headers
175
+ """
176
+ context = self.get_context_data(request, **kwargs)
177
+ response = render(request, self.template_name, context)
178
+ return self.add_no_cache_headers(response)
179
+
180
+
181
+ # endregion
182
+
183
+
184
+ # region - Endpoint (Read-Only)
185
+ @register_model_view(IPFabricEndpoint, "list", path="", detail=False)
186
+ class IPFabricEndpointListView(generic.ObjectListView):
187
+ queryset = IPFabricEndpoint.objects.annotate(
188
+ filters_count=models.Count("filters", distinct=True)
189
+ )
190
+ table = IPFabricEndpointTable
191
+ filterset = IPFabricEndpointFilterSet
192
+ template_name = "ipfabric_netbox/htmx_list.html"
193
+
194
+
195
+ @register_model_view(IPFabricEndpoint)
196
+ class IPFabricEndpointView(GetRelatedModelsMixin, generic.ObjectView):
197
+ queryset = IPFabricEndpoint.objects.all()
198
+ actions = ()
199
+
200
+ def get_extra_context(self, request, instance):
201
+ return {
202
+ "related_models": self.get_related_models(
203
+ request,
204
+ instance,
205
+ extra=(
206
+ (
207
+ IPFabricFilter.objects.restrict(request.user, "view").filter(
208
+ endpoints=instance
209
+ ),
210
+ "endpoints",
211
+ ),
212
+ ),
213
+ ),
214
+ }
215
+
216
+
217
+ @register_model_view(
218
+ IPFabricEndpoint,
219
+ name="filters",
220
+ path="filters",
221
+ kwargs={"model": IPFabricEndpoint},
222
+ )
223
+ class IPFabricEndpointFiltersView(CombinedExpressionsBaseView):
224
+ """Display combined filters for an endpoint with optional sync selection."""
225
+
226
+ template_name = "ipfabric_netbox/inc/endpoint_filters_with_selector.html"
227
+
228
+ def get_context_data(self, request, **kwargs):
229
+ """Provide context for endpoint filters with sync selection.
230
+
231
+ Args:
232
+ request: HTTP request (must be HTMX)
233
+ **kwargs: Must contain 'pk' (endpoint primary key)
234
+
235
+ Returns:
236
+ dict: Standardized context with endpoint object, available syncs,
237
+ and combined filters (optionally filtered by sync)
238
+ """
239
+ endpoint_pk = kwargs.get("pk")
240
+ sync_pk = request.GET.get("sync_pk")
241
+ from_sync = request.GET.get("from_sync") == "true"
242
+
243
+ # Return empty context if not HTMX or missing required parameters
244
+ if not request.htmx or not endpoint_pk:
245
+ return {
246
+ "object": None,
247
+ "combined_expressions": {},
248
+ "is_empty": True,
249
+ "context_type": "endpoint_all",
250
+ }
251
+
252
+ # Fetch endpoint
253
+ endpoint_obj = IPFabricEndpoint.objects.prefetch_related("filters").get(
254
+ pk=endpoint_pk
255
+ )
256
+
257
+ # Determine if we're filtering by sync
258
+ sync_obj = None
259
+ if sync_pk:
260
+ try:
261
+ sync_obj = IPFabricSync.objects.get(pk=sync_pk)
262
+ except IPFabricSync.DoesNotExist:
263
+ pass
264
+
265
+ # Combine filters (with optional sync filter)
266
+ combined = endpoint_obj.combine_filters(sync=sync_obj)
267
+
268
+ # Determine context type based on sync selection
269
+ context_type = "endpoint" if sync_obj else "endpoint_all"
270
+
271
+ context = {
272
+ "object": endpoint_obj,
273
+ "combined_expressions": combined,
274
+ "is_empty": len(combined) == 0,
275
+ "context_type": context_type,
276
+ "from_sync": from_sync,
277
+ }
278
+
279
+ # Add sync to context if one is selected
280
+ if sync_obj:
281
+ context["sync"] = sync_obj
282
+
283
+ # Only add sync selector context if not coming from a sync view
284
+ if not from_sync:
285
+ context["available_syncs"] = IPFabricSync.objects.all().order_by("name")
286
+ context["selected_sync_pk"] = int(sync_pk) if sync_pk else None
287
+
288
+ return context
289
+
290
+ def get(self, request, **kwargs):
291
+ """Handle GET requests, using appropriate template based on context.
292
+
293
+ Args:
294
+ request: The HTTP request object
295
+ **kwargs: URL parameters
296
+
297
+ Returns:
298
+ HttpResponse: Rendered template with no-cache headers
299
+ """
300
+ context = self.get_context_data(request, **kwargs)
301
+
302
+ # Determine which template to use
303
+ from_sync = request.GET.get("from_sync") == "true"
304
+ sync_pk_param = request.GET.get("sync_pk")
305
+
306
+ if from_sync:
307
+ # Coming from sync view - use simple template without selector
308
+ template_name = "ipfabric_netbox/inc/combined_expressions.html"
309
+ elif sync_pk_param is not None and not from_sync:
310
+ # Sync selection change via dropdown - use content-only template
311
+ template_name = "ipfabric_netbox/inc/combined_expressions_content.html"
312
+ else:
313
+ # Initial load from general endpoints - use template with selector
314
+ template_name = self.template_name
315
+
316
+ response = render(request, template_name, context)
317
+ return self.add_no_cache_headers(response)
318
+
319
+
320
+ # endregion
321
+
94
322
  # region - Transform Map Relationship Field
95
323
 
96
324
 
@@ -116,7 +344,6 @@ class IPFabricRelationshipFieldBulkDeleteView(generic.BulkDeleteView):
116
344
  table = IPFabricRelationshipFieldTable
117
345
 
118
346
 
119
- # This list is not linked in navigation, but it's needed for tests
120
347
  @register_model_view(IPFabricRelationshipField, "list", path="", detail=False)
121
348
  class IPFabricRelationshipFieldListView(generic.ObjectListView):
122
349
  queryset = IPFabricRelationshipField.objects.all()
@@ -242,9 +469,17 @@ class IPFabricTransformMapBulkDeleteView(generic.BulkDeleteView):
242
469
 
243
470
 
244
471
  @register_model_view(IPFabricTransformMap)
245
- class IPFabricTransformMapView(generic.ObjectView):
472
+ class IPFabricTransformMapView(GetRelatedModelsMixin, generic.ObjectView):
246
473
  queryset = IPFabricTransformMap.objects.all()
247
474
 
475
+ def get_extra_context(self, request, instance):
476
+ return {
477
+ "related_models": self.get_related_models(
478
+ request,
479
+ instance,
480
+ ),
481
+ }
482
+
248
483
 
249
484
  @register_model_view(IPFabricTransformMap, "restore", detail=False)
250
485
  class IPFabricTransformMapRestoreView(generic.ObjectListView):
@@ -331,7 +566,7 @@ class IPFabricTransformMapCloneView(BaseObjectView):
331
566
  # Clone the transform map - create a proper copy using Django model copying
332
567
  new_map = IPFabricTransformMap(
333
568
  name=form.cleaned_data["name"],
334
- source_model=obj.source_model,
569
+ source_endpoint=obj.source_endpoint,
335
570
  target_model=obj.target_model,
336
571
  group=form.cleaned_data["group"],
337
572
  )
@@ -446,7 +681,6 @@ class IPFabricTransformRelationshipView(generic.ObjectChildrenView):
446
681
  # region - Transform Map Field
447
682
 
448
683
 
449
- # This list is not linked in navigation, but it's needed for tests
450
684
  @register_model_view(IPFabricTransformField, "list", path="", detail=False)
451
685
  class IPFabricTransformFieldListView(generic.ObjectListView):
452
686
  queryset = IPFabricTransformField.objects.all()
@@ -845,7 +1079,7 @@ class IPFabricSyncBulkDeleteView(generic.BulkDeleteView):
845
1079
 
846
1080
 
847
1081
  @register_model_view(IPFabricSync, "transformmaps")
848
- class IPFabricTransformMapTabView(generic.ObjectChildrenView):
1082
+ class IPFabricTransformMapSyncTabView(generic.ObjectChildrenView):
849
1083
  queryset = IPFabricSync.objects.all()
850
1084
  child_model = IPFabricTransformMap
851
1085
  table = IPFabricTransformMapTable
@@ -865,6 +1099,105 @@ class IPFabricTransformMapTabView(generic.ObjectChildrenView):
865
1099
  )
866
1100
 
867
1101
 
1102
+ @register_model_view(IPFabricSync, "filters")
1103
+ class IPFabricTransformMapFilterTabView(generic.ObjectChildrenView):
1104
+ queryset = IPFabricSync.objects.all()
1105
+ child_model = IPFabricFilter
1106
+ table = IPFabricFilterTable
1107
+ template_name = "generic/object_children.html"
1108
+ actions = (AddObject, BulkDelete)
1109
+ tab = ViewTab(
1110
+ label=_("Filters"),
1111
+ badge=lambda obj: obj.filters.count(),
1112
+ permission="ipfabric_netbox.view_ipfabricfilter",
1113
+ )
1114
+
1115
+ def get_children(self, request, parent):
1116
+ return parent.filters.all()
1117
+
1118
+
1119
+ @register_model_view(IPFabricSync, "endpoints")
1120
+ class IPFabricSyncEndpointTabView(generic.ObjectChildrenView):
1121
+ queryset = IPFabricSync.objects.all()
1122
+ child_model = IPFabricEndpoint
1123
+ table = IPFabricEndpointTable
1124
+ template_name = "generic/object_children.html"
1125
+ actions = ()
1126
+ tab = ViewTab(
1127
+ label=_("Endpoints"),
1128
+ badge=lambda obj: IPFabricEndpoint.objects.filter(filters__syncs=obj)
1129
+ .distinct()
1130
+ .count(),
1131
+ permission="ipfabric_netbox.view_ipfabricendpoint",
1132
+ )
1133
+
1134
+ def get_children(self, request, parent):
1135
+ # Get all unique endpoints used by filters in this sync
1136
+ return (
1137
+ IPFabricEndpoint.objects.filter(filters__syncs=parent)
1138
+ .distinct()
1139
+ .annotate(
1140
+ sync_pk=models.Value(parent.pk, output_field=models.IntegerField()),
1141
+ filters_count=models.Count(
1142
+ "filters", filter=models.Q(filters__syncs=parent)
1143
+ ),
1144
+ )
1145
+ )
1146
+
1147
+ def get_table(self, *args, **kwargs):
1148
+ table = super().get_table(*args, **kwargs)
1149
+ # Override default columns to show the context-specific ones
1150
+ table.default_columns = ("name", "endpoint", "filters_count", "show_filters")
1151
+ return table
1152
+
1153
+
1154
+ @register_model_view(
1155
+ IPFabricSync,
1156
+ name="endpoint_filters",
1157
+ path="endpoint/<int:endpoint_pk>/filters",
1158
+ kwargs={"model": IPFabricSync},
1159
+ )
1160
+ class IPFabricSyncEndpointFiltersView(CombinedExpressionsBaseView):
1161
+ """Display combined filters for an endpoint within a specific sync."""
1162
+
1163
+ def get_context_data(self, request, **kwargs):
1164
+ """Provide context for endpoint filters within a sync.
1165
+
1166
+ Args:
1167
+ request: HTTP request (must be HTMX)
1168
+ **kwargs: Must contain 'pk' (sync primary key) and 'endpoint_pk'
1169
+
1170
+ Returns:
1171
+ dict: Standardized context with endpoint, sync, and combined filters
1172
+ """
1173
+ sync_pk = kwargs.get("pk")
1174
+ endpoint_pk = kwargs.get("endpoint_pk")
1175
+
1176
+ # Return empty context if not HTMX or missing required parameters
1177
+ if not request.htmx or not sync_pk or not endpoint_pk:
1178
+ return {
1179
+ "object": None,
1180
+ "combined_expressions": {},
1181
+ "is_empty": True,
1182
+ "context_type": "endpoint",
1183
+ }
1184
+
1185
+ # Fetch sync and endpoint objects
1186
+ sync_obj = IPFabricSync.objects.get(pk=sync_pk)
1187
+ endpoint_obj = IPFabricEndpoint.objects.get(pk=endpoint_pk)
1188
+
1189
+ # Combine filters for this endpoint within this sync
1190
+ combined = endpoint_obj.combine_filters(sync=sync_obj)
1191
+
1192
+ return {
1193
+ "object": endpoint_obj, # The endpoint is the main object
1194
+ "sync": sync_obj, # Pass sync for template context
1195
+ "combined_expressions": combined,
1196
+ "is_empty": len(combined) == 0,
1197
+ "context_type": "endpoint",
1198
+ }
1199
+
1200
+
868
1201
  @register_model_view(IPFabricSync, "ingestion")
869
1202
  class IPFabricIngestionTabView(generic.ObjectChildrenView):
870
1203
  queryset = IPFabricSync.objects.all()
@@ -1234,3 +1567,384 @@ class IPFabricTable(generic.ObjectView):
1234
1567
 
1235
1568
 
1236
1569
  # endregion
1570
+
1571
+
1572
+ # region - Filter
1573
+ @register_model_view(IPFabricFilter, "list", path="", detail=False)
1574
+ class IPFabricFilterListView(generic.ObjectListView):
1575
+ queryset = IPFabricFilter.objects.all()
1576
+ table = IPFabricFilterTable
1577
+ filterset = IPFabricFilterFilterSet
1578
+ template_name = "ipfabric_netbox/htmx_list.html"
1579
+
1580
+
1581
+ @register_model_view(IPFabricFilter, "add", detail=False)
1582
+ @register_model_view(IPFabricFilter, "edit")
1583
+ class IPFabricFilterEditView(generic.ObjectEditView):
1584
+ queryset = IPFabricFilter.objects.all()
1585
+ form = IPFabricFilterForm
1586
+ default_return_url = "plugins:ipfabric_netbox:ipfabricfilter_list"
1587
+
1588
+
1589
+ @register_model_view(IPFabricFilter, "delete")
1590
+ class IPFabricFilterDeleteView(generic.ObjectDeleteView):
1591
+ queryset = IPFabricFilter.objects.all()
1592
+ default_return_url = "plugins:ipfabric_netbox:ipfabricfilter_list"
1593
+
1594
+
1595
+ @register_model_view(IPFabricFilter, "bulk_import", path="import", detail=False)
1596
+ class IPFabricFilterBulkImportView(generic.BulkImportView):
1597
+ queryset = IPFabricFilter.objects.all()
1598
+ model_form = IPFabricFilterBulkImportForm
1599
+
1600
+
1601
+ @register_model_view(IPFabricFilter, "bulk_edit", path="edit", detail=False)
1602
+ class IPFabricFilterBulkEditView(generic.BulkEditView):
1603
+ queryset = IPFabricFilter.objects.all()
1604
+ table = IPFabricFilterTable
1605
+ form = IPFabricFilterBulkEditForm
1606
+
1607
+
1608
+ @register_model_view(IPFabricFilter, "bulk_rename", path="rename", detail=False)
1609
+ class IPFabricFilterBulkRenameView(generic.BulkRenameView):
1610
+ queryset = IPFabricFilter.objects.all()
1611
+
1612
+
1613
+ @register_model_view(IPFabricFilter, "bulk_delete", path="delete", detail=False)
1614
+ class IPFabricFilterBulkDeleteView(generic.BulkDeleteView):
1615
+ queryset = IPFabricFilter.objects.all()
1616
+ table = IPFabricFilterTable
1617
+
1618
+
1619
+ @register_model_view(IPFabricFilter)
1620
+ class IPFabricFilterView(GetRelatedModelsMixin, generic.ObjectView):
1621
+ queryset = IPFabricFilter.objects.all()
1622
+
1623
+ def get_extra_context(self, request, instance):
1624
+ return {
1625
+ "related_models": self.get_related_models(
1626
+ request,
1627
+ instance,
1628
+ extra=(
1629
+ (
1630
+ IPFabricSync.objects.restrict(request.user, "view").filter(
1631
+ filters__in=[instance]
1632
+ ),
1633
+ "filter_id",
1634
+ ),
1635
+ (
1636
+ IPFabricEndpoint.objects.restrict(request.user, "view").filter(
1637
+ filters__in=[instance]
1638
+ ),
1639
+ "ipfabric_filter_id",
1640
+ ),
1641
+ (
1642
+ IPFabricFilterExpression.objects.restrict(
1643
+ request.user, "view"
1644
+ ).filter(filters__in=[instance]),
1645
+ "ipfabric_filter_id",
1646
+ ),
1647
+ ),
1648
+ ),
1649
+ }
1650
+
1651
+
1652
+ @register_model_view(
1653
+ IPFabricFilter,
1654
+ name="combined_expressions",
1655
+ path="combined-expressions",
1656
+ kwargs={"model": IPFabricFilter},
1657
+ )
1658
+ class IPFabricFilterCombinedExpressionsView(CombinedExpressionsBaseView):
1659
+ """Display merged expressions for a single filter."""
1660
+
1661
+ def get_context_data(self, request, **kwargs):
1662
+ """Provide context for filter's merged expressions.
1663
+
1664
+ Args:
1665
+ request: HTTP request (must be HTMX)
1666
+ **kwargs: Must contain 'pk' (filter primary key)
1667
+
1668
+ Returns:
1669
+ dict: Standardized context with filter object and merged expressions
1670
+ """
1671
+ filter_pk = kwargs.get("pk")
1672
+
1673
+ # Return empty context if not HTMX or missing required parameters
1674
+ if not request.htmx or not filter_pk:
1675
+ return {
1676
+ "object": None,
1677
+ "combined_expressions": [],
1678
+ "is_empty": True,
1679
+ "context_type": "filter",
1680
+ }
1681
+
1682
+ # Force fresh query from database, prefetch expressions
1683
+ filter_obj = IPFabricFilter.objects.prefetch_related("expressions").get(
1684
+ pk=filter_pk
1685
+ )
1686
+ combined = filter_obj.merge_expressions()
1687
+
1688
+ return {
1689
+ "object": filter_obj,
1690
+ "combined_expressions": combined,
1691
+ "is_empty": len(combined) == 0,
1692
+ "context_type": "filter",
1693
+ }
1694
+
1695
+
1696
+ # endregion
1697
+
1698
+
1699
+ # region - Filter Expression
1700
+ @register_model_view(IPFabricFilterExpression, "list", path="", detail=False)
1701
+ class IPFabricFilterExpressionListView(generic.ObjectListView):
1702
+ queryset = IPFabricFilterExpression.objects.all()
1703
+ table = IPFabricFilterExpressionTable
1704
+ filterset = IPFabricFilterExpressionFilterSet
1705
+
1706
+
1707
+ @register_model_view(IPFabricFilterExpression, "add", detail=False)
1708
+ @register_model_view(IPFabricFilterExpression, "edit")
1709
+ class IPFabricFilterExpressionEditView(generic.ObjectEditView):
1710
+ queryset = IPFabricFilterExpression.objects.all()
1711
+ form = IPFabricFilterExpressionForm
1712
+ default_return_url = "plugins:ipfabric_netbox:ipfabricfilterexpression_list"
1713
+ template_name = "ipfabric_netbox/ipfabricfilterexpression_edit.html"
1714
+
1715
+ def get_extra_context(self, request, instance):
1716
+ """Add test configuration context to the template."""
1717
+ context = super().get_extra_context(request, instance)
1718
+
1719
+ # Add test-related context if editing existing object
1720
+ if instance.pk:
1721
+ # Get available test sources and endpoints
1722
+ sources, endpoints = get_filter_expression_test_candidates(instance)
1723
+ context["available_test_sources"] = list(sources)
1724
+ context["available_test_endpoints"] = list(endpoints)
1725
+ context["can_test"] = bool(sources and endpoints)
1726
+ else:
1727
+ context["can_test"] = False
1728
+
1729
+ return context
1730
+
1731
+
1732
+ @register_model_view(IPFabricFilterExpression, "delete")
1733
+ class IPFabricFilterExpressionDeleteView(generic.ObjectDeleteView):
1734
+ queryset = IPFabricFilterExpression.objects.all()
1735
+ default_return_url = "plugins:ipfabric_netbox:ipfabricfilterexpression_list"
1736
+
1737
+
1738
+ @register_model_view(
1739
+ IPFabricFilterExpression, "bulk_import", path="import", detail=False
1740
+ )
1741
+ class IPFabricFilterExpressionBulkImportView(generic.BulkImportView):
1742
+ queryset = IPFabricFilterExpression.objects.all()
1743
+ model_form = IPFabricFilterExpressionBulkImportForm
1744
+
1745
+
1746
+ @register_model_view(IPFabricFilterExpression, "bulk_edit", path="edit", detail=False)
1747
+ class IPFabricFilterExpressionBulkEditView(generic.BulkEditView):
1748
+ queryset = IPFabricFilterExpression.objects.all()
1749
+ table = IPFabricFilterExpressionTable
1750
+ form = IPFabricFilterExpressionBulkEditForm
1751
+
1752
+
1753
+ @register_model_view(
1754
+ IPFabricFilterExpression, "bulk_rename", path="rename", detail=False
1755
+ )
1756
+ class IPFabricFilterExpressionBulkRenameView(generic.BulkRenameView):
1757
+ queryset = IPFabricFilterExpression.objects.all()
1758
+
1759
+
1760
+ @register_model_view(
1761
+ IPFabricFilterExpression, "bulk_delete", path="delete", detail=False
1762
+ )
1763
+ class IPFabricFilterExpressionBulkDeleteView(generic.BulkDeleteView):
1764
+ queryset = IPFabricFilterExpression.objects.all()
1765
+ table = IPFabricFilterExpressionTable
1766
+
1767
+
1768
+ @register_model_view(IPFabricFilterExpression)
1769
+ class IPFabricFilterExpressionView(GetRelatedModelsMixin, generic.ObjectView):
1770
+ queryset = IPFabricFilterExpression.objects.all()
1771
+
1772
+ def get_extra_context(self, request, instance):
1773
+ return {
1774
+ "related_models": self.get_related_models(
1775
+ request,
1776
+ instance,
1777
+ extra=(
1778
+ (
1779
+ IPFabricFilter.objects.restrict(request.user, "view").filter(
1780
+ expressions__in=[instance]
1781
+ ),
1782
+ "expression_id",
1783
+ ),
1784
+ ),
1785
+ ),
1786
+ }
1787
+
1788
+
1789
+ @register_model_view(IPFabricFilterExpression, "test")
1790
+ class IPFabricFilterExpressionTestView(View):
1791
+ """Test a filter expression against IP Fabric API."""
1792
+
1793
+ def post(self, request, pk=None):
1794
+ # Get test parameters from POST data
1795
+ test_source_id = request.POST.get("test_source")
1796
+ test_endpoint_id = request.POST.get("test_endpoint")
1797
+ expression_json = request.POST.get("expression")
1798
+
1799
+ return self._test_expression_logic(
1800
+ request, test_source_id, test_endpoint_id, expression_json
1801
+ )
1802
+
1803
+ @staticmethod
1804
+ def _test_expression_logic(
1805
+ request, test_source_id, test_endpoint_id, expression_json
1806
+ ):
1807
+ """Shared logic for testing filter expressions (works for both saved and unsaved)."""
1808
+
1809
+ # Validate required fields
1810
+ if not test_source_id:
1811
+ return HttpResponse(
1812
+ json.dumps({"success": False, "error": "Please select a Test Source."}),
1813
+ content_type="application/json",
1814
+ )
1815
+
1816
+ if not test_endpoint_id:
1817
+ return HttpResponse(
1818
+ json.dumps(
1819
+ {"success": False, "error": "Please select a Test Endpoint."}
1820
+ ),
1821
+ content_type="application/json",
1822
+ )
1823
+
1824
+ # Validate expression JSON
1825
+ if expression_json:
1826
+ try:
1827
+ expression_data = json.loads(expression_json)
1828
+ if not isinstance(expression_data, list):
1829
+ return HttpResponse(
1830
+ json.dumps(
1831
+ {
1832
+ "success": False,
1833
+ "error": "Expression must be a JSON list.",
1834
+ }
1835
+ ),
1836
+ content_type="application/json",
1837
+ )
1838
+ for idx, item in enumerate(expression_data):
1839
+ if not isinstance(item, dict):
1840
+ return HttpResponse(
1841
+ json.dumps(
1842
+ {
1843
+ "success": False,
1844
+ "error": f"Expression item at index {idx} must be a dictionary.",
1845
+ }
1846
+ ),
1847
+ content_type="application/json",
1848
+ )
1849
+ except json.JSONDecodeError as e:
1850
+ return HttpResponse(
1851
+ json.dumps({"success": False, "error": f"Invalid JSON: {str(e)}"}),
1852
+ content_type="application/json",
1853
+ )
1854
+ else:
1855
+ return HttpResponse(
1856
+ json.dumps({"success": False, "error": "Expression is required."}),
1857
+ content_type="application/json",
1858
+ )
1859
+
1860
+ # Get source and endpoint objects
1861
+ try:
1862
+ source = IPFabricSource.objects.get(pk=test_source_id)
1863
+ except IPFabricSource.DoesNotExist:
1864
+ return HttpResponse(
1865
+ json.dumps({"success": False, "error": "Selected source not found."}),
1866
+ content_type="application/json",
1867
+ status=404,
1868
+ )
1869
+
1870
+ # Check if source is LOCAL
1871
+ if source.type != IPFabricSourceTypeChoices.LOCAL:
1872
+ return HttpResponse(
1873
+ json.dumps(
1874
+ {
1875
+ "success": False,
1876
+ "error": "Cannot test against REMOTE sources. Please select a LOCAL IP Fabric source with direct API access.",
1877
+ }
1878
+ ),
1879
+ content_type="application/json",
1880
+ )
1881
+
1882
+ try:
1883
+ endpoint = IPFabricEndpoint.objects.get(pk=test_endpoint_id)
1884
+ except IPFabricEndpoint.DoesNotExist:
1885
+ return HttpResponse(
1886
+ json.dumps({"success": False, "error": "Selected endpoint not found."}),
1887
+ content_type="application/json",
1888
+ status=404,
1889
+ )
1890
+
1891
+ # Test the expression against IP Fabric API
1892
+ try:
1893
+ # Check that source has auth token
1894
+ auth = source.parameters.get("auth")
1895
+ if not auth:
1896
+ return HttpResponse(
1897
+ json.dumps(
1898
+ {
1899
+ "success": False,
1900
+ "error": "Source is missing API authentication token.",
1901
+ }
1902
+ ),
1903
+ content_type="application/json",
1904
+ )
1905
+
1906
+ # Prepare parameters for IPFabric client (same pattern as other views)
1907
+ parameters = source.parameters.copy()
1908
+ parameters.update(
1909
+ {
1910
+ "snapshot_id": "$last",
1911
+ "base_url": source.url,
1912
+ "pagination": {"limit": 1, "start": 0},
1913
+ }
1914
+ )
1915
+ # Build filter - use AND type as default (IPF doesn't distinguish between types at this level)
1916
+ filter_dict = {"and": expression_data}
1917
+ ipf = IPFabric(parameters=parameters)
1918
+ results = ipf.ipf.fetch_all(
1919
+ url=endpoint.endpoint, snapshot_id="$last", filters=filter_dict
1920
+ )
1921
+
1922
+ # Return success
1923
+ return HttpResponse(
1924
+ json.dumps(
1925
+ {
1926
+ "success": True,
1927
+ "count": len(results),
1928
+ "message": f"Test successful! Expression matched {len(results)} result(s) (limited to 1 for testing).",
1929
+ }
1930
+ ),
1931
+ content_type="application/json",
1932
+ )
1933
+
1934
+ except Exception as e:
1935
+ # Return error with helpful context
1936
+ error_msg = str(e)
1937
+
1938
+ # Enhance error message for common issues
1939
+ if "Unrecognized key(s)" in error_msg:
1940
+ error_msg += f" | Hint: The field name in your expression doesn't exist in the '{endpoint.endpoint}' endpoint. Check the IP Fabric API documentation for valid field names for this table."
1941
+ elif "Invalid Input" in error_msg or "VALIDATION_FAILED" in error_msg:
1942
+ error_msg += f" | Hint: Check that your filter syntax is correct and field names are valid for endpoint '{endpoint.endpoint}'."
1943
+
1944
+ return HttpResponse(
1945
+ json.dumps({"success": False, "error": f"API Error: {error_msg}"}),
1946
+ content_type="application/json",
1947
+ )
1948
+
1949
+
1950
+ # endregion