ipfabric_netbox 4.3.2b9__py3-none-any.whl → 4.3.2b11__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.
- ipfabric_netbox/__init__.py +1 -1
- ipfabric_netbox/api/serializers.py +112 -7
- ipfabric_netbox/api/urls.py +6 -0
- ipfabric_netbox/api/views.py +23 -0
- ipfabric_netbox/choices.py +74 -40
- ipfabric_netbox/data/endpoint.json +52 -0
- ipfabric_netbox/data/filters.json +51 -0
- ipfabric_netbox/data/transform_map.json +190 -176
- ipfabric_netbox/exceptions.py +7 -5
- ipfabric_netbox/filtersets.py +310 -41
- ipfabric_netbox/forms.py +330 -80
- ipfabric_netbox/graphql/__init__.py +6 -0
- ipfabric_netbox/graphql/enums.py +5 -5
- ipfabric_netbox/graphql/filters.py +56 -4
- ipfabric_netbox/graphql/schema.py +28 -0
- ipfabric_netbox/graphql/types.py +61 -1
- ipfabric_netbox/jobs.py +12 -1
- ipfabric_netbox/migrations/0022_prepare_for_filters.py +182 -0
- ipfabric_netbox/migrations/0023_populate_filters_data.py +303 -0
- ipfabric_netbox/migrations/0024_finish_filters.py +29 -0
- ipfabric_netbox/migrations/0025_add_vss_chassis_endpoint.py +166 -0
- ipfabric_netbox/models.py +432 -17
- ipfabric_netbox/navigation.py +98 -24
- ipfabric_netbox/tables.py +194 -9
- ipfabric_netbox/templates/ipfabric_netbox/htmx_list.html +5 -0
- ipfabric_netbox/templates/ipfabric_netbox/inc/combined_expressions.html +59 -0
- ipfabric_netbox/templates/ipfabric_netbox/inc/combined_expressions_content.html +39 -0
- ipfabric_netbox/templates/ipfabric_netbox/inc/endpoint_filters_with_selector.html +54 -0
- ipfabric_netbox/templates/ipfabric_netbox/ipfabricendpoint.html +39 -0
- ipfabric_netbox/templates/ipfabric_netbox/ipfabricfilter.html +51 -0
- ipfabric_netbox/templates/ipfabric_netbox/ipfabricfilterexpression.html +39 -0
- ipfabric_netbox/templates/ipfabric_netbox/ipfabricfilterexpression_edit.html +150 -0
- ipfabric_netbox/templates/ipfabric_netbox/ipfabricsync.html +1 -1
- ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap.html +16 -2
- ipfabric_netbox/templatetags/ipfabric_netbox_helpers.py +68 -0
- ipfabric_netbox/tests/api/test_api.py +333 -13
- ipfabric_netbox/tests/test_filtersets.py +2592 -0
- ipfabric_netbox/tests/test_forms.py +1349 -74
- ipfabric_netbox/tests/test_models.py +242 -34
- ipfabric_netbox/tests/test_views.py +2031 -26
- ipfabric_netbox/urls.py +35 -0
- ipfabric_netbox/utilities/endpoint.py +83 -0
- ipfabric_netbox/utilities/filters.py +88 -0
- ipfabric_netbox/utilities/ipfutils.py +393 -377
- ipfabric_netbox/utilities/logging.py +7 -7
- ipfabric_netbox/utilities/transform_map.py +144 -5
- ipfabric_netbox/views.py +719 -5
- {ipfabric_netbox-4.3.2b9.dist-info → ipfabric_netbox-4.3.2b11.dist-info}/METADATA +2 -2
- {ipfabric_netbox-4.3.2b9.dist-info → ipfabric_netbox-4.3.2b11.dist-info}/RECORD +50 -33
- {ipfabric_netbox-4.3.2b9.dist-info → ipfabric_netbox-4.3.2b11.dist-info}/WHEEL +1 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{% extends 'generic/object.html' %}
|
|
2
|
+
{% load static %}
|
|
3
|
+
{% load helpers %}
|
|
4
|
+
{% load plugins %}
|
|
5
|
+
{% load render_table from django_tables2 %}
|
|
6
|
+
{% load i18n %}
|
|
7
|
+
|
|
8
|
+
{% block content %}
|
|
9
|
+
<div class="row mb-3">
|
|
10
|
+
<div class="col col-md-6">
|
|
11
|
+
<div class="card">
|
|
12
|
+
<h5 class="card-header">{% trans "Filter Expression Information" %}</h5>
|
|
13
|
+
<div class="card-body">
|
|
14
|
+
<table class="table table-hover attr-table">
|
|
15
|
+
<tr>
|
|
16
|
+
<th scope="row">{% trans "Name" %}</th>
|
|
17
|
+
<td>{{ object.name }}</td>
|
|
18
|
+
</tr>
|
|
19
|
+
<tr>
|
|
20
|
+
<th scope="row">{% trans "Description" %}</th>
|
|
21
|
+
<td>{{ object.description | placeholder }}</td>
|
|
22
|
+
</tr>
|
|
23
|
+
<tr>
|
|
24
|
+
<th scope="row">{% trans "Expression" %}</th>
|
|
25
|
+
<td>{{ object.expression | placeholder }}</td>
|
|
26
|
+
</tr>
|
|
27
|
+
</table>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
{% plugin_left_page object %}
|
|
31
|
+
</div>
|
|
32
|
+
<div class="col col-12 col-md-6">
|
|
33
|
+
{% include 'inc/panels/related_objects.html' %}
|
|
34
|
+
{% include 'inc/panels/comments.html' %}
|
|
35
|
+
{% include 'inc/panels/custom_fields.html' %}
|
|
36
|
+
{% plugin_right_page object %}
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
{% endblock %}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
{% extends 'generic/object_edit.html' %}
|
|
2
|
+
{% load static %}
|
|
3
|
+
{% load form_helpers %}
|
|
4
|
+
{% load i18n %}
|
|
5
|
+
|
|
6
|
+
{% block form %}
|
|
7
|
+
{# Render hidden fields #}
|
|
8
|
+
{% for field in form.hidden_fields %}
|
|
9
|
+
{{ field }}
|
|
10
|
+
{% endfor %}
|
|
11
|
+
|
|
12
|
+
{# Render regular fieldsets #}
|
|
13
|
+
{% for fieldset in form.fieldsets %}
|
|
14
|
+
{% if fieldset.name != "Test Expression" %}
|
|
15
|
+
{% render_fieldset form fieldset %}
|
|
16
|
+
{% else %}
|
|
17
|
+
{# Custom rendering for Test Expression fieldset with button #}
|
|
18
|
+
<div class="bg-info-subtle border border-info rounded-1 pt-3 px-3 mb-3">
|
|
19
|
+
<div class="field-group mb-3">
|
|
20
|
+
<div class="row">
|
|
21
|
+
<h2 class="col-9 offset-3">
|
|
22
|
+
{% trans "Test Expression" %}
|
|
23
|
+
</h2>
|
|
24
|
+
<p class="text-muted small mb-3 offset-3">
|
|
25
|
+
Test this filter expression against a live IP Fabric source to verify it returns the expected results.
|
|
26
|
+
</p>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
{# Render the fields in this fieldset (test_source and test_endpoint) #}
|
|
32
|
+
{% render_field form.test_source %}
|
|
33
|
+
{% render_field form.test_endpoint %}
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
{# Test button section #}
|
|
37
|
+
<div class="mb-3">
|
|
38
|
+
<div class="row">
|
|
39
|
+
<div class="col-9 offset-3">
|
|
40
|
+
<button type="button"
|
|
41
|
+
id="test-expression-btn"
|
|
42
|
+
class="btn btn-primary"
|
|
43
|
+
onclick="testExpression('{{ object.pk|default:"" }}', '{{ csrf_token }}')">
|
|
44
|
+
{% trans "Test Expression" %}
|
|
45
|
+
</button>
|
|
46
|
+
|
|
47
|
+
<div id="test-result" class="mt-3"></div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
{% endif %}
|
|
53
|
+
{% endfor %}
|
|
54
|
+
|
|
55
|
+
{# Custom fields #}
|
|
56
|
+
{% if form.custom_fields %}
|
|
57
|
+
<div class="field-group mb-5">
|
|
58
|
+
<div class="row">
|
|
59
|
+
<h2 class="col-9 offset-3">{% trans "Custom Fields" %}</h2>
|
|
60
|
+
</div>
|
|
61
|
+
{% render_custom_fields form %}
|
|
62
|
+
</div>
|
|
63
|
+
{% endif %}
|
|
64
|
+
|
|
65
|
+
{# Comments #}
|
|
66
|
+
{% if form.comments %}
|
|
67
|
+
<div class="field-group mb-5">
|
|
68
|
+
{% render_field form.comments %}
|
|
69
|
+
</div>
|
|
70
|
+
{% endif %}
|
|
71
|
+
|
|
72
|
+
{# Changelog message #}
|
|
73
|
+
{% if form.changelog_message %}
|
|
74
|
+
<div class="bg-primary-subtle border border-primary rounded-1 pt-3 px-3 mb-3">
|
|
75
|
+
{% render_field form.changelog_message %}
|
|
76
|
+
</div>
|
|
77
|
+
{% endif %}
|
|
78
|
+
|
|
79
|
+
{# Load JavaScript for AJAX testing - inlined to ensure it's always available #}
|
|
80
|
+
<script>
|
|
81
|
+
/**
|
|
82
|
+
* Test filter expression against IP Fabric API
|
|
83
|
+
*/
|
|
84
|
+
function testExpression(pk, csrfToken) {
|
|
85
|
+
const btn = document.getElementById('test-expression-btn');
|
|
86
|
+
const result = document.getElementById('test-result');
|
|
87
|
+
const testSource = document.getElementById('id_test_source')?.value;
|
|
88
|
+
const testEndpoint = document.getElementById('id_test_endpoint')?.value;
|
|
89
|
+
const expression = document.getElementById('id_expression')?.value;
|
|
90
|
+
|
|
91
|
+
// Clear any previous result message
|
|
92
|
+
result.innerHTML = '';
|
|
93
|
+
|
|
94
|
+
// Validate required fields
|
|
95
|
+
if (!testSource) {
|
|
96
|
+
result.innerHTML = '<div class="alert alert-warning"><strong>Required:</strong> Please select a Test Source.</div>';
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!testEndpoint) {
|
|
101
|
+
result.innerHTML = '<div class="alert alert-warning"><strong>Required:</strong> Please select a Test Endpoint.</div>';
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!expression) {
|
|
106
|
+
result.innerHTML = '<div class="alert alert-warning"><strong>Required:</strong> Please enter an expression to test.</div>';
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Show loading
|
|
111
|
+
btn.disabled = true;
|
|
112
|
+
btn.innerHTML = '<i class="mdi mdi-loading mdi-spin"></i> Testing...';
|
|
113
|
+
|
|
114
|
+
// Use the same endpoint logic for both saved and unsaved expressions
|
|
115
|
+
const url = pk
|
|
116
|
+
? `/plugins/ipfabric/filter-expression/${pk}/test/`
|
|
117
|
+
: `/plugins/ipfabric/filter-expression/test/`;
|
|
118
|
+
|
|
119
|
+
// Make AJAX request
|
|
120
|
+
fetch(url, {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
headers: {
|
|
123
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
124
|
+
'X-CSRFToken': csrfToken
|
|
125
|
+
},
|
|
126
|
+
body: new URLSearchParams({
|
|
127
|
+
test_source: testSource,
|
|
128
|
+
test_endpoint: testEndpoint,
|
|
129
|
+
expression: expression
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
.then(response => response.json())
|
|
133
|
+
.then(data => {
|
|
134
|
+
btn.disabled = false;
|
|
135
|
+
btn.innerHTML = '<i class="mdi mdi-test-tube"></i> {% trans "Test Expression" %}';
|
|
136
|
+
|
|
137
|
+
if (data.success) {
|
|
138
|
+
result.innerHTML = `<div class="alert alert-success"><strong>Success!</strong> ${data.message}</div>`;
|
|
139
|
+
} else {
|
|
140
|
+
result.innerHTML = `<div class="alert alert-danger"><strong>Error:</strong> ${data.error}</div>`;
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
.catch(error => {
|
|
144
|
+
btn.disabled = false;
|
|
145
|
+
btn.innerHTML = '<i class="mdi mdi-test-tube"></i> {% trans "Test Expression" %}';
|
|
146
|
+
result.innerHTML = `<div class="alert alert-danger"><strong>Error:</strong> ${error.message}</div>`;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
</script>
|
|
150
|
+
{% endblock form %}
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
<h5 class="card-header">{% trans "Parameters" %}</h5>
|
|
82
82
|
<div class="card-body">
|
|
83
83
|
<table class="table table-hover attr-table">
|
|
84
|
-
{% for name, field in object.parameters
|
|
84
|
+
{% for name, field in object.parameters|sort_parameters_hierarchical:object %}
|
|
85
85
|
<tr>
|
|
86
86
|
<th scope="row">{{ name }}</th>
|
|
87
87
|
<td>
|
|
@@ -33,8 +33,20 @@
|
|
|
33
33
|
<td>{{ object.group | linkify | placeholder}}</td>
|
|
34
34
|
</tr>
|
|
35
35
|
<tr>
|
|
36
|
-
<th scope="row">{% trans "
|
|
37
|
-
<td>
|
|
36
|
+
<th scope="row">{% trans "Parent Transform Maps" %}</th>
|
|
37
|
+
<td>
|
|
38
|
+
{% if object.parents.exists %}
|
|
39
|
+
{% for parent in object.parents.all %}
|
|
40
|
+
<a href="{{ parent.get_absolute_url }}">{{ parent.name }}</a>{% if not forloop.last %}, {% endif %}
|
|
41
|
+
{% endfor %}
|
|
42
|
+
{% else %}
|
|
43
|
+
<span class="text-muted">{{ "" | placeholder }}</span>
|
|
44
|
+
{% endif %}
|
|
45
|
+
</td>
|
|
46
|
+
</tr>
|
|
47
|
+
<tr>
|
|
48
|
+
<th scope="row">{% trans "Source Endpoint" %}</th>
|
|
49
|
+
<td>{{ object.source_endpoint | linkify }}</td>
|
|
38
50
|
</tr>
|
|
39
51
|
<tr>
|
|
40
52
|
<th scope="row">{% trans "Target Model" %}</th>
|
|
@@ -46,6 +58,8 @@
|
|
|
46
58
|
{% plugin_left_page object %}
|
|
47
59
|
</div>
|
|
48
60
|
<div class="col col-md-6">
|
|
61
|
+
{% include 'inc/panels/related_objects.html' %}
|
|
62
|
+
{% include 'inc/panels/comments.html' %}
|
|
49
63
|
{% include 'inc/panels/custom_fields.html' %}
|
|
50
64
|
{% plugin_right_page object %}
|
|
51
65
|
</div>
|
|
@@ -1,8 +1,17 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from collections import OrderedDict
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
1
5
|
from django import template
|
|
2
6
|
from django.apps import apps
|
|
3
7
|
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from ipfabric_netbox.models import IPFabricSync
|
|
10
|
+
|
|
4
11
|
register = template.Library()
|
|
5
12
|
|
|
13
|
+
logger = logging.getLogger("ipfabric_netbox.helpers")
|
|
14
|
+
|
|
6
15
|
|
|
7
16
|
@register.filter()
|
|
8
17
|
def resolve_object(pk, model_path):
|
|
@@ -15,3 +24,62 @@ def resolve_object(pk, model_path):
|
|
|
15
24
|
return model.objects.get(pk=pk)
|
|
16
25
|
except Exception:
|
|
17
26
|
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@register.filter()
|
|
30
|
+
def sort_parameters_hierarchical(
|
|
31
|
+
parameters_dict: dict, sync_obj: "IPFabricSync | None" = None
|
|
32
|
+
) -> list:
|
|
33
|
+
"""
|
|
34
|
+
Sort parameters hierarchically for IPFabricSync objects:
|
|
35
|
+
1. 'groups' first
|
|
36
|
+
2. Models in hierarchical order from IPFabricSync.get_model_hierarchy()
|
|
37
|
+
|
|
38
|
+
For other objects, returns items unsorted.
|
|
39
|
+
|
|
40
|
+
Usage: {{ object.parameters|sort_parameters_hierarchical:object }}
|
|
41
|
+
"""
|
|
42
|
+
if not parameters_dict or not isinstance(parameters_dict, dict):
|
|
43
|
+
return []
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
group_ids = parameters_dict.get("groups", [])
|
|
47
|
+
|
|
48
|
+
# Get the hierarchical model order from IPFabricSync (returns transform maps)
|
|
49
|
+
transform_maps = sync_obj.__class__.get_model_hierarchy(group_ids)
|
|
50
|
+
# Convert transform maps to app_label.model format
|
|
51
|
+
hierarchy_order = [
|
|
52
|
+
f"{tm.target_model.app_label}.{tm.target_model.model}"
|
|
53
|
+
for tm in transform_maps
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
# Group models by app label while maintaining hierarchical order
|
|
57
|
+
app_models = OrderedDict()
|
|
58
|
+
for model_name in hierarchy_order:
|
|
59
|
+
if "." in model_name:
|
|
60
|
+
app_label = model_name.split(".")[0]
|
|
61
|
+
if app_label not in app_models:
|
|
62
|
+
app_models[app_label] = []
|
|
63
|
+
app_models[app_label].append(model_name)
|
|
64
|
+
|
|
65
|
+
# Build sorted list: groups first, then all app models
|
|
66
|
+
sorted_keys = []
|
|
67
|
+
for app_label, models in app_models.items():
|
|
68
|
+
sorted_keys.extend(models)
|
|
69
|
+
|
|
70
|
+
result = []
|
|
71
|
+
# Start with keys that weren't in the hierarchy
|
|
72
|
+
for key, value in parameters_dict.items():
|
|
73
|
+
if key not in sorted_keys:
|
|
74
|
+
result.append((key, value))
|
|
75
|
+
|
|
76
|
+
# Return items in the sorted order
|
|
77
|
+
for key in sorted_keys:
|
|
78
|
+
if key in parameters_dict:
|
|
79
|
+
result.append((key, parameters_dict[key]))
|
|
80
|
+
|
|
81
|
+
return result
|
|
82
|
+
except Exception:
|
|
83
|
+
# Fallback to simple alphabetical sort if something goes wrong
|
|
84
|
+
logger.warning("Failed to sort parameters hierarchically", exc_info=True)
|
|
85
|
+
return sorted(parameters_dict.items())
|