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
@@ -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.items %}
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 "Source Model" %}</th>
37
- <td>ipfabric | {{ object.source_model }}</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,59 @@ 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
49
+ model_hierarchy = sync_obj.__class__.get_model_hierarchy(group_ids)
50
+ # Convert ContentType objects to app_label.model format
51
+ hierarchy_order = [f"{ct.app_label}.{ct.model}" for ct in model_hierarchy]
52
+
53
+ # Group models by app label while maintaining hierarchical order
54
+ app_models = OrderedDict()
55
+ for model_name in hierarchy_order:
56
+ if "." in model_name:
57
+ app_label = model_name.split(".")[0]
58
+ if app_label not in app_models:
59
+ app_models[app_label] = []
60
+ app_models[app_label].append(model_name)
61
+
62
+ # Build sorted list: groups first, then all app models
63
+ sorted_keys = []
64
+ for app_label, models in app_models.items():
65
+ sorted_keys.extend(models)
66
+
67
+ result = []
68
+ # Start with keys that weren't in the hierarchy
69
+ for key, value in parameters_dict.items():
70
+ if key not in sorted_keys:
71
+ result.append((key, value))
72
+
73
+ # Return items in the sorted order
74
+ for key in sorted_keys:
75
+ if key in parameters_dict:
76
+ result.append((key, parameters_dict[key]))
77
+
78
+ return result
79
+ except Exception:
80
+ # Fallback to simple alphabetical sort if something goes wrong
81
+ logger.warning("Failed to sort parameters hierarchically", exc_info=True)
82
+ return sorted(parameters_dict.items())