nautobot 2.4.21__py3-none-any.whl → 2.4.22__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/choices.py +4 -0
- nautobot/apps/utils.py +8 -0
- nautobot/circuits/views.py +6 -2
- nautobot/core/cli/migrate_deprecated_templates.py +28 -9
- nautobot/core/filters.py +4 -0
- nautobot/core/forms/__init__.py +2 -0
- nautobot/core/forms/widgets.py +21 -2
- nautobot/core/settings.py +6 -0
- nautobot/core/templates/widgets/number_input_with_choices.html +44 -0
- nautobot/core/templatetags/helpers.py +9 -7
- nautobot/core/tests/nautobot_config.py +3 -0
- nautobot/core/tests/test_templatetags_helpers.py +6 -0
- nautobot/core/tests/test_ui.py +49 -1
- nautobot/core/tests/test_utils.py +41 -1
- nautobot/core/ui/object_detail.py +7 -2
- nautobot/core/urls.py +7 -8
- nautobot/core/utils/filtering.py +11 -1
- nautobot/core/utils/lookup.py +46 -0
- nautobot/core/views/mixins.py +21 -16
- nautobot/dcim/api/serializers.py +3 -0
- nautobot/dcim/choices.py +49 -0
- nautobot/dcim/constants.py +7 -0
- nautobot/dcim/filters/__init__.py +7 -0
- nautobot/dcim/forms.py +89 -3
- nautobot/dcim/migrations/0075_interface_duplex_interface_speed_and_more.py +32 -0
- nautobot/dcim/models/device_component_templates.py +33 -1
- nautobot/dcim/models/device_components.py +21 -0
- nautobot/dcim/tables/devices.py +14 -0
- nautobot/dcim/tables/devicetypes.py +8 -1
- nautobot/dcim/templates/dcim/interface.html +8 -0
- nautobot/dcim/templates/dcim/interface_edit.html +2 -0
- nautobot/dcim/tests/test_api.py +186 -6
- nautobot/dcim/tests/test_filters.py +32 -0
- nautobot/dcim/tests/test_forms.py +110 -8
- nautobot/dcim/tests/test_graphql.py +44 -1
- nautobot/dcim/tests/test_models.py +265 -0
- nautobot/dcim/tests/test_tables.py +160 -0
- nautobot/dcim/tests/test_views.py +64 -1
- nautobot/dcim/views.py +86 -77
- nautobot/extras/forms/forms.py +3 -1
- nautobot/extras/templates/extras/plugin_detail.html +2 -2
- nautobot/extras/urls.py +0 -14
- nautobot/extras/views.py +1 -1
- nautobot/ipam/ui.py +0 -17
- nautobot/ipam/views.py +2 -2
- nautobot/project-static/js/forms.js +92 -14
- nautobot/virtualization/tests/test_models.py +4 -2
- nautobot/virtualization/views.py +1 -0
- {nautobot-2.4.21.dist-info → nautobot-2.4.22.dist-info}/METADATA +4 -4
- {nautobot-2.4.21.dist-info → nautobot-2.4.22.dist-info}/RECORD +54 -51
- {nautobot-2.4.21.dist-info → nautobot-2.4.22.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.4.21.dist-info → nautobot-2.4.22.dist-info}/NOTICE +0 -0
- {nautobot-2.4.21.dist-info → nautobot-2.4.22.dist-info}/WHEEL +0 -0
- {nautobot-2.4.21.dist-info → nautobot-2.4.22.dist-info}/entry_points.txt +0 -0
nautobot/dcim/views.py
CHANGED
|
@@ -485,25 +485,31 @@ class LocationUIViewSet(NautobotUIViewSet):
|
|
|
485
485
|
)
|
|
486
486
|
|
|
487
487
|
def get_extra_context(self, request, instance):
|
|
488
|
-
|
|
489
|
-
return super().get_extra_context(request, instance)
|
|
488
|
+
context = super().get_extra_context(request, instance)
|
|
490
489
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
490
|
+
if self.action == "retrieve":
|
|
491
|
+
# This query can get really expensive when there are big location trees in the DB. By casting it to a list we
|
|
492
|
+
# ensure it is only performed once rather than as a subquery for each of the different count stats.
|
|
493
|
+
related_locations = list(
|
|
494
|
+
instance.descendants(include_self=True).restrict(request.user, "view").values_list("pk", flat=True)
|
|
495
|
+
)
|
|
496
496
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
497
|
+
rack_groups = (
|
|
498
|
+
RackGroup.objects.annotate(rack_count=count_related(Rack, "rack_group"))
|
|
499
|
+
.restrict(request.user, "view")
|
|
500
|
+
.filter(location__in=related_locations)
|
|
501
|
+
)
|
|
502
502
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
503
|
+
context.update(
|
|
504
|
+
{
|
|
505
|
+
"rack_groups": rack_groups,
|
|
506
|
+
"rack_count": Rack.objects.restrict(request.user, "view")
|
|
507
|
+
.filter(location__in=related_locations)
|
|
508
|
+
.count(),
|
|
509
|
+
}
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
return context
|
|
507
513
|
|
|
508
514
|
|
|
509
515
|
class MigrateLocationDataToContactView(generic.ObjectEditView):
|
|
@@ -1480,65 +1486,68 @@ class ModuleTypeUIViewSet(
|
|
|
1480
1486
|
return super().get_required_permission()
|
|
1481
1487
|
|
|
1482
1488
|
def get_extra_context(self, request, instance):
|
|
1483
|
-
|
|
1484
|
-
|
|
1489
|
+
context = super().get_extra_context(request, instance)
|
|
1490
|
+
if self.action == "retrieve":
|
|
1491
|
+
instance_count = Module.objects.restrict(request.user).filter(module_type=instance).count()
|
|
1485
1492
|
|
|
1486
|
-
|
|
1493
|
+
# Component tables
|
|
1494
|
+
consoleport_table = tables.ConsolePortTemplateTable(
|
|
1495
|
+
ConsolePortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
|
|
1496
|
+
orderable=False,
|
|
1497
|
+
)
|
|
1498
|
+
consoleserverport_table = tables.ConsoleServerPortTemplateTable(
|
|
1499
|
+
ConsoleServerPortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
|
|
1500
|
+
orderable=False,
|
|
1501
|
+
)
|
|
1502
|
+
powerport_table = tables.PowerPortTemplateTable(
|
|
1503
|
+
PowerPortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
|
|
1504
|
+
orderable=False,
|
|
1505
|
+
)
|
|
1506
|
+
poweroutlet_table = tables.PowerOutletTemplateTable(
|
|
1507
|
+
PowerOutletTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
|
|
1508
|
+
orderable=False,
|
|
1509
|
+
)
|
|
1510
|
+
interface_table = tables.InterfaceTemplateTable(
|
|
1511
|
+
list(InterfaceTemplate.objects.restrict(request.user, "view").filter(module_type=instance)),
|
|
1512
|
+
orderable=False,
|
|
1513
|
+
)
|
|
1514
|
+
front_port_table = tables.FrontPortTemplateTable(
|
|
1515
|
+
FrontPortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
|
|
1516
|
+
orderable=False,
|
|
1517
|
+
)
|
|
1518
|
+
rear_port_table = tables.RearPortTemplateTable(
|
|
1519
|
+
RearPortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
|
|
1520
|
+
orderable=False,
|
|
1521
|
+
)
|
|
1522
|
+
modulebay_table = tables.ModuleBayTemplateTable(
|
|
1523
|
+
ModuleBayTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
|
|
1524
|
+
orderable=False,
|
|
1525
|
+
)
|
|
1526
|
+
if request.user.has_perm("dcim.change_moduletype"):
|
|
1527
|
+
consoleport_table.columns.show("pk")
|
|
1528
|
+
consoleserverport_table.columns.show("pk")
|
|
1529
|
+
powerport_table.columns.show("pk")
|
|
1530
|
+
poweroutlet_table.columns.show("pk")
|
|
1531
|
+
interface_table.columns.show("pk")
|
|
1532
|
+
front_port_table.columns.show("pk")
|
|
1533
|
+
rear_port_table.columns.show("pk")
|
|
1534
|
+
modulebay_table.columns.show("pk")
|
|
1487
1535
|
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
poweroutlet_table = tables.PowerOutletTemplateTable(
|
|
1502
|
-
PowerOutletTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
|
|
1503
|
-
orderable=False,
|
|
1504
|
-
)
|
|
1505
|
-
interface_table = tables.InterfaceTemplateTable(
|
|
1506
|
-
list(InterfaceTemplate.objects.restrict(request.user, "view").filter(module_type=instance)),
|
|
1507
|
-
orderable=False,
|
|
1508
|
-
)
|
|
1509
|
-
front_port_table = tables.FrontPortTemplateTable(
|
|
1510
|
-
FrontPortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
|
|
1511
|
-
orderable=False,
|
|
1512
|
-
)
|
|
1513
|
-
rear_port_table = tables.RearPortTemplateTable(
|
|
1514
|
-
RearPortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
|
|
1515
|
-
orderable=False,
|
|
1516
|
-
)
|
|
1517
|
-
modulebay_table = tables.ModuleBayTemplateTable(
|
|
1518
|
-
ModuleBayTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
|
|
1519
|
-
orderable=False,
|
|
1520
|
-
)
|
|
1521
|
-
if request.user.has_perm("dcim.change_moduletype"):
|
|
1522
|
-
consoleport_table.columns.show("pk")
|
|
1523
|
-
consoleserverport_table.columns.show("pk")
|
|
1524
|
-
powerport_table.columns.show("pk")
|
|
1525
|
-
poweroutlet_table.columns.show("pk")
|
|
1526
|
-
interface_table.columns.show("pk")
|
|
1527
|
-
front_port_table.columns.show("pk")
|
|
1528
|
-
rear_port_table.columns.show("pk")
|
|
1529
|
-
modulebay_table.columns.show("pk")
|
|
1536
|
+
context.update(
|
|
1537
|
+
{
|
|
1538
|
+
"instance_count": instance_count,
|
|
1539
|
+
"consoleport_table": consoleport_table,
|
|
1540
|
+
"consoleserverport_table": consoleserverport_table,
|
|
1541
|
+
"powerport_table": powerport_table,
|
|
1542
|
+
"poweroutlet_table": poweroutlet_table,
|
|
1543
|
+
"interface_table": interface_table,
|
|
1544
|
+
"front_port_table": front_port_table,
|
|
1545
|
+
"rear_port_table": rear_port_table,
|
|
1546
|
+
"modulebay_table": modulebay_table,
|
|
1547
|
+
}
|
|
1548
|
+
)
|
|
1530
1549
|
|
|
1531
|
-
return
|
|
1532
|
-
"instance_count": instance_count,
|
|
1533
|
-
"consoleport_table": consoleport_table,
|
|
1534
|
-
"consoleserverport_table": consoleserverport_table,
|
|
1535
|
-
"powerport_table": powerport_table,
|
|
1536
|
-
"poweroutlet_table": poweroutlet_table,
|
|
1537
|
-
"interface_table": interface_table,
|
|
1538
|
-
"front_port_table": front_port_table,
|
|
1539
|
-
"rear_port_table": rear_port_table,
|
|
1540
|
-
"modulebay_table": modulebay_table,
|
|
1541
|
-
}
|
|
1550
|
+
return context
|
|
1542
1551
|
|
|
1543
1552
|
@action(
|
|
1544
1553
|
detail=False,
|
|
@@ -2302,7 +2311,7 @@ class DeviceUIViewSet(NautobotUIViewSet):
|
|
|
2302
2311
|
|
|
2303
2312
|
def get_queryset(self):
|
|
2304
2313
|
queryset = super().get_queryset()
|
|
2305
|
-
if self.
|
|
2314
|
+
if self.action == "retrieve":
|
|
2306
2315
|
queryset = queryset.select_related(
|
|
2307
2316
|
"cluster__cluster_group",
|
|
2308
2317
|
"controller_managed_device_group__controller",
|
|
@@ -3062,7 +3071,7 @@ class DeviceUIViewSet(NautobotUIViewSet):
|
|
|
3062
3071
|
def get_extra_context(self, request, instance):
|
|
3063
3072
|
extra_context = super().get_extra_context(request, instance)
|
|
3064
3073
|
|
|
3065
|
-
if self.
|
|
3074
|
+
if self.action == "retrieve":
|
|
3066
3075
|
# VirtualChassis members
|
|
3067
3076
|
if instance.virtual_chassis is not None:
|
|
3068
3077
|
vc_members = (
|
|
@@ -5816,7 +5825,7 @@ class ControllerUIViewSet(NautobotUIViewSet):
|
|
|
5816
5825
|
object_detail.DistinctViewTab(
|
|
5817
5826
|
weight=700,
|
|
5818
5827
|
tab_id="wireless_networks",
|
|
5819
|
-
url_name="dcim:
|
|
5828
|
+
url_name="dcim:controller_wireless_networks",
|
|
5820
5829
|
label="Wireless Networks",
|
|
5821
5830
|
related_object_attribute="wireless_network_assignments",
|
|
5822
5831
|
panels=(
|
|
@@ -5840,12 +5849,12 @@ class ControllerUIViewSet(NautobotUIViewSet):
|
|
|
5840
5849
|
@action(
|
|
5841
5850
|
detail=True,
|
|
5842
5851
|
url_path="wireless-networks",
|
|
5843
|
-
url_name="
|
|
5852
|
+
url_name="wireless_networks",
|
|
5844
5853
|
methods=["get"],
|
|
5845
5854
|
custom_view_base_action="view",
|
|
5846
5855
|
custom_view_additional_permissions=["wireless.view_controllermanageddevicegroupwirelessnetworkassignment"],
|
|
5847
5856
|
)
|
|
5848
|
-
def
|
|
5857
|
+
def wireless_networks(self, request, *args, **kwargs):
|
|
5849
5858
|
return Response({})
|
|
5850
5859
|
|
|
5851
5860
|
|
nautobot/extras/forms/forms.py
CHANGED
|
@@ -386,7 +386,9 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
|
|
|
386
386
|
role = DynamicModelMultipleChoiceField(
|
|
387
387
|
queryset=Role.objects.get_for_models([Device, VirtualMachine]), to_field_name="name", required=False
|
|
388
388
|
)
|
|
389
|
-
|
|
389
|
+
device_type = DynamicModelMultipleChoiceField(
|
|
390
|
+
queryset=DeviceType.objects.all(), to_field_name="model", required=False
|
|
391
|
+
)
|
|
390
392
|
platform = DynamicModelMultipleChoiceField(queryset=Platform.objects.all(), to_field_name="name", required=False)
|
|
391
393
|
cluster_group = DynamicModelMultipleChoiceField(
|
|
392
394
|
queryset=ClusterGroup.objects.all(), to_field_name="name", required=False
|
|
@@ -83,11 +83,11 @@
|
|
|
83
83
|
<table class="table table-hover panel-body attr-table">
|
|
84
84
|
<tr>
|
|
85
85
|
<td>Min Nautobot Version</td>
|
|
86
|
-
<td>v{{ object.min_version |
|
|
86
|
+
<td>{% if object.min_version %}v{{ object.min_version }}{% else %}{{ None|placeholder }}{% endif %}</td>
|
|
87
87
|
</tr>
|
|
88
88
|
<tr>
|
|
89
89
|
<td>Max Nautobot Version</td>
|
|
90
|
-
<td>v{{ object.max_version |
|
|
90
|
+
<td>{% if object.max_version %}v{{ object.max_version }}{% else %}{{ None|placeholder }}{% endif %}</td>
|
|
91
91
|
</tr>
|
|
92
92
|
</table>
|
|
93
93
|
</div>
|
nautobot/extras/urls.py
CHANGED
|
@@ -4,7 +4,6 @@ from nautobot.core.views.routers import NautobotUIViewSetRouter
|
|
|
4
4
|
from nautobot.extras import views
|
|
5
5
|
from nautobot.extras.models import (
|
|
6
6
|
Job,
|
|
7
|
-
Relationship,
|
|
8
7
|
)
|
|
9
8
|
|
|
10
9
|
app_name = "extras"
|
|
@@ -111,19 +110,6 @@ urlpatterns = [
|
|
|
111
110
|
path("jobs/<str:class_path>/run/", views.JobRunView.as_view(), name="job_run_by_class_path"),
|
|
112
111
|
path("jobs/edit/", views.JobBulkEditView.as_view(), name="job_bulk_edit"),
|
|
113
112
|
path("jobs/delete/", views.JobBulkDeleteView.as_view(), name="job_bulk_delete"),
|
|
114
|
-
# Custom relationships
|
|
115
|
-
path(
|
|
116
|
-
"relationships/<uuid:pk>/changelog/",
|
|
117
|
-
views.ObjectChangeLogView.as_view(),
|
|
118
|
-
name="relationship_changelog",
|
|
119
|
-
kwargs={"model": Relationship},
|
|
120
|
-
),
|
|
121
|
-
path(
|
|
122
|
-
"relationships/<uuid:pk>/notes/",
|
|
123
|
-
views.ObjectNotesView.as_view(),
|
|
124
|
-
name="relationship_notes",
|
|
125
|
-
kwargs={"model": Relationship},
|
|
126
|
-
),
|
|
127
113
|
# Secrets
|
|
128
114
|
path(
|
|
129
115
|
"secrets/provider/<str:provider_slug>/form/",
|
nautobot/extras/views.py
CHANGED
|
@@ -1194,7 +1194,7 @@ class GitRepositoryUIViewSet(NautobotUIViewSet):
|
|
|
1194
1194
|
context = {
|
|
1195
1195
|
**super().get_extra_context(request, instance),
|
|
1196
1196
|
"result": job_result or {},
|
|
1197
|
-
"base_template": "extras/
|
|
1197
|
+
"base_template": "extras/gitrepository_retrieve.html",
|
|
1198
1198
|
"object": instance,
|
|
1199
1199
|
"active_tab": "result",
|
|
1200
1200
|
"verbose_name": instance._meta.verbose_name,
|
nautobot/ipam/ui.py
CHANGED
|
@@ -10,29 +10,12 @@ from nautobot.core.ui.object_detail import (
|
|
|
10
10
|
Button,
|
|
11
11
|
KeyValueTablePanel,
|
|
12
12
|
ObjectFieldsPanel,
|
|
13
|
-
ObjectsTablePanel,
|
|
14
13
|
)
|
|
15
14
|
from nautobot.core.views.utils import get_obj_from_context
|
|
16
15
|
|
|
17
16
|
logger = logging.getLogger(__name__)
|
|
18
17
|
|
|
19
18
|
|
|
20
|
-
# TODO: can be removed as a part of NAUTOBOT-1051
|
|
21
|
-
class PrefixChildTablePanel(ObjectsTablePanel):
|
|
22
|
-
def should_render(self, context: Context):
|
|
23
|
-
if not super().should_render(context):
|
|
24
|
-
return False
|
|
25
|
-
return context.get("active_tab") == "prefixes"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
# TODO: can be removed as a part of NAUTOBOT-1051
|
|
29
|
-
class IPAddressTablePanel(ObjectsTablePanel):
|
|
30
|
-
def should_render(self, context: Context):
|
|
31
|
-
if not super().should_render(context):
|
|
32
|
-
return False
|
|
33
|
-
return context.get("active_tab") == "ip-addresses"
|
|
34
|
-
|
|
35
|
-
|
|
36
19
|
class AddChildPrefixButton(Button):
|
|
37
20
|
"""Custom button to add a child prefix inside a Prefix detail view."""
|
|
38
21
|
|
nautobot/ipam/views.py
CHANGED
|
@@ -432,7 +432,7 @@ class PrefixUIViewSet(NautobotUIViewSet):
|
|
|
432
432
|
related_object_attribute="default_descendants",
|
|
433
433
|
url_name="ipam:prefix_prefixes",
|
|
434
434
|
panels=(
|
|
435
|
-
|
|
435
|
+
object_detail.ObjectsTablePanel(
|
|
436
436
|
section=SectionChoices.FULL_WIDTH,
|
|
437
437
|
weight=100,
|
|
438
438
|
context_table_key="prefix_table",
|
|
@@ -449,7 +449,7 @@ class PrefixUIViewSet(NautobotUIViewSet):
|
|
|
449
449
|
related_object_attribute="all_ips",
|
|
450
450
|
url_name="ipam:prefix_ipaddresses",
|
|
451
451
|
panels=[
|
|
452
|
-
|
|
452
|
+
object_detail.ObjectsTablePanel(
|
|
453
453
|
section=SectionChoices.FULL_WIDTH,
|
|
454
454
|
weight=100,
|
|
455
455
|
context_table_key="ip_table",
|
|
@@ -223,6 +223,19 @@ function initializeFormActionClick(context){
|
|
|
223
223
|
function initializeBulkEditNullification(context){
|
|
224
224
|
this_context = $(context);
|
|
225
225
|
this_context.find('input:checkbox[name=_nullify]').click(function() {
|
|
226
|
+
var $field = $('#id_' + this.value);
|
|
227
|
+
|
|
228
|
+
// If this is a NumberWithSelect (input-group + caret menu), don't hide the
|
|
229
|
+
// field. Some other fields (e.g.Interface: LAG, Bridge) currently do nothing
|
|
230
|
+
// when _nullify is checked, so this is consistent.
|
|
231
|
+
var $group = $field.closest('.input-group');
|
|
232
|
+
var isNumberWithSelect = $group.length &&
|
|
233
|
+
$group.find('.input-group-btn .dropdown-menu a.set_value').length > 0;
|
|
234
|
+
if (isNumberWithSelect) {
|
|
235
|
+
return; // no UI change; _nullify still submitted
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Existing behavior for other fields
|
|
226
239
|
$('#id_' + this.value).toggle('disabled');
|
|
227
240
|
});
|
|
228
241
|
}
|
|
@@ -590,19 +603,57 @@ function initializeVLANModeSelection(context){
|
|
|
590
603
|
|
|
591
604
|
function initializeMultiValueChar(context, dropdownParent=null){
|
|
592
605
|
this_context = $(context);
|
|
593
|
-
this_context.find('.nautobot-select2-multi-value-char').
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
"
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
+
this_context.find('.nautobot-select2-multi-value-char').each(function(){
|
|
607
|
+
var $el = $(this);
|
|
608
|
+
$el.select2({
|
|
609
|
+
allowClear: true,
|
|
610
|
+
tags: true,
|
|
611
|
+
theme: "bootstrap",
|
|
612
|
+
placeholder: "---------",
|
|
613
|
+
multiple: true,
|
|
614
|
+
dropdownParent: dropdownParent,
|
|
615
|
+
width: "off",
|
|
616
|
+
tokenSeparators: [',', ' '],
|
|
617
|
+
"language": {
|
|
618
|
+
"noResults": function(){
|
|
619
|
+
return "Type something to add it as an option";
|
|
620
|
+
}
|
|
621
|
+
},
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
// Ensure pressing Enter in the Select2 search adds the current token instead of submitting the form
|
|
625
|
+
$el.on('select2:open', function(){
|
|
626
|
+
const container = document.querySelector('.select2-container--open');
|
|
627
|
+
if (!container) return;
|
|
628
|
+
const search = container.querySelector('input.select2-search__field');
|
|
629
|
+
if (!search) return;
|
|
630
|
+
|
|
631
|
+
// Avoid stacking multiple handlers
|
|
632
|
+
if (search.getAttribute('data-enter-binds')) return;
|
|
633
|
+
search.setAttribute('data-enter-binds', '1');
|
|
634
|
+
|
|
635
|
+
search.addEventListener('keydown', function(e){
|
|
636
|
+
if (e.key === 'Enter'){
|
|
637
|
+
e.preventDefault();
|
|
638
|
+
e.stopPropagation();
|
|
639
|
+
const val = this.value.trim();
|
|
640
|
+
if (!val) return;
|
|
641
|
+
const sel = $el.get(0);
|
|
642
|
+
// If option doesn't exist, create it; otherwise select it
|
|
643
|
+
let found = Array.prototype.find.call(sel.options, function(opt){ return String(opt.value) === String(val); });
|
|
644
|
+
if (!found) {
|
|
645
|
+
sel.add(new Option(val, val, true, true));
|
|
646
|
+
} else {
|
|
647
|
+
found.selected = true;
|
|
648
|
+
}
|
|
649
|
+
// Clear the search box and notify Select2
|
|
650
|
+
this.value = '';
|
|
651
|
+
$($el).trigger('change');
|
|
652
|
+
// Close the dropdown so it doesn't linger after add
|
|
653
|
+
try { $el.select2('close'); } catch (e) {}
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
});
|
|
606
657
|
});
|
|
607
658
|
}
|
|
608
659
|
|
|
@@ -714,7 +765,34 @@ function initializeDynamicFilterForm(context){
|
|
|
714
765
|
lookup_type_value = $(this).find(".lookup_type-select").val();
|
|
715
766
|
lookup_value = $(this).find(".lookup_value-input");
|
|
716
767
|
lookup_value.attr("name", lookup_type_value);
|
|
717
|
-
})
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
// Pre-populate filter selects (default + advanced) from current URL query params,
|
|
771
|
+
// including free-form values that are not part of the preset choices.
|
|
772
|
+
(function prepopulateFilterSelectsFromURL(){
|
|
773
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
774
|
+
// Only target Select2 tag controls inside the filter UI (default sidebar and advanced modal).
|
|
775
|
+
// Avoids touching unrelated Select2 tagging fields elsewhere on the page (e.g., tags inputs).
|
|
776
|
+
const selector = '#default-filter form select.nautobot-select2-multi-value-char, #advanced-filter select.nautobot-select2-multi-value-char';
|
|
777
|
+
this_context.find(selector).each(function(){
|
|
778
|
+
const sel = this;
|
|
779
|
+
const name = sel.getAttribute('name');
|
|
780
|
+
if (!name) { return; }
|
|
781
|
+
const values = urlParams.getAll(name);
|
|
782
|
+
if (!values.length) { return; }
|
|
783
|
+
values.forEach(function(v){
|
|
784
|
+
let found = Array.prototype.find.call(sel.options, function(opt){ return String(opt.value) === String(v); });
|
|
785
|
+
if (!found) {
|
|
786
|
+
sel.add(new Option(v, v, true, true));
|
|
787
|
+
} else {
|
|
788
|
+
found.selected = true;
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
if (window.jQuery && $(sel).data('select2')) {
|
|
792
|
+
$(sel).trigger('change');
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
})();
|
|
718
796
|
|
|
719
797
|
// Remove applied filters
|
|
720
798
|
this_context.find(".remove-filter-param").on("click", function(){
|
|
@@ -131,6 +131,7 @@ class VMInterfaceTestCase(TestCase): # TODO: change to BaseModelTestCase
|
|
|
131
131
|
name="Int1", virtual_machine=self.virtualmachine, status=self.int_status
|
|
132
132
|
)
|
|
133
133
|
ips = list(IPAddress.objects.all()[:10])
|
|
134
|
+
self.assertEqual(len(ips), 10)
|
|
134
135
|
|
|
135
136
|
# baseline (no vm_interface to ip address relationships exists)
|
|
136
137
|
self.assertFalse(IPAddressToInterface.objects.filter(vm_interface=vm_interface).exists())
|
|
@@ -171,7 +172,8 @@ class VMInterfaceTestCase(TestCase): # TODO: change to BaseModelTestCase
|
|
|
171
172
|
vm_interface = VMInterface.objects.create(
|
|
172
173
|
name="Int1", virtual_machine=self.virtualmachine, status=self.int_status
|
|
173
174
|
)
|
|
174
|
-
ips = list(IPAddress.objects.
|
|
175
|
+
ips = list(IPAddress.objects.filter(ip_version=4)[:10])
|
|
176
|
+
self.assertEqual(len(ips), 10)
|
|
175
177
|
|
|
176
178
|
# baseline (no vm_interface to ip address relationships exists)
|
|
177
179
|
self.assertFalse(IPAddressToInterface.objects.filter(vm_interface=vm_interface).exists())
|
|
@@ -219,7 +221,7 @@ class VMInterfaceTestCase(TestCase): # TODO: change to BaseModelTestCase
|
|
|
219
221
|
self.virtualmachine.refresh_from_db()
|
|
220
222
|
self.assertEqual(self.virtualmachine.primary_ip4, None)
|
|
221
223
|
# NOTE: This effectively tests what happens when you pass remove_ip_addresses None; it
|
|
222
|
-
# NOTE: does not remove a v6 address, because there are no v6 IPs
|
|
224
|
+
# NOTE: does not remove a v6 address, because there are no v6 IPs used in this test
|
|
223
225
|
# NOTE: class.
|
|
224
226
|
count = vm_interface.remove_ip_addresses(self.virtualmachine.primary_ip6)
|
|
225
227
|
self.assertEqual(count, 0)
|
nautobot/virtualization/views.py
CHANGED
|
@@ -379,6 +379,7 @@ class VirtualMachineUIViewSet(NautobotUIViewSet):
|
|
|
379
379
|
detail=True,
|
|
380
380
|
url_path="config-context",
|
|
381
381
|
url_name="configcontext",
|
|
382
|
+
custom_view_base_action="view",
|
|
382
383
|
custom_view_additional_permissions=["extras.view_configcontext"],
|
|
383
384
|
)
|
|
384
385
|
def config_context(self, request, pk):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: nautobot
|
|
3
|
-
Version: 2.4.
|
|
3
|
+
Version: 2.4.22
|
|
4
4
|
Summary: Source of truth and network automation platform.
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Keywords: Nautobot
|
|
@@ -20,7 +20,7 @@ Provides-Extra: mysql
|
|
|
20
20
|
Provides-Extra: napalm
|
|
21
21
|
Provides-Extra: remote-storage
|
|
22
22
|
Provides-Extra: sso
|
|
23
|
-
Requires-Dist: Django (>=4.2.
|
|
23
|
+
Requires-Dist: Django (>=4.2.26,<4.3.0)
|
|
24
24
|
Requires-Dist: GitPython (>=3.1.45,<3.2.0)
|
|
25
25
|
Requires-Dist: Jinja2 (>=3.1.6,<3.2.0)
|
|
26
26
|
Requires-Dist: Markdown (>=3.8.2,<3.9.0)
|
|
@@ -32,7 +32,7 @@ Requires-Dist: django-ajax-tables (>=1.1.1,<1.2.0)
|
|
|
32
32
|
Requires-Dist: django-auth-ldap (>=5.2.0,<5.3.0) ; extra == "all" or extra == "ldap"
|
|
33
33
|
Requires-Dist: django-celery-beat (>=2.7.0,<2.8.0)
|
|
34
34
|
Requires-Dist: django-celery-results (>=2.6.0,<2.7.0)
|
|
35
|
-
Requires-Dist: django-constance (>=4.3.
|
|
35
|
+
Requires-Dist: django-constance (>=4.3.3,<4.4.0)
|
|
36
36
|
Requires-Dist: django-cors-headers (>=4.9.0,<4.10.0)
|
|
37
37
|
Requires-Dist: django-db-file-storage (>=0.5.6.1,<0.6.0.0)
|
|
38
38
|
Requires-Dist: django-extensions (>=4.1,<4.2)
|
|
@@ -60,7 +60,7 @@ Requires-Dist: mysqlclient (>=2.2.7,<2.3.0) ; extra == "all" or extra == "mysql"
|
|
|
60
60
|
Requires-Dist: napalm (>=4.1.0,<6.0.0) ; extra == "all" or extra == "napalm"
|
|
61
61
|
Requires-Dist: netaddr (>=1.3.0,<1.4.0)
|
|
62
62
|
Requires-Dist: netutils (>=1.14.0,<2.0.0)
|
|
63
|
-
Requires-Dist: nh3 (>=0.3.
|
|
63
|
+
Requires-Dist: nh3 (>=0.3.2,<0.4.0)
|
|
64
64
|
Requires-Dist: packaging (>=23.1)
|
|
65
65
|
Requires-Dist: prometheus-client (>=0.23.1,<0.24.0)
|
|
66
66
|
Requires-Dist: psycopg2-binary (>=2.9.11,<2.10.0)
|