nautobot 2.4.17__py3-none-any.whl → 2.4.18__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 nautobot might be problematic. Click here for more details.
- nautobot/apps/views.py +2 -0
- nautobot/circuits/templates/circuits/circuittermination_retrieve.html +1 -8
- nautobot/circuits/templates/circuits/inc/circuit_termination_speed_fragment.html +9 -0
- nautobot/circuits/tests/integration/test_circuit.py +2 -2
- nautobot/circuits/views.py +32 -15
- nautobot/core/filters.py +2 -2
- nautobot/core/settings.py +1 -0
- nautobot/core/settings.yaml +9 -0
- nautobot/core/tables.py +21 -23
- nautobot/core/templates/components/breadcrumbs.html +19 -0
- nautobot/core/templates/generic/object_changelog.html +0 -2
- nautobot/core/templates/generic/object_list.html +15 -12
- nautobot/core/templates/generic/object_notes.html +0 -2
- nautobot/core/templates/generic/object_retrieve.html +16 -9
- nautobot/core/templatetags/helpers.py +24 -0
- nautobot/core/templatetags/ui_framework.py +40 -5
- nautobot/core/testing/filters.py +37 -21
- nautobot/core/testing/views.py +25 -0
- nautobot/core/tests/test_tables.py +43 -6
- nautobot/core/tests/test_templatetags_ui_framework.py +146 -0
- nautobot/core/tests/test_titles.py +2 -2
- nautobot/core/tests/test_ui.py +14 -1
- nautobot/core/tests/test_views.py +45 -0
- nautobot/core/ui/breadcrumbs.py +13 -8
- nautobot/core/ui/object_detail.py +43 -5
- nautobot/core/ui/titles.py +9 -5
- nautobot/core/views/__init__.py +24 -3
- nautobot/core/views/generic.py +42 -17
- nautobot/core/views/mixins.py +146 -12
- nautobot/core/views/utils.py +117 -0
- nautobot/dcim/models/devices.py +4 -0
- nautobot/dcim/tables/__init__.py +2 -0
- nautobot/dcim/tables/devices.py +24 -0
- nautobot/dcim/tables/power.py +2 -2
- nautobot/dcim/templates/dcim/device/base.html +1 -11
- nautobot/dcim/templates/dcim/device_component.html +0 -19
- nautobot/dcim/templates/dcim/modulebay_retrieve.html +0 -16
- nautobot/dcim/templates/dcim/virtualchassis_retrieve.html +1 -50
- nautobot/dcim/tests/test_views.py +41 -0
- nautobot/dcim/views.py +160 -39
- nautobot/extras/filters/mixins.py +1 -1
- nautobot/extras/forms/forms.py +15 -0
- nautobot/extras/models/groups.py +10 -1
- nautobot/extras/models/jobs.py +2 -2
- nautobot/extras/plugins/views.py +18 -5
- nautobot/extras/tables.py +4 -2
- nautobot/extras/templates/extras/customfield_retrieve.html +1 -128
- nautobot/extras/templates/extras/dynamicgroup.html +2 -99
- nautobot/extras/templates/extras/dynamicgroup_edit.html +2 -199
- nautobot/extras/templates/extras/dynamicgroup_retrieve.html +99 -0
- nautobot/extras/templates/extras/dynamicgroup_update.html +199 -0
- nautobot/extras/templates/extras/gitrepository.html +2 -82
- nautobot/extras/templates/extras/gitrepository_object_edit.html +2 -13
- nautobot/extras/templates/extras/gitrepository_retrieve.html +82 -0
- nautobot/extras/templates/extras/gitrepository_update.html +13 -0
- nautobot/extras/templates/extras/note_retrieve.html +0 -52
- nautobot/extras/templates/extras/plugin_detail.html +3 -7
- nautobot/extras/templates/extras/plugins_list.html +0 -2
- nautobot/extras/tests/test_dynamicgroups.py +73 -18
- nautobot/extras/tests/test_views.py +5 -0
- nautobot/extras/urls.py +2 -94
- nautobot/extras/views.py +424 -430
- nautobot/ipam/querysets.py +3 -3
- nautobot/ipam/signals.py +6 -1
- nautobot/ipam/templates/ipam/prefix.html +0 -8
- nautobot/ipam/tests/test_api.py +5 -0
- nautobot/ipam/tests/test_models.py +387 -0
- nautobot/ipam/tests/test_querysets.py +46 -0
- nautobot/ipam/utils/migrations.py +1 -1
- nautobot/ipam/views.py +17 -8
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +72 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +45 -9
- nautobot/project-static/docs/code-reference/nautobot/apps/views.html +393 -15
- nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +1 -1
- nautobot/project-static/docs/development/core/getting-started.html +0 -15
- nautobot/project-static/docs/development/core/ui-component-framework.html +6 -11
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/release-notes/version-2.4.html +222 -0
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +300 -300
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guide/administration/configuration/settings.html +27 -0
- nautobot/project-static/img/nautobot_icon.svg +32 -34
- nautobot/project-static/js/table_sorting_indicator.js +0 -2
- {nautobot-2.4.17.dist-info → nautobot-2.4.18.dist-info}/METADATA +4 -4
- {nautobot-2.4.17.dist-info → nautobot-2.4.18.dist-info}/RECORD +90 -85
- nautobot/core/templates/inc/breadcrumbs.html +0 -14
- nautobot/project-static/docs/requirements.txt +0 -14
- {nautobot-2.4.17.dist-info → nautobot-2.4.18.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.4.17.dist-info → nautobot-2.4.18.dist-info}/NOTICE +0 -0
- {nautobot-2.4.17.dist-info → nautobot-2.4.18.dist-info}/WHEEL +0 -0
- {nautobot-2.4.17.dist-info → nautobot-2.4.18.dist-info}/entry_points.txt +0 -0
|
@@ -1,51 +1,2 @@
|
|
|
1
1
|
{% extends 'generic/object_retrieve.html' %}
|
|
2
|
-
{%
|
|
3
|
-
|
|
4
|
-
{% block content_left_page %}
|
|
5
|
-
<div class="panel panel-default">
|
|
6
|
-
<div class="panel-heading">
|
|
7
|
-
<strong>Virtual Chassis</strong>
|
|
8
|
-
</div>
|
|
9
|
-
<table class="table table-hover panel-body attr-table">
|
|
10
|
-
<tr>
|
|
11
|
-
<td>Domain</td>
|
|
12
|
-
<td>{{ object.domain|placeholder }}</td>
|
|
13
|
-
</tr>
|
|
14
|
-
<tr>
|
|
15
|
-
<td>Master</td>
|
|
16
|
-
<td>{{ object.master|hyperlinked_object }}</td>
|
|
17
|
-
</tr>
|
|
18
|
-
</table>
|
|
19
|
-
</div>
|
|
20
|
-
{% endblock content_left_page %}
|
|
21
|
-
|
|
22
|
-
{% block content_right_page %}
|
|
23
|
-
<div class="panel panel-default">
|
|
24
|
-
<div class="panel-heading">
|
|
25
|
-
<strong>Members</strong>
|
|
26
|
-
</div>
|
|
27
|
-
<table class="table table-hover panel-body attr-table">
|
|
28
|
-
<tr>
|
|
29
|
-
<th>Device</th>
|
|
30
|
-
<th>Position</th>
|
|
31
|
-
<th>Master</th>
|
|
32
|
-
<th>Priority</th>
|
|
33
|
-
</tr>
|
|
34
|
-
{% for vc_member in members %}
|
|
35
|
-
<tr{% if vc_member == device %} class="info"{% endif %}>
|
|
36
|
-
<td>{{ vc_member|hyperlinked_object }}</td>
|
|
37
|
-
<td><span class="badge badge-default">{{ vc_member.vc_position }}</span></td>
|
|
38
|
-
<td>{% if object.master == vc_member %}{{ True | render_boolean }}{% endif %}</td>
|
|
39
|
-
<td>{{ vc_member.vc_priority|placeholder }}</td>
|
|
40
|
-
</tr>
|
|
41
|
-
{% endfor %}
|
|
42
|
-
</table>
|
|
43
|
-
{% if perms.dcim.change_virtualchassis %}
|
|
44
|
-
<div class="panel-footer text-right noprint">
|
|
45
|
-
<a href="{% url 'dcim:virtualchassis_add_member' pk=object.pk %}?location={{ object.master.location.pk }}&rack={{ object.master.rack.pk }}" class="btn btn-primary btn-xs">
|
|
46
|
-
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Member
|
|
47
|
-
</a>
|
|
48
|
-
</div>
|
|
49
|
-
{% endif %}
|
|
50
|
-
</div>
|
|
51
|
-
{% endblock content_right_page %}
|
|
2
|
+
{% comment %}3.0 TODO: remove this template, which only exists for backward compatibility with 2.4 and earlier{% endcomment %}
|
|
@@ -4202,6 +4202,47 @@ class VirtualChassisTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
4202
4202
|
# Sanity check:
|
|
4203
4203
|
self.assertBodyContains(response, "<th>Name</th>", html=True)
|
|
4204
4204
|
|
|
4205
|
+
def test_set_master_after_adding_member(self):
|
|
4206
|
+
"""Ensure master can be set for a member that was added via the Add Member flow."""
|
|
4207
|
+
self.add_permissions(
|
|
4208
|
+
"dcim.view_device",
|
|
4209
|
+
"dcim.view_virtualchassis",
|
|
4210
|
+
"dcim.change_virtualchassis",
|
|
4211
|
+
"dcim.change_device",
|
|
4212
|
+
)
|
|
4213
|
+
|
|
4214
|
+
# Create VC
|
|
4215
|
+
vc = VirtualChassis.objects.create(name="VC-test", domain="domain-test")
|
|
4216
|
+
|
|
4217
|
+
# Simulate adding a member via the separate "add-member" flow by creating the device with virtual_chassis
|
|
4218
|
+
member = Device.objects.create(
|
|
4219
|
+
device_type=self.devices[0].device_type,
|
|
4220
|
+
role=self.devices[0].role,
|
|
4221
|
+
status=self.devices[0].status,
|
|
4222
|
+
name="separately-added-device",
|
|
4223
|
+
location=self.devices[0].location,
|
|
4224
|
+
virtual_chassis=vc,
|
|
4225
|
+
vc_position=1,
|
|
4226
|
+
)
|
|
4227
|
+
|
|
4228
|
+
# Now edit the VC and set the master to the existing member
|
|
4229
|
+
payload_data = {
|
|
4230
|
+
"name": vc.name,
|
|
4231
|
+
"domain": vc.domain,
|
|
4232
|
+
"master": str(member.pk),
|
|
4233
|
+
# no members formset rows are required because the member already exists
|
|
4234
|
+
"form-TOTAL_FORMS": "0",
|
|
4235
|
+
"form-INITIAL_FORMS": "0",
|
|
4236
|
+
"form-MIN_NUM_FORMS": "0",
|
|
4237
|
+
"form-MAX_NUM_FORMS": "1000",
|
|
4238
|
+
}
|
|
4239
|
+
|
|
4240
|
+
url = reverse("dcim:virtualchassis_edit", kwargs={"pk": vc.pk})
|
|
4241
|
+
self.client.post(url, data=payload_data, follow=True)
|
|
4242
|
+
|
|
4243
|
+
vc.refresh_from_db()
|
|
4244
|
+
self.assertEqual(vc.master, member)
|
|
4245
|
+
|
|
4205
4246
|
|
|
4206
4247
|
class PowerPanelTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
4207
4248
|
model = PowerPanel
|
nautobot/dcim/views.py
CHANGED
|
@@ -21,7 +21,7 @@ from django.urls import reverse
|
|
|
21
21
|
from django.utils.encoding import iri_to_uri
|
|
22
22
|
from django.utils.functional import cached_property
|
|
23
23
|
from django.utils.html import format_html
|
|
24
|
-
from django.utils.http import url_has_allowed_host_and_scheme
|
|
24
|
+
from django.utils.http import url_has_allowed_host_and_scheme, urlencode
|
|
25
25
|
from django.views.generic import View
|
|
26
26
|
from django_tables2 import RequestConfig
|
|
27
27
|
from rest_framework.decorators import action
|
|
@@ -42,6 +42,7 @@ from nautobot.core.ui.breadcrumbs import (
|
|
|
42
42
|
Breadcrumbs,
|
|
43
43
|
InstanceBreadcrumbItem,
|
|
44
44
|
ModelBreadcrumbItem,
|
|
45
|
+
ViewNameBreadcrumbItem,
|
|
45
46
|
)
|
|
46
47
|
from nautobot.core.ui.bulk_buttons import (
|
|
47
48
|
BulkDeleteButton,
|
|
@@ -49,6 +50,7 @@ from nautobot.core.ui.bulk_buttons import (
|
|
|
49
50
|
BulkRenameButton,
|
|
50
51
|
)
|
|
51
52
|
from nautobot.core.ui.choices import SectionChoices
|
|
53
|
+
from nautobot.core.ui.titles import Titles
|
|
52
54
|
from nautobot.core.utils.lookup import get_form_for_model
|
|
53
55
|
from nautobot.core.utils.permissions import get_permission_for_model
|
|
54
56
|
from nautobot.core.utils.requests import normalize_querydict
|
|
@@ -515,6 +517,7 @@ class RackGroupUIViewSet(NautobotUIViewSet):
|
|
|
515
517
|
if self.action == "retrieve" and instance:
|
|
516
518
|
racks = (
|
|
517
519
|
Rack.objects.restrict(request.user, "view")
|
|
520
|
+
# Note this filter - we want the table to include racks assigned to child rack groups as well
|
|
518
521
|
.filter(rack_group__in=instance.descendants(include_self=True))
|
|
519
522
|
.select_related("role", "location", "tenant")
|
|
520
523
|
)
|
|
@@ -590,6 +593,10 @@ class RackElevationListView(generic.ObjectListView):
|
|
|
590
593
|
filterset_form = forms.RackFilterForm
|
|
591
594
|
action_buttons = []
|
|
592
595
|
template_name = "dcim/rack_elevation_list.html"
|
|
596
|
+
view_titles = Titles(titles={"list": "Rack Elevation"})
|
|
597
|
+
breadcrumbs = Breadcrumbs(
|
|
598
|
+
items={"list": [ViewNameBreadcrumbItem(view_name="dcim:rack_elevation_list", label="Rack Elevation")]}
|
|
599
|
+
)
|
|
593
600
|
|
|
594
601
|
def extra_context(self):
|
|
595
602
|
racks = self.queryset
|
|
@@ -644,11 +651,15 @@ class RackReservationUIViewSet(NautobotUIViewSet):
|
|
|
644
651
|
|
|
645
652
|
object_detail_content = object_detail.ObjectDetailContent(
|
|
646
653
|
panels=(
|
|
647
|
-
object_detail.
|
|
654
|
+
object_detail.ObjectFieldsPanel(
|
|
648
655
|
section=SectionChoices.LEFT_HALF,
|
|
649
656
|
weight=100,
|
|
650
657
|
label="Rack",
|
|
651
|
-
|
|
658
|
+
fields=["rack__location", "rack__rack_group", "rack"],
|
|
659
|
+
key_transforms={
|
|
660
|
+
"rack__location": "Location",
|
|
661
|
+
"rack__rack_group": "Rack Group",
|
|
662
|
+
},
|
|
652
663
|
),
|
|
653
664
|
object_detail.ObjectFieldsPanel(
|
|
654
665
|
section=SectionChoices.LEFT_HALF,
|
|
@@ -664,23 +675,6 @@ class RackReservationUIViewSet(NautobotUIViewSet):
|
|
|
664
675
|
),
|
|
665
676
|
)
|
|
666
677
|
|
|
667
|
-
def get_extra_context(self, request, instance):
|
|
668
|
-
context = super().get_extra_context(request, instance)
|
|
669
|
-
if self.action == "retrieve":
|
|
670
|
-
context["rack_data"] = self.get_rack_context(instance)
|
|
671
|
-
return context
|
|
672
|
-
|
|
673
|
-
def get_rack_context(self, instance):
|
|
674
|
-
rack = getattr(instance, "rack", None)
|
|
675
|
-
if not rack:
|
|
676
|
-
return {}
|
|
677
|
-
|
|
678
|
-
return {
|
|
679
|
-
"location": rack.location,
|
|
680
|
-
"rack_group": rack.rack_group,
|
|
681
|
-
"rack": rack,
|
|
682
|
-
}
|
|
683
|
-
|
|
684
678
|
def get_object(self):
|
|
685
679
|
obj = super().get_object()
|
|
686
680
|
|
|
@@ -1890,13 +1884,14 @@ class PlatformUIViewSet(NautobotUIViewSet):
|
|
|
1890
1884
|
#
|
|
1891
1885
|
|
|
1892
1886
|
|
|
1893
|
-
class
|
|
1887
|
+
class DevicePageMixin:
|
|
1894
1888
|
breadcrumbs = Breadcrumbs(
|
|
1895
1889
|
items={
|
|
1896
1890
|
"detail": [
|
|
1897
1891
|
ModelBreadcrumbItem(model=Device),
|
|
1898
1892
|
ModelBreadcrumbItem(
|
|
1899
1893
|
model=Device,
|
|
1894
|
+
label=lambda c: c["object"].location,
|
|
1900
1895
|
reverse_query_params=lambda c: {"location": c["object"].location.pk},
|
|
1901
1896
|
),
|
|
1902
1897
|
InstanceBreadcrumbItem(
|
|
@@ -1911,6 +1906,49 @@ class DeviceBreadcrumbsMixin:
|
|
|
1911
1906
|
)
|
|
1912
1907
|
|
|
1913
1908
|
|
|
1909
|
+
class DeviceComponentPageMixin:
|
|
1910
|
+
"""
|
|
1911
|
+
This class hold the breadcrumbs paths for Device Components Pages like console ports.
|
|
1912
|
+
Depending on whether the component is associated with a device or a module, the appropriate breadcrumb path will be rendered.
|
|
1913
|
+
|
|
1914
|
+
For example:
|
|
1915
|
+
- Console Port assigned to the module: Modules / <Module name and link to details> / Console Ports (dcim/modules/<id>/console-ports/) / <Console Port name>
|
|
1916
|
+
- Console Port assigned to the device: Devices / <Device name and link to details> / Console Ports (dcim/devices/<id>/console-ports/) / <Console Port name>
|
|
1917
|
+
"""
|
|
1918
|
+
|
|
1919
|
+
breadcrumbs = Breadcrumbs(
|
|
1920
|
+
items={
|
|
1921
|
+
"detail": [
|
|
1922
|
+
ModelBreadcrumbItem(model=Device, should_render=lambda c: c["object"].device is not None),
|
|
1923
|
+
InstanceBreadcrumbItem(
|
|
1924
|
+
instance=lambda c: c["object"].device, should_render=lambda c: c["object"].device is not None
|
|
1925
|
+
),
|
|
1926
|
+
ViewNameBreadcrumbItem(
|
|
1927
|
+
view_name_key="device_breadcrumb_url",
|
|
1928
|
+
should_render=lambda c: c["object"].device is not None and c.get("device_breadcrumb_url"),
|
|
1929
|
+
reverse_kwargs=lambda c: {"pk": c["object"].device.pk},
|
|
1930
|
+
label=lambda c: c["object"]._meta.verbose_name_plural,
|
|
1931
|
+
),
|
|
1932
|
+
ModelBreadcrumbItem(model=Module, should_render=lambda c: c["object"].device is None),
|
|
1933
|
+
InstanceBreadcrumbItem(
|
|
1934
|
+
instance=lambda c: c["object"].module, should_render=lambda c: c["object"].device is None
|
|
1935
|
+
),
|
|
1936
|
+
ViewNameBreadcrumbItem(
|
|
1937
|
+
view_name_key="module_breadcrumb_url",
|
|
1938
|
+
should_render=lambda c: c["object"].device is None and c.get("module_breadcrumb_url"),
|
|
1939
|
+
reverse_kwargs=lambda c: {"pk": c["object"].module.pk},
|
|
1940
|
+
label=lambda c: c["object"]._meta.verbose_name_plural,
|
|
1941
|
+
),
|
|
1942
|
+
]
|
|
1943
|
+
}
|
|
1944
|
+
)
|
|
1945
|
+
view_titles = Titles(
|
|
1946
|
+
titles={
|
|
1947
|
+
"detail": "{% if object.device %}{{ object.device }}{% else %}{{ object.module.display }}{% endif %} / {{ object }}"
|
|
1948
|
+
}
|
|
1949
|
+
)
|
|
1950
|
+
|
|
1951
|
+
|
|
1914
1952
|
class DeviceListView(generic.ObjectListView):
|
|
1915
1953
|
queryset = Device.objects.select_related(
|
|
1916
1954
|
"device_type__manufacturer", # Needed for __str__() on device_type
|
|
@@ -1921,7 +1959,7 @@ class DeviceListView(generic.ObjectListView):
|
|
|
1921
1959
|
template_name = "dcim/device_list.html"
|
|
1922
1960
|
|
|
1923
1961
|
|
|
1924
|
-
class DeviceView(generic.ObjectView):
|
|
1962
|
+
class DeviceView(DevicePageMixin, generic.ObjectView):
|
|
1925
1963
|
queryset = Device.objects.select_related(
|
|
1926
1964
|
"cluster__cluster_group",
|
|
1927
1965
|
"controller_managed_device_group__controller",
|
|
@@ -2177,7 +2215,7 @@ class DeviceView(generic.ObjectView):
|
|
|
2177
2215
|
}
|
|
2178
2216
|
|
|
2179
2217
|
|
|
2180
|
-
class DeviceComponentTabView(generic.ObjectView):
|
|
2218
|
+
class DeviceComponentTabView(DevicePageMixin, generic.ObjectView):
|
|
2181
2219
|
queryset = Device.objects.all()
|
|
2182
2220
|
|
|
2183
2221
|
def get_extra_context(self, request, instance):
|
|
@@ -2185,6 +2223,7 @@ class DeviceComponentTabView(generic.ObjectView):
|
|
|
2185
2223
|
module_count = instance.module_bays.filter(installed_module__isnull=False).count()
|
|
2186
2224
|
|
|
2187
2225
|
return {
|
|
2226
|
+
**super().get_extra_context(request, instance),
|
|
2188
2227
|
"modulebay_count": modulebay_count,
|
|
2189
2228
|
"module_count": f"{module_count}/{modulebay_count}",
|
|
2190
2229
|
}
|
|
@@ -2444,7 +2483,7 @@ class DeviceConfigView(generic.ObjectView):
|
|
|
2444
2483
|
}
|
|
2445
2484
|
|
|
2446
2485
|
|
|
2447
|
-
class DeviceConfigContextView(ObjectConfigContextView):
|
|
2486
|
+
class DeviceConfigContextView(DevicePageMixin, ObjectConfigContextView):
|
|
2448
2487
|
base_template = "dcim/device/base.html"
|
|
2449
2488
|
|
|
2450
2489
|
@cached_property
|
|
@@ -2455,7 +2494,7 @@ class DeviceConfigContextView(ObjectConfigContextView):
|
|
|
2455
2494
|
return Device.objects.annotate_config_context_data()
|
|
2456
2495
|
|
|
2457
2496
|
|
|
2458
|
-
class DeviceChangeLogView(ObjectChangeLogView):
|
|
2497
|
+
class DeviceChangeLogView(DevicePageMixin, ObjectChangeLogView):
|
|
2459
2498
|
base_template = "dcim/device/base.html"
|
|
2460
2499
|
|
|
2461
2500
|
|
|
@@ -3007,7 +3046,7 @@ class ConsolePortListView(generic.ObjectListView):
|
|
|
3007
3046
|
action_buttons = ("import", "export")
|
|
3008
3047
|
|
|
3009
3048
|
|
|
3010
|
-
class ConsolePortView(generic.ObjectView):
|
|
3049
|
+
class ConsolePortView(DeviceComponentPageMixin, generic.ObjectView):
|
|
3011
3050
|
queryset = ConsolePort.objects.all()
|
|
3012
3051
|
|
|
3013
3052
|
def get_extra_context(self, request, instance):
|
|
@@ -3073,7 +3112,7 @@ class ConsoleServerPortListView(generic.ObjectListView):
|
|
|
3073
3112
|
action_buttons = ("import", "export")
|
|
3074
3113
|
|
|
3075
3114
|
|
|
3076
|
-
class ConsoleServerPortView(generic.ObjectView):
|
|
3115
|
+
class ConsoleServerPortView(DeviceComponentPageMixin, generic.ObjectView):
|
|
3077
3116
|
queryset = ConsoleServerPort.objects.all()
|
|
3078
3117
|
|
|
3079
3118
|
def get_extra_context(self, request, instance):
|
|
@@ -3139,7 +3178,7 @@ class PowerPortListView(generic.ObjectListView):
|
|
|
3139
3178
|
action_buttons = ("import", "export")
|
|
3140
3179
|
|
|
3141
3180
|
|
|
3142
|
-
class PowerPortView(generic.ObjectView):
|
|
3181
|
+
class PowerPortView(DeviceComponentPageMixin, generic.ObjectView):
|
|
3143
3182
|
queryset = PowerPort.objects.all()
|
|
3144
3183
|
|
|
3145
3184
|
def get_extra_context(self, request, instance):
|
|
@@ -3205,7 +3244,7 @@ class PowerOutletListView(generic.ObjectListView):
|
|
|
3205
3244
|
action_buttons = ("import", "export")
|
|
3206
3245
|
|
|
3207
3246
|
|
|
3208
|
-
class PowerOutletView(generic.ObjectView):
|
|
3247
|
+
class PowerOutletView(DeviceComponentPageMixin, generic.ObjectView):
|
|
3209
3248
|
queryset = PowerOutlet.objects.all()
|
|
3210
3249
|
|
|
3211
3250
|
def get_extra_context(self, request, instance):
|
|
@@ -3271,7 +3310,10 @@ class InterfaceListView(generic.ObjectListView):
|
|
|
3271
3310
|
action_buttons = ("import", "export")
|
|
3272
3311
|
|
|
3273
3312
|
|
|
3274
|
-
class InterfaceView(
|
|
3313
|
+
class InterfaceView(
|
|
3314
|
+
DeviceComponentPageMixin,
|
|
3315
|
+
generic.ObjectView,
|
|
3316
|
+
):
|
|
3275
3317
|
queryset = Interface.objects.all()
|
|
3276
3318
|
|
|
3277
3319
|
def get_extra_context(self, request, instance):
|
|
@@ -3401,7 +3443,7 @@ class FrontPortListView(generic.ObjectListView):
|
|
|
3401
3443
|
action_buttons = ("import", "export")
|
|
3402
3444
|
|
|
3403
3445
|
|
|
3404
|
-
class FrontPortView(generic.ObjectView):
|
|
3446
|
+
class FrontPortView(DeviceComponentPageMixin, generic.ObjectView):
|
|
3405
3447
|
queryset = FrontPort.objects.all()
|
|
3406
3448
|
|
|
3407
3449
|
def get_extra_context(self, request, instance):
|
|
@@ -3467,7 +3509,7 @@ class RearPortListView(generic.ObjectListView):
|
|
|
3467
3509
|
action_buttons = ("import", "export")
|
|
3468
3510
|
|
|
3469
3511
|
|
|
3470
|
-
class RearPortView(generic.ObjectView):
|
|
3512
|
+
class RearPortView(DeviceComponentPageMixin, generic.ObjectView):
|
|
3471
3513
|
queryset = RearPort.objects.all()
|
|
3472
3514
|
|
|
3473
3515
|
def get_extra_context(self, request, instance):
|
|
@@ -3533,7 +3575,7 @@ class DeviceBayListView(generic.ObjectListView):
|
|
|
3533
3575
|
action_buttons = ("import", "export")
|
|
3534
3576
|
|
|
3535
3577
|
|
|
3536
|
-
class DeviceBayView(generic.ObjectView):
|
|
3578
|
+
class DeviceBayView(DeviceComponentPageMixin, generic.ObjectView):
|
|
3537
3579
|
queryset = DeviceBay.objects.all()
|
|
3538
3580
|
|
|
3539
3581
|
def get_extra_context(self, request, instance):
|
|
@@ -3695,6 +3737,35 @@ class ModuleBayUIViewSet(ModuleBayCommonViewSetMixin, NautobotUIViewSet):
|
|
|
3695
3737
|
),
|
|
3696
3738
|
)
|
|
3697
3739
|
)
|
|
3740
|
+
breadcrumbs = Breadcrumbs(
|
|
3741
|
+
items={
|
|
3742
|
+
"detail": [
|
|
3743
|
+
# Breadcrumb path if ModuleBay is linked with device
|
|
3744
|
+
ModelBreadcrumbItem(model=Device, should_render=lambda c: c["object"].parent_device),
|
|
3745
|
+
InstanceBreadcrumbItem(
|
|
3746
|
+
instance=lambda c: c["object"].parent_device, should_render=lambda c: c["object"].parent_device
|
|
3747
|
+
),
|
|
3748
|
+
ViewNameBreadcrumbItem(
|
|
3749
|
+
view_name_key="device_breadcrumb_url",
|
|
3750
|
+
should_render=lambda c: c["object"].parent_device and c.get("device_breadcrumb_url"),
|
|
3751
|
+
reverse_kwargs=lambda c: {"pk": c["object"].parent_device.pk},
|
|
3752
|
+
label=lambda c: c["object"]._meta.verbose_name_plural,
|
|
3753
|
+
),
|
|
3754
|
+
# Breadcrumb path if ModuleBay is linked with module
|
|
3755
|
+
ModelBreadcrumbItem(model=Module, should_render=lambda c: c["object"].parent_device is None),
|
|
3756
|
+
InstanceBreadcrumbItem(
|
|
3757
|
+
instance=lambda c: c["object"].parent_module,
|
|
3758
|
+
should_render=lambda c: c["object"].parent_device is None,
|
|
3759
|
+
),
|
|
3760
|
+
ViewNameBreadcrumbItem(
|
|
3761
|
+
view_name_key="module_breadcrumb_url",
|
|
3762
|
+
should_render=lambda c: c["object"].parent_device is None and c.get("module_breadcrumb_url"),
|
|
3763
|
+
reverse_kwargs=lambda c: {"pk": c["object"].parent_module.pk},
|
|
3764
|
+
label=lambda c: c["object"]._meta.verbose_name_plural,
|
|
3765
|
+
),
|
|
3766
|
+
]
|
|
3767
|
+
}
|
|
3768
|
+
)
|
|
3698
3769
|
|
|
3699
3770
|
def get_extra_context(self, request, instance):
|
|
3700
3771
|
context = super().get_extra_context(request, instance)
|
|
@@ -3748,7 +3819,7 @@ class InventoryItemListView(generic.ObjectListView):
|
|
|
3748
3819
|
action_buttons = ("import", "export")
|
|
3749
3820
|
|
|
3750
3821
|
|
|
3751
|
-
class InventoryItemView(generic.ObjectView):
|
|
3822
|
+
class InventoryItemView(DeviceComponentPageMixin, generic.ObjectView):
|
|
3752
3823
|
queryset = InventoryItem.objects.all().select_related("device", "manufacturer", "software_version")
|
|
3753
3824
|
|
|
3754
3825
|
def get_extra_context(self, request, instance):
|
|
@@ -4108,6 +4179,10 @@ class ConsoleConnectionsListView(ConnectionsListView):
|
|
|
4108
4179
|
table = tables.ConsoleConnectionTable
|
|
4109
4180
|
template_name = "dcim/console_port_connection_list.html"
|
|
4110
4181
|
action_buttons = ("export",)
|
|
4182
|
+
view_titles = Titles(titles={"list": "Console Connections"})
|
|
4183
|
+
breadcrumbs = Breadcrumbs(
|
|
4184
|
+
items={"list": [ViewNameBreadcrumbItem(view_name="dcim:console_connections_list", label="Console Connections")]}
|
|
4185
|
+
)
|
|
4111
4186
|
|
|
4112
4187
|
def extra_context(self):
|
|
4113
4188
|
return {
|
|
@@ -4124,6 +4199,10 @@ class PowerConnectionsListView(ConnectionsListView):
|
|
|
4124
4199
|
table = tables.PowerConnectionTable
|
|
4125
4200
|
template_name = "dcim/power_port_connection_list.html"
|
|
4126
4201
|
action_buttons = ("export",)
|
|
4202
|
+
view_titles = Titles(titles={"list": "Power Connections"})
|
|
4203
|
+
breadcrumbs = Breadcrumbs(
|
|
4204
|
+
items={"list": [ViewNameBreadcrumbItem(view_name="dcim:power_connections_list", label="Power Connections")]}
|
|
4205
|
+
)
|
|
4127
4206
|
|
|
4128
4207
|
def extra_context(self):
|
|
4129
4208
|
return {
|
|
@@ -4140,6 +4219,12 @@ class InterfaceConnectionsListView(ConnectionsListView):
|
|
|
4140
4219
|
table = tables.InterfaceConnectionTable
|
|
4141
4220
|
template_name = "dcim/interface_connection_list.html"
|
|
4142
4221
|
action_buttons = ("export",)
|
|
4222
|
+
view_titles = Titles(titles={"list": "Interface Connections"})
|
|
4223
|
+
breadcrumbs = Breadcrumbs(
|
|
4224
|
+
items={
|
|
4225
|
+
"list": [ViewNameBreadcrumbItem(view_name="dcim:interface_connections_list", label="Interface Connections")]
|
|
4226
|
+
}
|
|
4227
|
+
)
|
|
4143
4228
|
|
|
4144
4229
|
def __init__(self, *args, **kwargs):
|
|
4145
4230
|
super().__init__(*args, **kwargs)
|
|
@@ -4182,10 +4267,50 @@ class VirtualChassisUIViewSet(NautobotUIViewSet):
|
|
|
4182
4267
|
bulk_update_form_class = forms.VirtualChassisBulkEditForm
|
|
4183
4268
|
filterset_class = filters.VirtualChassisFilterSet
|
|
4184
4269
|
filterset_form_class = forms.VirtualChassisFilterForm
|
|
4185
|
-
form_class = forms.VirtualChassisCreateForm
|
|
4186
4270
|
serializer_class = serializers.VirtualChassisSerializer
|
|
4187
4271
|
table_class = tables.VirtualChassisTable
|
|
4188
4272
|
queryset = VirtualChassis.objects.all()
|
|
4273
|
+
create_form_class = forms.VirtualChassisCreateForm
|
|
4274
|
+
update_form_class = forms.VirtualChassisForm
|
|
4275
|
+
|
|
4276
|
+
class MembersObjectsTablePanel(object_detail.ObjectsTablePanel):
|
|
4277
|
+
def _get_table_add_url(self, context):
|
|
4278
|
+
obj = get_obj_from_context(context)
|
|
4279
|
+
request = context["request"]
|
|
4280
|
+
return_url = context.get("return_url", obj.get_absolute_url())
|
|
4281
|
+
|
|
4282
|
+
if not request.user.has_perm("dcim.change_virtualchassis"):
|
|
4283
|
+
return None
|
|
4284
|
+
|
|
4285
|
+
params = []
|
|
4286
|
+
master = obj.master
|
|
4287
|
+
|
|
4288
|
+
if master is not None:
|
|
4289
|
+
if master.location is not None:
|
|
4290
|
+
params.append(("location", master.location.pk))
|
|
4291
|
+
|
|
4292
|
+
if master.rack is not None:
|
|
4293
|
+
params.append(("rack", master.rack.pk))
|
|
4294
|
+
|
|
4295
|
+
params.append(("return_url", return_url))
|
|
4296
|
+
return reverse("dcim:virtualchassis_add_member", kwargs={"pk": obj.pk}) + "?" + urlencode(params)
|
|
4297
|
+
|
|
4298
|
+
object_detail_content = object_detail.ObjectDetailContent(
|
|
4299
|
+
panels=[
|
|
4300
|
+
object_detail.ObjectFieldsPanel(
|
|
4301
|
+
section=SectionChoices.LEFT_HALF,
|
|
4302
|
+
weight=100,
|
|
4303
|
+
fields="__all__",
|
|
4304
|
+
),
|
|
4305
|
+
MembersObjectsTablePanel(
|
|
4306
|
+
section=SectionChoices.RIGHT_HALF,
|
|
4307
|
+
weight=100,
|
|
4308
|
+
table_class=tables.VirtualChassisMembersTable,
|
|
4309
|
+
table_filter="virtual_chassis",
|
|
4310
|
+
table_title="Members",
|
|
4311
|
+
),
|
|
4312
|
+
]
|
|
4313
|
+
)
|
|
4189
4314
|
|
|
4190
4315
|
def get_extra_context(self, request, instance):
|
|
4191
4316
|
context = super().get_extra_context(request, instance)
|
|
@@ -4215,10 +4340,6 @@ class VirtualChassisUIViewSet(NautobotUIViewSet):
|
|
|
4215
4340
|
}
|
|
4216
4341
|
)
|
|
4217
4342
|
|
|
4218
|
-
elif self.action == "retrieve":
|
|
4219
|
-
members = Device.objects.restrict(request.user).filter(virtual_chassis=instance)
|
|
4220
|
-
context.update({"members": members})
|
|
4221
|
-
|
|
4222
4343
|
return context
|
|
4223
4344
|
|
|
4224
4345
|
def form_save(self, form, **kwargs):
|
|
@@ -209,7 +209,7 @@ class RelationshipFilter(django_filters.ModelMultipleChoiceFilter):
|
|
|
209
209
|
return query
|
|
210
210
|
|
|
211
211
|
def filter(self, qs, value):
|
|
212
|
-
if not value or any(v in EMPTY_VALUES for v in value):
|
|
212
|
+
if not value or any(v in EMPTY_VALUES for v in value) or (hasattr(value, "exists") and not value.exists()):
|
|
213
213
|
return qs
|
|
214
214
|
|
|
215
215
|
query = self.generate_query(value)
|
nautobot/extras/forms/forms.py
CHANGED
|
@@ -144,6 +144,7 @@ __all__ = (
|
|
|
144
144
|
"CustomLinkFilterForm",
|
|
145
145
|
"CustomLinkForm",
|
|
146
146
|
"DynamicGroupBulkAssignForm",
|
|
147
|
+
"DynamicGroupBulkEditForm",
|
|
147
148
|
"DynamicGroupFilterForm",
|
|
148
149
|
"DynamicGroupForm",
|
|
149
150
|
"DynamicGroupMembershipFormSet",
|
|
@@ -704,6 +705,20 @@ class CustomLinkFilterForm(BootstrapMixin, forms.Form):
|
|
|
704
705
|
#
|
|
705
706
|
# Dynamic Groups
|
|
706
707
|
#
|
|
708
|
+
class DynamicGroupBulkEditForm(NautobotBulkEditForm):
|
|
709
|
+
pk = forms.ModelMultipleChoiceField(queryset=DynamicGroup.objects.all(), widget=forms.MultipleHiddenInput())
|
|
710
|
+
description = forms.CharField(max_length=CHARFIELD_MAX_LENGTH, required=False)
|
|
711
|
+
tenant = DynamicModelChoiceField(
|
|
712
|
+
queryset=Tenant.objects.all(),
|
|
713
|
+
required=False,
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
class Meta:
|
|
717
|
+
model = DynamicGroup
|
|
718
|
+
fields = [
|
|
719
|
+
"description",
|
|
720
|
+
"tenant",
|
|
721
|
+
]
|
|
707
722
|
|
|
708
723
|
|
|
709
724
|
class DynamicGroupForm(TenancyForm, NautobotModelForm):
|
nautobot/extras/models/groups.py
CHANGED
|
@@ -806,7 +806,16 @@ class DynamicGroup(PrimaryModel):
|
|
|
806
806
|
def _get_group_queryset(self):
|
|
807
807
|
"""Construct the queryset representing dynamic membership of this group."""
|
|
808
808
|
query = self.generate_query()
|
|
809
|
-
|
|
809
|
+
# https://github.com/nautobot/nautobot/issues/7631
|
|
810
|
+
# Some queries may result in duplicate records, hence the need for `.distinct()`.
|
|
811
|
+
# Use of `.distinct()` in general is a code smell and a performance hit, but given the wide variety of
|
|
812
|
+
# filters that can be applied, support for both MySQL and PostgreSQL, and limitations of the Django ORM,
|
|
813
|
+
# I don't see a clear alternative at this time.
|
|
814
|
+
#
|
|
815
|
+
# Additionally, due to the use of `.distinct()`, in combination with our use of `.only("id")`
|
|
816
|
+
# on this queryset, e.g. in `_set_members()`, we also need to override any default model ordering on the
|
|
817
|
+
# queryset in order to avoid SQL errors like "each EXCEPT query must have the same number of columns"
|
|
818
|
+
return self.model.objects.filter(query).order_by("id").distinct()
|
|
810
819
|
|
|
811
820
|
# TODO: unused in core
|
|
812
821
|
def add_child(self, child, operator, weight):
|
nautobot/extras/models/jobs.py
CHANGED
|
@@ -48,7 +48,7 @@ from nautobot.extras.constants import (
|
|
|
48
48
|
)
|
|
49
49
|
from nautobot.extras.managers import JobResultManager, ScheduledJobsManager
|
|
50
50
|
from nautobot.extras.models import ChangeLoggedModel, GitRepository
|
|
51
|
-
from nautobot.extras.models.mixins import ContactMixin, DynamicGroupsModelMixin, NotesMixin
|
|
51
|
+
from nautobot.extras.models.mixins import ContactMixin, DynamicGroupsModelMixin, NotesMixin, SavedViewMixin
|
|
52
52
|
from nautobot.extras.querysets import JobQuerySet, ScheduledJobExtendedQuerySet
|
|
53
53
|
from nautobot.extras.utils import (
|
|
54
54
|
ChangeLoggedModelsQuery,
|
|
@@ -631,7 +631,7 @@ class JobQueueAssignment(BaseModel):
|
|
|
631
631
|
"custom_links",
|
|
632
632
|
"graphql",
|
|
633
633
|
)
|
|
634
|
-
class JobResult(BaseModel, CustomFieldModel):
|
|
634
|
+
class JobResult(SavedViewMixin, BaseModel, CustomFieldModel):
|
|
635
635
|
"""
|
|
636
636
|
This model stores the results from running a Job.
|
|
637
637
|
"""
|
nautobot/extras/plugins/views.py
CHANGED
|
@@ -86,10 +86,8 @@ class InstalledAppsView(GenericView):
|
|
|
86
86
|
"""
|
|
87
87
|
|
|
88
88
|
table = InstalledAppsTable
|
|
89
|
-
breadcrumbs = Breadcrumbs(
|
|
90
|
-
|
|
91
|
-
)
|
|
92
|
-
view_titles = Titles(titles={"generic": "Installed Apps"})
|
|
89
|
+
breadcrumbs = Breadcrumbs(items={"*": [ViewNameBreadcrumbItem(view_name="apps:apps_list", label="Installed Apps")]})
|
|
90
|
+
view_titles = Titles(titles={"*": "Installed Apps"})
|
|
93
91
|
|
|
94
92
|
def get(self, request):
|
|
95
93
|
marketplace_data = load_marketplace_data()
|
|
@@ -129,7 +127,6 @@ class InstalledAppsView(GenericView):
|
|
|
129
127
|
"filter_form": None,
|
|
130
128
|
"app_icons": app_icons,
|
|
131
129
|
"display": display,
|
|
132
|
-
"view_action": "generic",
|
|
133
130
|
"breadcrumbs": self.breadcrumbs,
|
|
134
131
|
"view_titles": self.view_titles,
|
|
135
132
|
},
|
|
@@ -141,6 +138,20 @@ class InstalledAppDetailView(GenericView):
|
|
|
141
138
|
View for showing details of an installed App.
|
|
142
139
|
"""
|
|
143
140
|
|
|
141
|
+
breadcrumbs = Breadcrumbs(
|
|
142
|
+
items={
|
|
143
|
+
"*": [
|
|
144
|
+
ViewNameBreadcrumbItem(view_name="apps:apps_list", label="Installed Apps"),
|
|
145
|
+
ViewNameBreadcrumbItem(
|
|
146
|
+
view_name="apps:app_detail",
|
|
147
|
+
reverse_kwargs=lambda context: {"app": context["app_data"]["package"]},
|
|
148
|
+
label=lambda context: context["app_data"]["name"],
|
|
149
|
+
),
|
|
150
|
+
]
|
|
151
|
+
}
|
|
152
|
+
)
|
|
153
|
+
view_titles = Titles(titles={"*": "{{ app_data.name | bettertitle }}"})
|
|
154
|
+
|
|
144
155
|
def get(self, request, app=None, plugin=None):
|
|
145
156
|
if plugin and not app:
|
|
146
157
|
app = plugin
|
|
@@ -154,6 +165,8 @@ class InstalledAppDetailView(GenericView):
|
|
|
154
165
|
{
|
|
155
166
|
"app_data": extract_app_data(app_config, load_marketplace_data()),
|
|
156
167
|
"object": app_config,
|
|
168
|
+
"breadcrumbs": self.breadcrumbs,
|
|
169
|
+
"view_titles": self.view_titles,
|
|
157
170
|
},
|
|
158
171
|
)
|
|
159
172
|
|
nautobot/extras/tables.py
CHANGED
|
@@ -736,11 +736,12 @@ class JobTable(BaseTable):
|
|
|
736
736
|
accessor="latest_result",
|
|
737
737
|
template_code="""
|
|
738
738
|
{% if value %}
|
|
739
|
-
{{ value.date_created|date:
|
|
739
|
+
{{ value.date_created|date:SHORT_DATETIME_FORMAT }} by {{ value.user }}
|
|
740
740
|
{% else %}
|
|
741
741
|
<span class="text-muted">Never</span>
|
|
742
742
|
{% endif %}
|
|
743
743
|
""",
|
|
744
|
+
extra_context={"SHORT_DATETIME_FORMAT": settings.SHORT_DATETIME_FORMAT},
|
|
744
745
|
linkify=lambda value: value.get_absolute_url() if value else None,
|
|
745
746
|
)
|
|
746
747
|
last_status = tables.TemplateColumn(
|
|
@@ -906,6 +907,7 @@ class JobResultTable(BaseTable):
|
|
|
906
907
|
linkify=True,
|
|
907
908
|
verbose_name="Scheduled Job",
|
|
908
909
|
)
|
|
910
|
+
duration = tables.Column(orderable=False)
|
|
909
911
|
actions = ButtonsColumn(JobResult, buttons=("delete",), prepend_template=JOB_RESULT_BUTTONS)
|
|
910
912
|
|
|
911
913
|
def __init__(self, *args, **kwargs):
|
|
@@ -1069,7 +1071,7 @@ class ObjectMetadataTable(BaseTable):
|
|
|
1069
1071
|
)
|
|
1070
1072
|
# This is needed so that render_value method below does not skip itself
|
|
1071
1073
|
# when metadata_type.data_type is TYPE_CONTACT_TEAM and we need it to display either contact or team
|
|
1072
|
-
value = tables.Column(empty_values=[])
|
|
1074
|
+
value = tables.Column(empty_values=[], order_by=("_value",))
|
|
1073
1075
|
|
|
1074
1076
|
class Meta(BaseTable.Meta):
|
|
1075
1077
|
model = ObjectMetadata
|