nautobot 2.4.3__py3-none-any.whl → 2.4.4__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.
- nautobot/apps/filters.py +2 -0
- nautobot/circuits/filters.py +1 -1
- nautobot/circuits/tests/test_models.py +5 -3
- nautobot/cloud/filters.py +3 -6
- nautobot/cloud/tests/test_filters.py +21 -0
- nautobot/core/admin.py +2 -0
- nautobot/core/jobs/__init__.py +2 -1
- nautobot/core/management/commands/generate_performance_test_endpoints.py +9 -6
- nautobot/core/models/utils.py +6 -1
- nautobot/core/templates/inc/javascript.html +1 -0
- nautobot/core/templatetags/ui_framework.py +20 -4
- nautobot/core/testing/forms.py +1 -1
- nautobot/core/tests/test_api.py +1 -1
- nautobot/core/tests/test_graphql.py +3 -3
- nautobot/core/tests/test_jobs.py +4 -1
- nautobot/core/ui/object_detail.py +1 -1
- nautobot/dcim/api/serializers.py +36 -0
- nautobot/dcim/api/views.py +1 -1
- nautobot/dcim/elevations.py +17 -4
- nautobot/dcim/factory.py +9 -1
- nautobot/dcim/filters/__init__.py +27 -1
- nautobot/dcim/forms.py +13 -1
- nautobot/dcim/models/devices.py +11 -5
- nautobot/dcim/signals.py +26 -0
- nautobot/dcim/templates/dcim/virtualdevicecontext_retrieve.html +0 -62
- nautobot/dcim/templates/dcim/virtualdevicecontext_update.html +6 -0
- nautobot/dcim/tests/test_api.py +176 -0
- nautobot/dcim/tests/test_filters.py +56 -3
- nautobot/dcim/tests/test_models.py +40 -0
- nautobot/dcim/views.py +24 -14
- nautobot/extras/api/mixins.py +1 -1
- nautobot/extras/api/views.py +2 -2
- nautobot/extras/filters/__init__.py +4 -0
- nautobot/extras/models/datasources.py +7 -3
- nautobot/extras/plugins/__init__.py +26 -1
- nautobot/extras/templates/extras/inc/jobresult.html +12 -13
- nautobot/extras/templates/extras/objectchange.html +28 -12
- nautobot/extras/tests/test_api.py +16 -15
- nautobot/extras/tests/test_filters.py +2 -0
- nautobot/extras/tests/test_plugins.py +32 -1
- nautobot/extras/tests/test_views.py +12 -2
- nautobot/extras/views.py +3 -0
- nautobot/ipam/api/serializers.py +7 -8
- nautobot/ipam/api/views.py +2 -2
- nautobot/ipam/factory.py +27 -8
- nautobot/ipam/filters.py +67 -29
- nautobot/ipam/formfields.py +51 -0
- nautobot/ipam/forms.py +13 -1
- nautobot/ipam/migrations/0051_added_optional_vrf_relationship_to_vdc.py +41 -0
- nautobot/ipam/models.py +63 -5
- nautobot/ipam/tables.py +21 -7
- nautobot/ipam/tests/test_api.py +107 -66
- nautobot/ipam/tests/test_filters.py +145 -5
- nautobot/ipam/tests/test_views.py +15 -2
- nautobot/project-static/css/base.css +11 -0
- nautobot/project-static/css/dark.css +2 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +62 -0
- nautobot/project-static/docs/development/apps/api/configuration-view.html +0 -3
- nautobot/project-static/docs/development/apps/api/models/graphql.html +0 -4
- nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +94 -1
- nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +0 -3
- nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +0 -3
- nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +0 -3
- nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +0 -3
- nautobot/project-static/docs/development/apps/api/prometheus.html +0 -3
- nautobot/project-static/docs/development/apps/api/testing.html +0 -6
- nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +0 -3
- nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +0 -3
- nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +0 -3
- nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +0 -3
- nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +1 -7
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +0 -7
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +0 -4
- nautobot/project-static/docs/development/apps/api/views/notes.html +0 -3
- nautobot/project-static/docs/development/apps/index.html +2 -35
- nautobot/project-static/docs/development/apps/migration/code-updates.html +1 -1
- nautobot/project-static/docs/development/core/application-registry.html +0 -6
- nautobot/project-static/docs/development/core/best-practices.html +0 -27
- nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +58 -4
- nautobot/project-static/docs/development/core/getting-started.html +12 -16
- nautobot/project-static/docs/development/core/homepage.html +0 -3
- nautobot/project-static/docs/development/core/style-guide.html +0 -5
- nautobot/project-static/docs/development/core/templates.html +0 -3
- nautobot/project-static/docs/development/core/testing.html +0 -9
- nautobot/project-static/docs/development/jobs/index.html +3 -29
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/overview/application_stack.html +0 -18
- nautobot/project-static/docs/release-notes/version-2.4.html +191 -0
- nautobot/project-static/docs/requirements.txt +1 -1
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +290 -290
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guide/administration/configuration/settings.html +0 -10
- nautobot/project-static/docs/user-guide/administration/guides/docker.html +0 -15
- nautobot/project-static/docs/user-guide/administration/installation/index.html +0 -16
- nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +1 -4
- nautobot/project-static/docs/user-guide/administration/installation/services.html +0 -11
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +3 -3
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +5 -35
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/tables/v2-code-location-changes.yaml +1 -1
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +1 -1
- nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +0 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +0 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +1 -17
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +0 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +0 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +1 -7
- nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +0 -6
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +0 -3
- nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +0 -8
- nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/graphql.html +0 -6
- nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +0 -3
- nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +3 -15
- nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +0 -26
- nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +0 -8
- nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +0 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +0 -8
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +0 -7
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +0 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +0 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +0 -14
- nautobot/project-static/docs/user-guide/platform-functionality/note.html +0 -3
- nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +1 -10
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +0 -3
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +0 -14
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +0 -19
- nautobot/project-static/docs/user-guide/platform-functionality/secret.html +3 -9
- nautobot/project-static/docs/user-guide/platform-functionality/status.html +0 -8
- nautobot/project-static/docs/user-guide/platform-functionality/tag.html +0 -4
- nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +1 -13
- nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +0 -5
- nautobot/project-static/js/editor.js +292 -0
- nautobot/project-static/monaco-editor-0.52.2/README.md +81 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/base/worker/workerMain.js +31 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/basic-languages/xml/xml.js +10 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/basic-languages/yaml/yaml.js +10 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/editor/editor.main.css +8 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/editor/editor.main.js +798 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/language/json/jsonMode.js +19 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/language/json/jsonWorker.js +42 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/loader.js +11 -0
- nautobot/tenancy/filters/__init__.py +3 -5
- nautobot/tenancy/tests/test_filters.py +10 -0
- nautobot/virtualization/views.py +0 -1
- nautobot/wireless/tables.py +9 -4
- nautobot/wireless/tests/test_api.py +0 -9
- {nautobot-2.4.3.dist-info → nautobot-2.4.4.dist-info}/METADATA +2 -2
- {nautobot-2.4.3.dist-info → nautobot-2.4.4.dist-info}/RECORD +175 -163
- {nautobot-2.4.3.dist-info → nautobot-2.4.4.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.4.3.dist-info → nautobot-2.4.4.dist-info}/NOTICE +0 -0
- {nautobot-2.4.3.dist-info → nautobot-2.4.4.dist-info}/WHEEL +0 -0
- {nautobot-2.4.3.dist-info → nautobot-2.4.4.dist-info}/entry_points.txt +0 -0
|
@@ -4,65 +4,3 @@
|
|
|
4
4
|
{% block extra_breadcrumbs %}
|
|
5
5
|
<li><a href="{% url 'dcim:device' pk=object.device.pk %}">{{ object.device }}</a></li>
|
|
6
6
|
{% endblock extra_breadcrumbs %}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
{% block content_left_page %}
|
|
10
|
-
<div class="panel panel-default">
|
|
11
|
-
<div class="panel-heading">
|
|
12
|
-
<strong>Virtual Device Context</strong>
|
|
13
|
-
</div>
|
|
14
|
-
<table class="table table-hover panel-body attr-table">
|
|
15
|
-
<tr>
|
|
16
|
-
<td>Name</td>
|
|
17
|
-
<td>
|
|
18
|
-
{{ object.name }}
|
|
19
|
-
</td>
|
|
20
|
-
</tr>
|
|
21
|
-
<tr>
|
|
22
|
-
<td>Identifier</td>
|
|
23
|
-
<td>
|
|
24
|
-
{{ object.identifier }}
|
|
25
|
-
</td>
|
|
26
|
-
</tr>
|
|
27
|
-
<tr>
|
|
28
|
-
<td>Role</td>
|
|
29
|
-
<td>
|
|
30
|
-
{{ object.role| hyperlinked_object_with_color }}
|
|
31
|
-
</td>
|
|
32
|
-
</tr>
|
|
33
|
-
<tr>
|
|
34
|
-
<td>Status</td>
|
|
35
|
-
<td>
|
|
36
|
-
{{ object.status| hyperlinked_object_with_color }}
|
|
37
|
-
</td>
|
|
38
|
-
</tr>
|
|
39
|
-
<tr>
|
|
40
|
-
<td>Device</td>
|
|
41
|
-
<td>
|
|
42
|
-
{{ object.device|hyperlinked_object }}
|
|
43
|
-
</td>
|
|
44
|
-
</tr>
|
|
45
|
-
<tr>
|
|
46
|
-
<td>Primary IPv4</td>
|
|
47
|
-
<td>
|
|
48
|
-
{{ object.primary_ip4|hyperlinked_object }}
|
|
49
|
-
</td>
|
|
50
|
-
</tr>
|
|
51
|
-
<tr>
|
|
52
|
-
<td>Primary IPv6</td>
|
|
53
|
-
<td>
|
|
54
|
-
{{ object.primary_ip6|hyperlinked_object }}
|
|
55
|
-
</td>
|
|
56
|
-
</tr>
|
|
57
|
-
{% include 'inc/tenant_table_row.html' %}
|
|
58
|
-
<tr>
|
|
59
|
-
<td>Description</td>
|
|
60
|
-
<td>{{ object.description|placeholder }}</td>
|
|
61
|
-
</tr>
|
|
62
|
-
</table>
|
|
63
|
-
</div>
|
|
64
|
-
{% endblock content_left_page %}
|
|
65
|
-
|
|
66
|
-
{% block content_full_width_page %}
|
|
67
|
-
{% include 'panel_table.html' with table=interfaces_table heading="Interfaces" %}
|
|
68
|
-
{% endblock content_full_width_page %}
|
|
@@ -23,6 +23,12 @@
|
|
|
23
23
|
{% endif %}
|
|
24
24
|
</div>
|
|
25
25
|
</div>
|
|
26
|
+
<div class="panel panel-default">
|
|
27
|
+
<div class="panel-heading"><strong>VRF Assignments</strong></div>
|
|
28
|
+
<div class="panel-body">
|
|
29
|
+
{% render_field form.vrfs %}
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
26
32
|
{% include 'inc/tenancy_form_panel.html' %}
|
|
27
33
|
{% include 'inc/extras_features_edit_form_fields.html' %}
|
|
28
34
|
{% endblock %}
|
nautobot/dcim/tests/test_api.py
CHANGED
|
@@ -1716,6 +1716,182 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
|
|
|
1716
1716
|
)
|
|
1717
1717
|
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
1718
1718
|
|
|
1719
|
+
def _parent_device_test_data(self):
|
|
1720
|
+
location = Location.objects.filter(location_type=LocationType.objects.get(name="Campus")).first()
|
|
1721
|
+
device_status = Status.objects.get_for_model(Device).first()
|
|
1722
|
+
device_role = Role.objects.get_for_model(Device).first()
|
|
1723
|
+
device_type = DeviceType.objects.first()
|
|
1724
|
+
|
|
1725
|
+
device_type_parent = DeviceType.objects.create(
|
|
1726
|
+
manufacturer=device_type.manufacturer,
|
|
1727
|
+
model=f"{device_type.model} Parent",
|
|
1728
|
+
u_height=device_type.u_height,
|
|
1729
|
+
subdevice_role=SubdeviceRoleChoices.ROLE_PARENT,
|
|
1730
|
+
)
|
|
1731
|
+
device_type_child = DeviceType.objects.create(
|
|
1732
|
+
manufacturer=device_type.manufacturer,
|
|
1733
|
+
model=f"{device_type.model} Child",
|
|
1734
|
+
u_height=device_type.u_height,
|
|
1735
|
+
subdevice_role=SubdeviceRoleChoices.ROLE_CHILD,
|
|
1736
|
+
)
|
|
1737
|
+
|
|
1738
|
+
parent_device = Device.objects.create(
|
|
1739
|
+
device_type=device_type_parent,
|
|
1740
|
+
role=device_role,
|
|
1741
|
+
status=device_status,
|
|
1742
|
+
name="Device Parent",
|
|
1743
|
+
location=location,
|
|
1744
|
+
)
|
|
1745
|
+
device_bay_1 = DeviceBay.objects.create(name="db1", device_id=parent_device.pk)
|
|
1746
|
+
device_bay_2 = DeviceBay.objects.create(name="db2", device_id=parent_device.pk)
|
|
1747
|
+
|
|
1748
|
+
return parent_device, device_bay_1, device_bay_2, device_type_child
|
|
1749
|
+
|
|
1750
|
+
def test_creating_device_with_parent_bay(self):
|
|
1751
|
+
# Create test data
|
|
1752
|
+
parent_device, device_bay_1, device_bay_2, device_type_child = self._parent_device_test_data()
|
|
1753
|
+
|
|
1754
|
+
self.add_permissions("dcim.add_device")
|
|
1755
|
+
url = reverse("dcim-api:device-list")
|
|
1756
|
+
|
|
1757
|
+
# Test creating device with parent bay by device bay data
|
|
1758
|
+
data = {
|
|
1759
|
+
"device_type": device_type_child.pk,
|
|
1760
|
+
"role": parent_device.role.pk,
|
|
1761
|
+
"location": parent_device.location.pk,
|
|
1762
|
+
"name": "Device parent bay test #1",
|
|
1763
|
+
"status": parent_device.status.pk,
|
|
1764
|
+
"parent_bay": {"device": {"name": parent_device.name}, "name": device_bay_1.name},
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
response = self.client.post(url, data, format="json", **self.header)
|
|
1768
|
+
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
|
1769
|
+
|
|
1770
|
+
created_device = Device.objects.get(name="Device parent bay test #1")
|
|
1771
|
+
self.assertEqual(created_device.parent_bay.pk, device_bay_1.pk)
|
|
1772
|
+
|
|
1773
|
+
# Test creating device with parent bay by device_bay.pk
|
|
1774
|
+
data = {
|
|
1775
|
+
"device_type": device_type_child.pk,
|
|
1776
|
+
"role": parent_device.role.pk,
|
|
1777
|
+
"location": parent_device.location.pk,
|
|
1778
|
+
"name": "Device parent bay test #2",
|
|
1779
|
+
"status": parent_device.status.pk,
|
|
1780
|
+
"parent_bay": device_bay_2.pk,
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
response = self.client.post(url, data, format="json", **self.header)
|
|
1784
|
+
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
|
1785
|
+
|
|
1786
|
+
created_device = Device.objects.get(name="Device parent bay test #2")
|
|
1787
|
+
self.assertEqual(created_device.parent_bay.pk, device_bay_2.pk)
|
|
1788
|
+
|
|
1789
|
+
# Test creating device with parent bay already taken
|
|
1790
|
+
data = {
|
|
1791
|
+
"device_type": device_type_child.pk,
|
|
1792
|
+
"role": parent_device.role.pk,
|
|
1793
|
+
"location": parent_device.location.pk,
|
|
1794
|
+
"name": "Device parent bay test #3",
|
|
1795
|
+
"status": parent_device.status.pk,
|
|
1796
|
+
"parent_bay": device_bay_1.pk,
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
response = self.client.post(url, data, format="json", **self.header)
|
|
1800
|
+
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
1801
|
+
self.assertIn("Cannot install device; parent bay is already taken", response.content.decode(response.charset))
|
|
1802
|
+
|
|
1803
|
+
# Assert that on the #1 device, parent bay stayed the same
|
|
1804
|
+
old_device = Device.objects.get(name="Device parent bay test #1")
|
|
1805
|
+
self.assertEqual(old_device.parent_bay.pk, device_bay_1.pk)
|
|
1806
|
+
|
|
1807
|
+
def test_update_device_with_parent_bay(self):
|
|
1808
|
+
# Create test data
|
|
1809
|
+
parent_device, device_bay_1, device_bay_2, device_type_child = self._parent_device_test_data()
|
|
1810
|
+
|
|
1811
|
+
self.add_permissions("dcim.change_device")
|
|
1812
|
+
|
|
1813
|
+
child_device = Device.objects.create(
|
|
1814
|
+
device_type=device_type_child,
|
|
1815
|
+
role=parent_device.role,
|
|
1816
|
+
location=parent_device.location,
|
|
1817
|
+
name="Device parent bay test #4",
|
|
1818
|
+
status=parent_device.status,
|
|
1819
|
+
)
|
|
1820
|
+
# Test setting parent bay during the update
|
|
1821
|
+
patch_data = {"parent_bay": device_bay_1.pk}
|
|
1822
|
+
response = self.client.patch(self._get_detail_url(child_device), patch_data, format="json", **self.header)
|
|
1823
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
1824
|
+
|
|
1825
|
+
updated_device = Device.objects.get(name="Device parent bay test #4")
|
|
1826
|
+
self.assertEqual(updated_device.parent_bay.pk, device_bay_1.pk)
|
|
1827
|
+
|
|
1828
|
+
# Changing the parent bay is not allowed without removing it first
|
|
1829
|
+
patch_data = {"parent_bay": device_bay_2.pk}
|
|
1830
|
+
response = self.client.patch(self._get_detail_url(child_device), patch_data, format="json", **self.header)
|
|
1831
|
+
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
1832
|
+
self.assertIn(
|
|
1833
|
+
f"Cannot install the specified device; device is already installed in {device_bay_1.name}",
|
|
1834
|
+
response.content.decode(response.charset),
|
|
1835
|
+
)
|
|
1836
|
+
|
|
1837
|
+
# Assert that parent bay stayed the same
|
|
1838
|
+
updated_device = Device.objects.get(name="Device parent bay test #4")
|
|
1839
|
+
self.assertEqual(updated_device.parent_bay.pk, device_bay_1.pk)
|
|
1840
|
+
|
|
1841
|
+
def test_reassign_device_to_parent_bay(self):
|
|
1842
|
+
# Create test data
|
|
1843
|
+
parent_device, device_bay_1, device_bay_2, device_type_child = self._parent_device_test_data()
|
|
1844
|
+
device_name = "Device parent bay test #5"
|
|
1845
|
+
child_device = Device.objects.create(
|
|
1846
|
+
device_type=device_type_child,
|
|
1847
|
+
role=parent_device.role,
|
|
1848
|
+
location=parent_device.location,
|
|
1849
|
+
name=device_name,
|
|
1850
|
+
status=parent_device.status,
|
|
1851
|
+
)
|
|
1852
|
+
device_bay_1.installed_device = child_device
|
|
1853
|
+
device_bay_1.save()
|
|
1854
|
+
|
|
1855
|
+
self.add_permissions("dcim.change_device", "dcim.view_device", "dcim.change_devicebay")
|
|
1856
|
+
child_device_detail_url = self._get_detail_url(child_device)
|
|
1857
|
+
|
|
1858
|
+
response = self.client.get(child_device_detail_url, **self.header)
|
|
1859
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
1860
|
+
self.assertEqual(response.json()["parent_bay"]["id"], str(device_bay_1.pk))
|
|
1861
|
+
|
|
1862
|
+
# Test unassigning parent bay
|
|
1863
|
+
patch_data = {"parent_bay": None}
|
|
1864
|
+
response = self.client.patch(child_device_detail_url, patch_data, format="json", **self.header)
|
|
1865
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
1866
|
+
|
|
1867
|
+
child_device.refresh_from_db()
|
|
1868
|
+
with self.assertRaises(DeviceBay.DoesNotExist):
|
|
1869
|
+
child_device.parent_bay
|
|
1870
|
+
|
|
1871
|
+
# And assign it again
|
|
1872
|
+
patch_data = {"parent_bay": device_bay_2.pk}
|
|
1873
|
+
response = self.client.patch(child_device_detail_url, patch_data, format="json", **self.header)
|
|
1874
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
1875
|
+
|
|
1876
|
+
child_device.refresh_from_db()
|
|
1877
|
+
self.assertEqual(child_device.parent_bay.pk, device_bay_2.pk)
|
|
1878
|
+
|
|
1879
|
+
# Unassign it through device bay
|
|
1880
|
+
patch_data = {"installed_device": None}
|
|
1881
|
+
response = self.client.patch(self._get_detail_url(device_bay_2), patch_data, format="json", **self.header)
|
|
1882
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
1883
|
+
|
|
1884
|
+
child_device.refresh_from_db()
|
|
1885
|
+
self.assertFalse(hasattr(child_device, "parent_bay"))
|
|
1886
|
+
|
|
1887
|
+
# And assign through device bay
|
|
1888
|
+
patch_data = {"installed_device": child_device.pk}
|
|
1889
|
+
response = self.client.patch(self._get_detail_url(device_bay_1), patch_data, format="json", **self.header)
|
|
1890
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
1891
|
+
|
|
1892
|
+
child_device.refresh_from_db()
|
|
1893
|
+
self.assertEqual(child_device.parent_bay.pk, device_bay_1.pk)
|
|
1894
|
+
|
|
1719
1895
|
|
|
1720
1896
|
class ModuleTestCase(APIViewTestCases.APIViewTestCase):
|
|
1721
1897
|
model = Module
|
|
@@ -157,6 +157,8 @@ def common_test_data(cls):
|
|
|
157
157
|
cls.loc0 = loc0
|
|
158
158
|
cls.loc1 = loc1
|
|
159
159
|
cls.nested_loc = nested_loc
|
|
160
|
+
cls.loc2 = loc2
|
|
161
|
+
cls.loc3 = loc3
|
|
160
162
|
|
|
161
163
|
provider = Provider.objects.first()
|
|
162
164
|
circuit_type = CircuitType.objects.first()
|
|
@@ -1150,6 +1152,24 @@ class RackGroupTestCase(FilterTestCases.FilterTestCase):
|
|
|
1150
1152
|
name="Rack Group 4",
|
|
1151
1153
|
location=cls.loc1,
|
|
1152
1154
|
)
|
|
1155
|
+
RackGroup.objects.create(
|
|
1156
|
+
name="Rack Group 5",
|
|
1157
|
+
location=cls.loc2,
|
|
1158
|
+
description="C",
|
|
1159
|
+
)
|
|
1160
|
+
RackGroup.objects.create(
|
|
1161
|
+
name="Rack Group 6",
|
|
1162
|
+
location=cls.loc2,
|
|
1163
|
+
)
|
|
1164
|
+
RackGroup.objects.create(
|
|
1165
|
+
name="Rack Group 7",
|
|
1166
|
+
location=cls.loc3,
|
|
1167
|
+
description="C",
|
|
1168
|
+
)
|
|
1169
|
+
RackGroup.objects.create(
|
|
1170
|
+
name="Rack Group 8",
|
|
1171
|
+
location=cls.loc3,
|
|
1172
|
+
)
|
|
1153
1173
|
|
|
1154
1174
|
def test_children(self):
|
|
1155
1175
|
child_groups = RackGroup.objects.filter(name__startswith="Child").filter(parent__isnull=False)[:2]
|
|
@@ -1161,6 +1181,24 @@ class RackGroupTestCase(FilterTestCases.FilterTestCase):
|
|
|
1161
1181
|
params = {"children": [rack_group_4.pk, rack_group_4.pk]}
|
|
1162
1182
|
self.assertFalse(self.filterset(params, self.queryset).qs.exists())
|
|
1163
1183
|
|
|
1184
|
+
def test_ancestors(self):
|
|
1185
|
+
with self.subTest():
|
|
1186
|
+
pk_list = []
|
|
1187
|
+
parent_locations = self.loc3.ancestors(include_self=True)
|
|
1188
|
+
pk_list.extend([v.pk for v in parent_locations])
|
|
1189
|
+
params = Q(location__pk__in=pk_list)
|
|
1190
|
+
expected_queryset = RackGroup.objects.filter(params)
|
|
1191
|
+
params = {"ancestors": [self.loc3.pk]}
|
|
1192
|
+
self.assertQuerysetEqualAndNotEmpty(self.filterset(params, self.queryset).qs, expected_queryset)
|
|
1193
|
+
with self.subTest():
|
|
1194
|
+
pk_list = []
|
|
1195
|
+
parent_locations = self.loc2.ancestors(include_self=True)
|
|
1196
|
+
pk_list.extend([v.pk for v in parent_locations])
|
|
1197
|
+
params = Q(location__pk__in=pk_list)
|
|
1198
|
+
expected_queryset = RackGroup.objects.filter(params)
|
|
1199
|
+
params = {"ancestors": [self.loc2.pk]}
|
|
1200
|
+
self.assertQuerysetEqualAndNotEmpty(self.filterset(params, self.queryset).qs, expected_queryset)
|
|
1201
|
+
|
|
1164
1202
|
|
|
1165
1203
|
class RackTestCase(FilterTestCases.FilterTestCase, FilterTestCases.TenancyFilterTestCaseMixin):
|
|
1166
1204
|
queryset = Rack.objects.all()
|
|
@@ -4188,12 +4226,13 @@ class InterfaceVDCAssignmentTestCase(FilterTestCases.FilterTestCase):
|
|
|
4188
4226
|
|
|
4189
4227
|
@classmethod
|
|
4190
4228
|
def setUpTestData(cls):
|
|
4191
|
-
|
|
4229
|
+
device_1 = Device.objects.first()
|
|
4230
|
+
device_2 = Device.objects.last()
|
|
4192
4231
|
vdc_status = Status.objects.get_for_model(VirtualDeviceContext)[0]
|
|
4193
4232
|
interface_status = Status.objects.get_for_model(Interface)[0]
|
|
4194
4233
|
interfaces = [
|
|
4195
4234
|
Interface.objects.create(
|
|
4196
|
-
device=
|
|
4235
|
+
device=device_1,
|
|
4197
4236
|
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
4198
4237
|
name=f"Interface 00{idx}",
|
|
4199
4238
|
status=interface_status,
|
|
@@ -4202,7 +4241,7 @@ class InterfaceVDCAssignmentTestCase(FilterTestCases.FilterTestCase):
|
|
|
4202
4241
|
]
|
|
4203
4242
|
vdcs = [
|
|
4204
4243
|
VirtualDeviceContext.objects.create(
|
|
4205
|
-
device=
|
|
4244
|
+
device=device_1,
|
|
4206
4245
|
status=vdc_status,
|
|
4207
4246
|
identifier=200 + idx,
|
|
4208
4247
|
name=f"Test VDC {idx}",
|
|
@@ -4213,3 +4252,17 @@ class InterfaceVDCAssignmentTestCase(FilterTestCases.FilterTestCase):
|
|
|
4213
4252
|
InterfaceVDCAssignment.objects.create(virtual_device_context=vdcs[1], interface=interfaces[0])
|
|
4214
4253
|
InterfaceVDCAssignment.objects.create(virtual_device_context=vdcs[1], interface=interfaces[1])
|
|
4215
4254
|
InterfaceVDCAssignment.objects.create(virtual_device_context=vdcs[2], interface=interfaces[2])
|
|
4255
|
+
InterfaceVDCAssignment.objects.create(
|
|
4256
|
+
virtual_device_context=VirtualDeviceContext.objects.create(
|
|
4257
|
+
device=device_2,
|
|
4258
|
+
status=vdc_status,
|
|
4259
|
+
identifier=200,
|
|
4260
|
+
name="Test VDC 0",
|
|
4261
|
+
),
|
|
4262
|
+
interface=Interface.objects.create(
|
|
4263
|
+
device=device_2,
|
|
4264
|
+
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
4265
|
+
name="Interface 000",
|
|
4266
|
+
status=interface_status,
|
|
4267
|
+
),
|
|
4268
|
+
)
|
|
@@ -1166,6 +1166,19 @@ class LocationTypeTestCase(TestCase):
|
|
|
1166
1166
|
child_loc.delete()
|
|
1167
1167
|
child.validated_save()
|
|
1168
1168
|
|
|
1169
|
+
def test_removing_content_type(self):
|
|
1170
|
+
"""Validation check to prevent removing an in-use content type from a LocationType."""
|
|
1171
|
+
|
|
1172
|
+
location_type = LocationType.objects.get(name="Campus")
|
|
1173
|
+
device_ct = ContentType.objects.get_for_model(Device)
|
|
1174
|
+
|
|
1175
|
+
with self.assertRaises(ValidationError) as cm:
|
|
1176
|
+
location_type.content_types.remove(device_ct)
|
|
1177
|
+
self.assertIn(
|
|
1178
|
+
f"Cannot remove the content type {device_ct} as currently at least one device is associated to a location",
|
|
1179
|
+
str(cm.exception),
|
|
1180
|
+
)
|
|
1181
|
+
|
|
1169
1182
|
|
|
1170
1183
|
class LocationTestCase(ModelTestCases.BaseModelTestCase):
|
|
1171
1184
|
model = Location
|
|
@@ -1895,6 +1908,33 @@ class DeviceTestCase(ModelTestCases.BaseModelTestCase):
|
|
|
1895
1908
|
parent_device.rack = rack
|
|
1896
1909
|
parent_device.save()
|
|
1897
1910
|
|
|
1911
|
+
# Test assigning a rack in the child location of the parent device location
|
|
1912
|
+
location_status = Status.objects.get_for_model(Location).first()
|
|
1913
|
+
child_location = Location.objects.create(
|
|
1914
|
+
name="Child Location 1",
|
|
1915
|
+
location_type=self.location_type_3,
|
|
1916
|
+
status=location_status,
|
|
1917
|
+
parent=parent_device.location,
|
|
1918
|
+
)
|
|
1919
|
+
child_rack = Rack.objects.create(name="Rack 2", location=child_location, status=self.device_status)
|
|
1920
|
+
parent_device.rack = child_rack
|
|
1921
|
+
parent_device.validated_save()
|
|
1922
|
+
|
|
1923
|
+
# Test assigning a rack outside the child locations of the parent device location
|
|
1924
|
+
new_location = Location.objects.create(
|
|
1925
|
+
name="New Location 1",
|
|
1926
|
+
status=location_status,
|
|
1927
|
+
location_type=self.location_type_3,
|
|
1928
|
+
)
|
|
1929
|
+
invalid_rack = Rack.objects.create(name="Rack 3", location=new_location, status=self.device_status)
|
|
1930
|
+
parent_device.rack = invalid_rack
|
|
1931
|
+
with self.assertRaises(ValidationError) as cm:
|
|
1932
|
+
parent_device.validated_save()
|
|
1933
|
+
self.assertIn(
|
|
1934
|
+
f'Rack "{invalid_rack}" does not belong to location "{parent_device.location}" and its descendants.',
|
|
1935
|
+
str(cm.exception),
|
|
1936
|
+
)
|
|
1937
|
+
|
|
1898
1938
|
child_mtime_after_parent_rack_update_save = str(Device.objects.get(name="Child Device 1").last_updated)
|
|
1899
1939
|
|
|
1900
1940
|
self.assertNotEqual(child_mtime_after_parent_noop_save, child_mtime_after_parent_rack_update_save)
|
nautobot/dcim/views.py
CHANGED
|
@@ -61,7 +61,7 @@ from nautobot.dcim.utils import get_all_network_driver_mappings, get_network_dri
|
|
|
61
61
|
from nautobot.extras.models import Contact, ContactAssociation, Role, Status, Team
|
|
62
62
|
from nautobot.extras.views import ObjectChangeLogView, ObjectConfigContextView, ObjectDynamicGroupsView
|
|
63
63
|
from nautobot.ipam.models import IPAddress, Prefix, Service, VLAN
|
|
64
|
-
from nautobot.ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable, VRFDeviceAssignmentTable
|
|
64
|
+
from nautobot.ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable, VRFDeviceAssignmentTable, VRFTable
|
|
65
65
|
from nautobot.virtualization.models import VirtualMachine
|
|
66
66
|
from nautobot.wireless.forms import ControllerManagedDeviceGroupWirelessNetworkFormSet
|
|
67
67
|
from nautobot.wireless.models import (
|
|
@@ -1922,7 +1922,7 @@ class DeviceView(generic.ObjectView):
|
|
|
1922
1922
|
|
|
1923
1923
|
# VRF assignments
|
|
1924
1924
|
vrf_assignments = instance.vrf_assignments.restrict(request.user, "view")
|
|
1925
|
-
vrf_table = VRFDeviceAssignmentTable(vrf_assignments
|
|
1925
|
+
vrf_table = VRFDeviceAssignmentTable(vrf_assignments)
|
|
1926
1926
|
|
|
1927
1927
|
# Software images
|
|
1928
1928
|
if instance.software_version is not None:
|
|
@@ -4513,15 +4513,25 @@ class VirtualDeviceContextUIViewSet(NautobotUIViewSet):
|
|
|
4513
4513
|
queryset = VirtualDeviceContext.objects.all()
|
|
4514
4514
|
serializer_class = serializers.VirtualDeviceContextSerializer
|
|
4515
4515
|
table_class = tables.VirtualDeviceContextTable
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4516
|
+
object_detail_content = object_detail.ObjectDetailContent(
|
|
4517
|
+
panels=(
|
|
4518
|
+
object_detail.ObjectFieldsPanel(
|
|
4519
|
+
section=SectionChoices.LEFT_HALF,
|
|
4520
|
+
weight=100,
|
|
4521
|
+
fields="__all__",
|
|
4522
|
+
),
|
|
4523
|
+
object_detail.ObjectsTablePanel(
|
|
4524
|
+
weight=200,
|
|
4525
|
+
table_class=tables.InterfaceTable,
|
|
4526
|
+
table_attribute="interfaces",
|
|
4527
|
+
section=SectionChoices.FULL_WIDTH,
|
|
4528
|
+
exclude_columns=["device"],
|
|
4529
|
+
),
|
|
4530
|
+
object_detail.ObjectsTablePanel(
|
|
4531
|
+
weight=300,
|
|
4532
|
+
table_class=VRFTable,
|
|
4533
|
+
table_attribute="vrfs",
|
|
4534
|
+
section=SectionChoices.FULL_WIDTH,
|
|
4535
|
+
),
|
|
4536
|
+
),
|
|
4537
|
+
)
|
nautobot/extras/api/mixins.py
CHANGED
nautobot/extras/api/views.py
CHANGED
|
@@ -642,7 +642,7 @@ class JobViewSetBase(
|
|
|
642
642
|
):
|
|
643
643
|
raise ValidationError(
|
|
644
644
|
{
|
|
645
|
-
"_task_queue": "_task_queue and _job_queue are both specified. Please
|
|
645
|
+
"_task_queue": "_task_queue and _job_queue are both specified. Please specify only one or another."
|
|
646
646
|
}
|
|
647
647
|
)
|
|
648
648
|
|
|
@@ -685,7 +685,7 @@ class JobViewSetBase(
|
|
|
685
685
|
"job_queue", None
|
|
686
686
|
):
|
|
687
687
|
raise ValidationError(
|
|
688
|
-
{"task_queue": "task_queue and job_queue are both specified. Please
|
|
688
|
+
{"task_queue": "task_queue and job_queue are both specified. Please specify only one or another."}
|
|
689
689
|
)
|
|
690
690
|
schedule_data = input_serializer.validated_data.get("schedule", None)
|
|
691
691
|
|
|
@@ -854,6 +854,10 @@ class JobFilterSet(BaseFilterSet, CustomFieldModelFilterSetMixin):
|
|
|
854
854
|
"description": "icontains",
|
|
855
855
|
},
|
|
856
856
|
)
|
|
857
|
+
job_queues = NaturalKeyOrPKMultipleChoiceFilter(
|
|
858
|
+
queryset=JobQueue.objects.all(),
|
|
859
|
+
label="Job Queue (name or ID)",
|
|
860
|
+
)
|
|
857
861
|
|
|
858
862
|
class Meta:
|
|
859
863
|
model = Job
|
|
@@ -131,16 +131,20 @@ class GitRepository(PrimaryModel):
|
|
|
131
131
|
|
|
132
132
|
def get_latest_sync(self):
|
|
133
133
|
"""
|
|
134
|
-
Return a `JobResult` for the latest sync operation.
|
|
134
|
+
Return a `JobResult` for the latest sync operation if one has occurred.
|
|
135
135
|
|
|
136
136
|
Returns:
|
|
137
|
-
JobResult
|
|
137
|
+
Returns a `JobResult` if the repo has been synced before, otherwise returns None.
|
|
138
138
|
"""
|
|
139
139
|
from nautobot.extras.models import JobResult
|
|
140
140
|
|
|
141
141
|
# This will match all "GitRepository" jobs (pull/refresh, dry-run, etc.)
|
|
142
142
|
prefix = "nautobot.core.jobs.GitRepository"
|
|
143
|
-
|
|
143
|
+
|
|
144
|
+
if JobResult.objects.filter(task_name__startswith=prefix, task_kwargs__repository=self.pk).exists():
|
|
145
|
+
return JobResult.objects.filter(task_name__startswith=prefix, task_kwargs__repository=self.pk).latest()
|
|
146
|
+
else:
|
|
147
|
+
return None
|
|
144
148
|
|
|
145
149
|
def to_csv(self):
|
|
146
150
|
return (
|
|
@@ -728,6 +728,31 @@ def register_plugin_menu_items(section_name, menu_items):
|
|
|
728
728
|
#
|
|
729
729
|
|
|
730
730
|
|
|
731
|
+
class CustomValidatorContext(dict):
|
|
732
|
+
def __init__(self, obj):
|
|
733
|
+
"""
|
|
734
|
+
If there is an active change context, meaning we are in a web request context,
|
|
735
|
+
we have access to the current user object. Otherwise, we are likely running inside
|
|
736
|
+
a management command or other non-web or non-Job context, and we should use an AnonymousUser.
|
|
737
|
+
This ensures people's custom validators don't outright break when running in non-web
|
|
738
|
+
contexts, and should generally provide a sane default, given validation based on the
|
|
739
|
+
user is commonly going to be least-privelege based, and thus the AnonymousUser will
|
|
740
|
+
cause such validation logic to fail closed.
|
|
741
|
+
"""
|
|
742
|
+
from django.contrib.auth.models import AnonymousUser
|
|
743
|
+
|
|
744
|
+
from nautobot.extras.signals import change_context_state
|
|
745
|
+
|
|
746
|
+
change_context = change_context_state.get()
|
|
747
|
+
user = None
|
|
748
|
+
if change_context:
|
|
749
|
+
user = change_context.get_user()
|
|
750
|
+
if user is None:
|
|
751
|
+
user = AnonymousUser()
|
|
752
|
+
|
|
753
|
+
super().__init__(object=obj, user=user)
|
|
754
|
+
|
|
755
|
+
|
|
731
756
|
class CustomValidator:
|
|
732
757
|
"""
|
|
733
758
|
This class is used to register plugin custom model validators which act on specified models. It contains the clean
|
|
@@ -742,7 +767,7 @@ class CustomValidator:
|
|
|
742
767
|
model = None
|
|
743
768
|
|
|
744
769
|
def __init__(self, obj):
|
|
745
|
-
self.context =
|
|
770
|
+
self.context = CustomValidatorContext(obj)
|
|
746
771
|
|
|
747
772
|
def validation_error(self, message):
|
|
748
773
|
"""
|
|
@@ -12,45 +12,43 @@
|
|
|
12
12
|
<strong>Summary of Results</strong>
|
|
13
13
|
</div>
|
|
14
14
|
<table class="table table-hover panel-body">
|
|
15
|
-
{% if result.job_model is not None %}
|
|
16
15
|
<tr>
|
|
17
16
|
<td>Job Description</td>
|
|
18
|
-
<td>{{ result.job_model.description | render_markdown }}</td>
|
|
17
|
+
<td>{{ result.job_model.description | render_markdown | placeholder }}</td>
|
|
19
18
|
</tr>
|
|
20
|
-
{% endif %}
|
|
21
19
|
<tr>
|
|
22
20
|
<td>Status</td>
|
|
23
21
|
<td><span id="pending-result-label">{% include 'extras/inc/job_label.html' with result=result %}</span></td>
|
|
24
22
|
</tr>
|
|
25
23
|
<tr>
|
|
26
24
|
<td>Started at</td>
|
|
27
|
-
<td>{{ result.date_created }}</td>
|
|
25
|
+
<td>{{ result.date_created | placeholder }}</td>
|
|
28
26
|
</tr>
|
|
29
27
|
<tr>
|
|
30
28
|
<td>User</td>
|
|
31
|
-
<td>{{ result.user }}</td>
|
|
29
|
+
<td>{{ result.user | placeholder }}</td>
|
|
32
30
|
</tr>
|
|
33
31
|
<tr>
|
|
34
32
|
<td>Duration</td>
|
|
35
33
|
<td>
|
|
36
|
-
{% if result.date_done %}
|
|
37
|
-
{{ result.duration }}
|
|
38
|
-
{% else %}
|
|
34
|
+
{% if result.date_created and not result.date_done %}
|
|
39
35
|
<img src="{% static 'img/ajax-loader.gif' %}">
|
|
36
|
+
{% else %}
|
|
37
|
+
{{ result.duration | placeholder}}
|
|
40
38
|
{% endif %}
|
|
41
39
|
</td>
|
|
42
40
|
</tr>
|
|
43
41
|
<tr>
|
|
44
42
|
<td>Return Value</td>
|
|
45
43
|
<td>
|
|
46
|
-
{% if result.date_done %}
|
|
44
|
+
{% if result.date_created and not result.date_done %}
|
|
45
|
+
<img src="{% static 'img/ajax-loader.gif' %}">
|
|
46
|
+
{% else %}
|
|
47
47
|
{% if result.result %}
|
|
48
48
|
<pre>{{ result.result | render_json }}</pre>
|
|
49
49
|
{% else %}
|
|
50
50
|
{{ result.result | placeholder }}
|
|
51
51
|
{% endif %}
|
|
52
|
-
{% else %}
|
|
53
|
-
<img src="{% static 'img/ajax-loader.gif' %}">
|
|
54
52
|
{% endif %}
|
|
55
53
|
</td>
|
|
56
54
|
</tr>
|
|
@@ -89,6 +87,7 @@
|
|
|
89
87
|
<input class="form-control" id="log-filter" type="text" placeholder="Filter log level or message" title="Filter log level or message" style="height: 23px" />
|
|
90
88
|
</div>
|
|
91
89
|
</div>
|
|
92
|
-
{%
|
|
90
|
+
{% if result and result.pk %}
|
|
91
|
+
{% ajax_table "log_table" "extras:jobresult_log-table" pk=result.pk %}
|
|
92
|
+
{% endif %}
|
|
93
93
|
</div>
|
|
94
|
-
|