nautobot 2.4.0__py3-none-any.whl → 2.4.1__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/core/celery/schedulers.py +1 -1
- nautobot/core/filters.py +48 -21
- nautobot/core/jobs/bulk_actions.py +56 -19
- nautobot/core/models/__init__.py +2 -0
- nautobot/core/tables.py +5 -1
- nautobot/core/testing/filters.py +25 -13
- nautobot/core/testing/integration.py +86 -4
- nautobot/core/tests/test_filters.py +209 -246
- nautobot/core/tests/test_jobs.py +250 -93
- nautobot/core/tests/test_models.py +9 -0
- nautobot/core/views/generic.py +80 -48
- nautobot/core/views/mixins.py +34 -6
- nautobot/dcim/api/serializers.py +2 -2
- nautobot/dcim/constants.py +6 -13
- nautobot/dcim/factory.py +6 -1
- nautobot/dcim/tests/integration/test_device_bulk_delete.py +189 -0
- nautobot/dcim/tests/integration/test_device_bulk_edit.py +181 -0
- nautobot/dcim/tests/test_api.py +0 -2
- nautobot/dcim/tests/test_models.py +42 -28
- nautobot/extras/forms/mixins.py +1 -1
- nautobot/extras/jobs.py +15 -6
- nautobot/extras/templatetags/job_buttons.py +4 -4
- nautobot/extras/tests/test_forms.py +13 -0
- nautobot/extras/tests/test_jobs.py +18 -13
- nautobot/extras/tests/test_models.py +6 -0
- nautobot/extras/tests/test_views.py +4 -3
- nautobot/ipam/tests/test_api.py +20 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +36 -1
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/release-notes/version-2.4.html +108 -0
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +288 -288
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/wireless/tests/test_views.py +22 -1
- {nautobot-2.4.0.dist-info → nautobot-2.4.1.dist-info}/METADATA +2 -2
- {nautobot-2.4.0.dist-info → nautobot-2.4.1.dist-info}/RECORD +40 -38
- {nautobot-2.4.0.dist-info → nautobot-2.4.1.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.4.0.dist-info → nautobot-2.4.1.dist-info}/NOTICE +0 -0
- {nautobot-2.4.0.dist-info → nautobot-2.4.1.dist-info}/WHEEL +0 -0
- {nautobot-2.4.0.dist-info → nautobot-2.4.1.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
from django.urls import reverse
|
|
2
|
+
|
|
3
|
+
from nautobot.core.testing.integration import BulkOperationsMixin, ObjectsListMixin, SeleniumTestCase
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BulkDeleteDeviceTestCase(SeleniumTestCase, ObjectsListMixin, BulkOperationsMixin):
|
|
7
|
+
"""
|
|
8
|
+
Test devices bulk delete.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def setUp(self):
|
|
12
|
+
super().setUp()
|
|
13
|
+
|
|
14
|
+
self.user.is_superuser = True
|
|
15
|
+
self.user.save()
|
|
16
|
+
self.login(self.user.username, self.password)
|
|
17
|
+
|
|
18
|
+
# Manufacturer
|
|
19
|
+
self.click_navbar_entry("Devices", "Manufacturers")
|
|
20
|
+
self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:manufacturer_list"))
|
|
21
|
+
self.click_list_view_add_button()
|
|
22
|
+
self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:manufacturer_add"))
|
|
23
|
+
self.browser.fill("name", "Test Manufacturer 1")
|
|
24
|
+
self.click_edit_form_create_button()
|
|
25
|
+
|
|
26
|
+
# Device Type
|
|
27
|
+
self.click_navbar_entry("Devices", "Device Types")
|
|
28
|
+
self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:devicetype_list"))
|
|
29
|
+
self.click_list_view_add_button()
|
|
30
|
+
self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:devicetype_add"))
|
|
31
|
+
self.fill_select2_field("manufacturer", "Test Manufacturer 1")
|
|
32
|
+
self.browser.fill("model", "Test Device Type 1")
|
|
33
|
+
self.click_edit_form_create_button()
|
|
34
|
+
|
|
35
|
+
# LocationType
|
|
36
|
+
self.click_navbar_entry("Organization", "Location Types")
|
|
37
|
+
self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:locationtype_list"))
|
|
38
|
+
self.click_list_view_add_button()
|
|
39
|
+
self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:locationtype_add"))
|
|
40
|
+
self.fill_select2_multiselect_field("content_types", "dcim | device")
|
|
41
|
+
self.browser.fill("name", "Test Location Type 1")
|
|
42
|
+
self.click_edit_form_create_button()
|
|
43
|
+
|
|
44
|
+
# Location 1
|
|
45
|
+
self.click_navbar_entry("Organization", "Locations")
|
|
46
|
+
self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:location_list"))
|
|
47
|
+
self.click_list_view_add_button()
|
|
48
|
+
self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:location_add"))
|
|
49
|
+
self.fill_select2_field("location_type", "Test Location Type 1")
|
|
50
|
+
self.fill_select2_field("status", "") # pick first status
|
|
51
|
+
self.browser.fill("name", "Test Location 1")
|
|
52
|
+
self.click_edit_form_create_button()
|
|
53
|
+
|
|
54
|
+
# Location 2
|
|
55
|
+
self.click_navbar_entry("Organization", "Locations")
|
|
56
|
+
self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:location_list"))
|
|
57
|
+
self.click_list_view_add_button()
|
|
58
|
+
self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:location_add"))
|
|
59
|
+
self.fill_select2_field("location_type", "Test Location Type 1")
|
|
60
|
+
self.fill_select2_field("status", "") # pick first status
|
|
61
|
+
self.browser.fill("name", "Test Location 2")
|
|
62
|
+
self.click_edit_form_create_button()
|
|
63
|
+
|
|
64
|
+
# Role
|
|
65
|
+
self.click_navbar_entry("Organization", "Roles")
|
|
66
|
+
self.assertEqual(self.browser.url, self.live_server_url + reverse("extras:role_list"))
|
|
67
|
+
self.click_list_view_add_button()
|
|
68
|
+
self.assertEqual(self.browser.url, self.live_server_url + reverse("extras:role_add"))
|
|
69
|
+
self.browser.fill("name", "Test Role 1")
|
|
70
|
+
self.fill_select2_multiselect_field("content_types", "dcim | device")
|
|
71
|
+
self.click_edit_form_create_button()
|
|
72
|
+
|
|
73
|
+
def _create_device(self, name, location="Test Location 1"):
|
|
74
|
+
self.click_navbar_entry("Devices", "Devices")
|
|
75
|
+
self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:device_list"))
|
|
76
|
+
self.click_list_view_add_button()
|
|
77
|
+
self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:device_add"))
|
|
78
|
+
self.browser.fill("name", name)
|
|
79
|
+
self.fill_select2_field("role", "Test Role 1")
|
|
80
|
+
self.fill_select2_field("device_type", "Test Device Type 1")
|
|
81
|
+
self.fill_select2_field("location", location)
|
|
82
|
+
self.fill_select2_field("status", "") # pick first status
|
|
83
|
+
self.click_edit_form_create_button()
|
|
84
|
+
|
|
85
|
+
def tearDown(self):
|
|
86
|
+
self.logout()
|
|
87
|
+
super().tearDown()
|
|
88
|
+
|
|
89
|
+
def _go_to_devices_list(self):
|
|
90
|
+
self.click_navbar_entry("Devices", "Devices")
|
|
91
|
+
self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:device_list"))
|
|
92
|
+
|
|
93
|
+
def test_bulk_delete_require_selection(self):
|
|
94
|
+
self._go_to_devices_list()
|
|
95
|
+
|
|
96
|
+
# Click "delete selected" without selecting anything
|
|
97
|
+
self.click_bulk_delete()
|
|
98
|
+
|
|
99
|
+
self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:device_list"))
|
|
100
|
+
self.assertTrue(self.browser.is_text_present("No devices were selected for deletion.", wait_time=5))
|
|
101
|
+
|
|
102
|
+
def test_bulk_delete_all_devices(self):
|
|
103
|
+
# Create device for test
|
|
104
|
+
self._create_device("Test Device Integration Test 1")
|
|
105
|
+
self._create_device("Test Device Integration Test 2")
|
|
106
|
+
self._go_to_devices_list()
|
|
107
|
+
|
|
108
|
+
# Select all devices and delete them
|
|
109
|
+
self.select_all_items()
|
|
110
|
+
self.click_bulk_delete()
|
|
111
|
+
self.confirm_bulk_delete_operation()
|
|
112
|
+
|
|
113
|
+
# Verify job output
|
|
114
|
+
self.assertIsBulkDeleteJob()
|
|
115
|
+
job_status = self.wait_for_job_result()
|
|
116
|
+
|
|
117
|
+
self.assertEqual(job_status, "Completed")
|
|
118
|
+
|
|
119
|
+
self._go_to_devices_list()
|
|
120
|
+
self.assertEqual(self.objects_list_visible_items, 0)
|
|
121
|
+
|
|
122
|
+
def test_bulk_delete_one_device(self):
|
|
123
|
+
# Create device for test
|
|
124
|
+
self._create_device("Test Device Integration Test 1")
|
|
125
|
+
self._create_device("Test Device Integration Test 2")
|
|
126
|
+
self._go_to_devices_list()
|
|
127
|
+
|
|
128
|
+
# Select one device and delete it
|
|
129
|
+
self.select_one_item()
|
|
130
|
+
self.click_bulk_delete()
|
|
131
|
+
self.browser.find_by_xpath('//button[@name="_confirm" and @type="submit"]').click()
|
|
132
|
+
|
|
133
|
+
# Verify job output
|
|
134
|
+
self.assertIsBulkDeleteJob()
|
|
135
|
+
job_status = self.wait_for_job_result()
|
|
136
|
+
|
|
137
|
+
self.assertEqual(job_status, "Completed")
|
|
138
|
+
|
|
139
|
+
self._go_to_devices_list()
|
|
140
|
+
self.assertEqual(self.objects_list_visible_items, 1)
|
|
141
|
+
|
|
142
|
+
def test_bulk_delete_all_filtered_devices(self):
|
|
143
|
+
# Create device for test
|
|
144
|
+
self._create_device("Test Device Integration Test 1")
|
|
145
|
+
self._create_device("Test Device Integration Test 2")
|
|
146
|
+
self._create_device("Test Device Integration Test 3", location="Test Location 2")
|
|
147
|
+
self._go_to_devices_list()
|
|
148
|
+
|
|
149
|
+
# Filter devices
|
|
150
|
+
self.apply_filter("location", "Test Location 2")
|
|
151
|
+
|
|
152
|
+
# Select all devices and delete them
|
|
153
|
+
self.select_all_items()
|
|
154
|
+
self.click_bulk_delete()
|
|
155
|
+
self.confirm_bulk_delete_operation()
|
|
156
|
+
|
|
157
|
+
# Verify job output
|
|
158
|
+
self.assertIsBulkDeleteJob()
|
|
159
|
+
job_status = self.wait_for_job_result()
|
|
160
|
+
|
|
161
|
+
self.assertEqual(job_status, "Completed")
|
|
162
|
+
|
|
163
|
+
self._go_to_devices_list()
|
|
164
|
+
self.assertEqual(self.objects_list_visible_items, 2)
|
|
165
|
+
|
|
166
|
+
def test_bulk_delete_one_filtered_devices(self):
|
|
167
|
+
# Create device for test
|
|
168
|
+
self._create_device("Test Device Integration Test 1")
|
|
169
|
+
self._create_device("Test Device Integration Test 2")
|
|
170
|
+
self._create_device("Test Device Integration Test 3", location="Test Location 2")
|
|
171
|
+
self._create_device("Test Device Integration Test 4", location="Test Location 2")
|
|
172
|
+
self._go_to_devices_list()
|
|
173
|
+
|
|
174
|
+
# Filter devices
|
|
175
|
+
self.apply_filter("location", "Test Location 2")
|
|
176
|
+
|
|
177
|
+
# Select one device and delete it
|
|
178
|
+
self.select_one_item()
|
|
179
|
+
self.click_bulk_delete()
|
|
180
|
+
self.confirm_bulk_delete_operation()
|
|
181
|
+
|
|
182
|
+
# Verify job output
|
|
183
|
+
self.assertIsBulkDeleteJob()
|
|
184
|
+
job_status = self.wait_for_job_result()
|
|
185
|
+
|
|
186
|
+
self.assertEqual(job_status, "Completed")
|
|
187
|
+
|
|
188
|
+
self._go_to_devices_list()
|
|
189
|
+
self.assertEqual(self.objects_list_visible_items, 3)
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
from django.urls import reverse
|
|
2
|
+
|
|
3
|
+
from nautobot.core.testing.integration import BulkOperationsMixin, ObjectsListMixin, SeleniumTestCase
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BulkEditDeviceTestCase(SeleniumTestCase, ObjectsListMixin, BulkOperationsMixin):
|
|
7
|
+
"""
|
|
8
|
+
Test devices bulk delete.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def setUp(self):
|
|
12
|
+
super().setUp()
|
|
13
|
+
|
|
14
|
+
self.user.is_superuser = True
|
|
15
|
+
self.user.save()
|
|
16
|
+
self.login(self.user.username, self.password)
|
|
17
|
+
|
|
18
|
+
# Manufacturer
|
|
19
|
+
self.click_navbar_entry("Devices", "Manufacturers")
|
|
20
|
+
self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:manufacturer_list"))
|
|
21
|
+
self.click_list_view_add_button()
|
|
22
|
+
self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:manufacturer_add"))
|
|
23
|
+
self.browser.fill("name", "Test Manufacturer 1")
|
|
24
|
+
self.click_edit_form_create_button()
|
|
25
|
+
|
|
26
|
+
# Device Type
|
|
27
|
+
self.click_navbar_entry("Devices", "Device Types")
|
|
28
|
+
self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:devicetype_list"))
|
|
29
|
+
self.click_list_view_add_button()
|
|
30
|
+
self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:devicetype_add"))
|
|
31
|
+
self.fill_select2_field("manufacturer", "Test Manufacturer 1")
|
|
32
|
+
self.browser.fill("model", "Test Device Type 1")
|
|
33
|
+
self.click_edit_form_create_button()
|
|
34
|
+
|
|
35
|
+
# LocationType
|
|
36
|
+
self.click_navbar_entry("Organization", "Location Types")
|
|
37
|
+
self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:locationtype_list"))
|
|
38
|
+
self.click_list_view_add_button()
|
|
39
|
+
self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:locationtype_add"))
|
|
40
|
+
self.fill_select2_multiselect_field("content_types", "dcim | device")
|
|
41
|
+
self.browser.fill("name", "Test Location Type 1")
|
|
42
|
+
self.click_edit_form_create_button()
|
|
43
|
+
|
|
44
|
+
# Location 1
|
|
45
|
+
self.click_navbar_entry("Organization", "Locations")
|
|
46
|
+
self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:location_list"))
|
|
47
|
+
self.click_list_view_add_button()
|
|
48
|
+
self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:location_add"))
|
|
49
|
+
self.fill_select2_field("location_type", "Test Location Type 1")
|
|
50
|
+
self.fill_select2_field("status", "") # pick first status
|
|
51
|
+
self.browser.fill("name", "Test Location 1")
|
|
52
|
+
self.click_edit_form_create_button()
|
|
53
|
+
|
|
54
|
+
# Location 2
|
|
55
|
+
self.click_navbar_entry("Organization", "Locations")
|
|
56
|
+
self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:location_list"))
|
|
57
|
+
self.click_list_view_add_button()
|
|
58
|
+
self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:location_add"))
|
|
59
|
+
self.fill_select2_field("location_type", "Test Location Type 1")
|
|
60
|
+
self.fill_select2_field("status", "") # pick first status
|
|
61
|
+
self.browser.fill("name", "Test Location 2")
|
|
62
|
+
self.click_edit_form_create_button()
|
|
63
|
+
|
|
64
|
+
# Role
|
|
65
|
+
self.click_navbar_entry("Organization", "Roles")
|
|
66
|
+
self.assertEqual(self.browser.url, self.live_server_url + reverse("extras:role_list"))
|
|
67
|
+
self.click_list_view_add_button()
|
|
68
|
+
self.assertEqual(self.browser.url, self.live_server_url + reverse("extras:role_add"))
|
|
69
|
+
self.browser.fill("name", "Test Role 1")
|
|
70
|
+
self.fill_select2_multiselect_field("content_types", "dcim | device")
|
|
71
|
+
self.click_edit_form_create_button()
|
|
72
|
+
|
|
73
|
+
def _create_device(self, name, location="Test Location 1"):
|
|
74
|
+
self.click_navbar_entry("Devices", "Devices")
|
|
75
|
+
self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:device_list"))
|
|
76
|
+
self.click_list_view_add_button()
|
|
77
|
+
self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:device_add"))
|
|
78
|
+
self.browser.fill("name", name)
|
|
79
|
+
self.fill_select2_field("role", "Test Role 1")
|
|
80
|
+
self.fill_select2_field("device_type", "Test Device Type 1")
|
|
81
|
+
self.fill_select2_field("location", location)
|
|
82
|
+
self.fill_select2_field("status", "") # pick first status
|
|
83
|
+
self.click_edit_form_create_button()
|
|
84
|
+
|
|
85
|
+
def tearDown(self):
|
|
86
|
+
self.logout()
|
|
87
|
+
super().tearDown()
|
|
88
|
+
|
|
89
|
+
def _go_to_devices_list(self):
|
|
90
|
+
self.click_navbar_entry("Devices", "Devices")
|
|
91
|
+
self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:device_list"))
|
|
92
|
+
|
|
93
|
+
def test_bulk_edit_require_selection(self):
|
|
94
|
+
self._go_to_devices_list()
|
|
95
|
+
|
|
96
|
+
# Click "edit selected" without selecting anything
|
|
97
|
+
self.click_bulk_edit()
|
|
98
|
+
|
|
99
|
+
self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:device_list"))
|
|
100
|
+
self.assertTrue(self.browser.is_text_present("No devices were selected.", wait_time=5))
|
|
101
|
+
|
|
102
|
+
def test_bulk_edit_all_devices(self):
|
|
103
|
+
# Create device for test
|
|
104
|
+
self._create_device("Test Device Integration Test 1")
|
|
105
|
+
self._create_device("Test Device Integration Test 2")
|
|
106
|
+
self._go_to_devices_list()
|
|
107
|
+
|
|
108
|
+
# Select all devices and edit them
|
|
109
|
+
self.select_all_items()
|
|
110
|
+
self.click_bulk_edit()
|
|
111
|
+
self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:device_bulk_edit"))
|
|
112
|
+
|
|
113
|
+
# Submit bulk edit form without any changes
|
|
114
|
+
self.submit_bulk_edit_operation()
|
|
115
|
+
|
|
116
|
+
# Verify job output
|
|
117
|
+
self.assertIsBulkEditJob()
|
|
118
|
+
self.assertJobStatusIsCompleted()
|
|
119
|
+
|
|
120
|
+
def test_bulk_edit_one_device(self):
|
|
121
|
+
# Create device for test
|
|
122
|
+
self._create_device("Test Device Integration Test 1")
|
|
123
|
+
self._create_device("Test Device Integration Test 2")
|
|
124
|
+
self._go_to_devices_list()
|
|
125
|
+
|
|
126
|
+
# Select one device and edit it
|
|
127
|
+
self.select_one_item()
|
|
128
|
+
self.click_bulk_edit()
|
|
129
|
+
self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:device_bulk_edit"))
|
|
130
|
+
|
|
131
|
+
# Submit bulk edit form without any changes
|
|
132
|
+
self.submit_bulk_edit_operation()
|
|
133
|
+
|
|
134
|
+
# Verify job output
|
|
135
|
+
self.assertIsBulkEditJob()
|
|
136
|
+
self.assertJobStatusIsCompleted()
|
|
137
|
+
|
|
138
|
+
def test_bulk_edit_all_filtered_devices(self):
|
|
139
|
+
# Create device for test
|
|
140
|
+
self._create_device("Test Device Integration Test 1")
|
|
141
|
+
self._create_device("Test Device Integration Test 2")
|
|
142
|
+
self._create_device("Test Device Integration Test 3", location="Test Location 2")
|
|
143
|
+
self._go_to_devices_list()
|
|
144
|
+
|
|
145
|
+
# Filter devices
|
|
146
|
+
self.apply_filter("location", "Test Location 2")
|
|
147
|
+
|
|
148
|
+
# Select all devices and edit them
|
|
149
|
+
self.select_all_items()
|
|
150
|
+
self.click_bulk_edit()
|
|
151
|
+
self.assertIn(self.live_server_url + reverse("dcim:device_bulk_edit"), self.browser.url)
|
|
152
|
+
|
|
153
|
+
# Submit bulk edit form without any changes
|
|
154
|
+
self.submit_bulk_edit_operation()
|
|
155
|
+
|
|
156
|
+
# Verify job output
|
|
157
|
+
self.assertIsBulkEditJob()
|
|
158
|
+
self.assertJobStatusIsCompleted()
|
|
159
|
+
|
|
160
|
+
def test_bulk_edit_one_filtered_devices(self):
|
|
161
|
+
# Create device for test
|
|
162
|
+
self._create_device("Test Device Integration Test 1")
|
|
163
|
+
self._create_device("Test Device Integration Test 2")
|
|
164
|
+
self._create_device("Test Device Integration Test 3", location="Test Location 2")
|
|
165
|
+
self._create_device("Test Device Integration Test 4", location="Test Location 2")
|
|
166
|
+
self._go_to_devices_list()
|
|
167
|
+
|
|
168
|
+
# Filter devices
|
|
169
|
+
self.apply_filter("location", "Test Location 2")
|
|
170
|
+
|
|
171
|
+
# Select one device and edit it
|
|
172
|
+
self.select_all_items()
|
|
173
|
+
self.click_bulk_edit()
|
|
174
|
+
self.assertIn(self.live_server_url + reverse("dcim:device_bulk_edit"), self.browser.url)
|
|
175
|
+
|
|
176
|
+
# Submit bulk edit form without any changes
|
|
177
|
+
self.submit_bulk_edit_operation()
|
|
178
|
+
|
|
179
|
+
# Verify job output
|
|
180
|
+
self.assertIsBulkEditJob()
|
|
181
|
+
self.assertJobStatusIsCompleted()
|
nautobot/dcim/tests/test_api.py
CHANGED
|
@@ -3366,7 +3366,6 @@ class ControllerTestCase(APIViewTestCases.APIViewTestCase):
|
|
|
3366
3366
|
"status": statuses[1].pk,
|
|
3367
3367
|
"role": roles[1].pk,
|
|
3368
3368
|
"location": locations[1].pk,
|
|
3369
|
-
"capabilities": [],
|
|
3370
3369
|
},
|
|
3371
3370
|
{
|
|
3372
3371
|
"name": "Controller 3",
|
|
@@ -3402,7 +3401,6 @@ class ControllerManagedDeviceGroupTestCase(APIViewTestCases.APIViewTestCase):
|
|
|
3402
3401
|
"name": "ControllerManagedDeviceGroup 2",
|
|
3403
3402
|
"controller": controllers[1].pk,
|
|
3404
3403
|
"weight": 150,
|
|
3405
|
-
"capabilities": [],
|
|
3406
3404
|
},
|
|
3407
3405
|
{
|
|
3408
3406
|
"name": "ControllerManagedDeviceGroup 3",
|
|
@@ -2185,45 +2185,59 @@ class CableTestCase(ModelTestCases.BaseModelTestCase):
|
|
|
2185
2185
|
"""
|
|
2186
2186
|
A cable cannot terminate to a virtual interface
|
|
2187
2187
|
"""
|
|
2188
|
+
virtual_interface_choices = self.interface_choices["Virtual interfaces"]
|
|
2189
|
+
|
|
2188
2190
|
self.cable.delete()
|
|
2189
2191
|
interface_status = Status.objects.get_for_model(Interface).first()
|
|
2190
|
-
virtual_interface = Interface.objects.create(
|
|
2191
|
-
device=self.device1,
|
|
2192
|
-
name="V1",
|
|
2193
|
-
type=InterfaceTypeChoices.TYPE_VIRTUAL,
|
|
2194
|
-
status=interface_status,
|
|
2195
|
-
)
|
|
2196
|
-
cable = Cable(termination_a=self.interface2, termination_b=virtual_interface)
|
|
2197
|
-
with self.assertRaises(ValidationError) as cm:
|
|
2198
|
-
cable.clean()
|
|
2199
2192
|
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2193
|
+
for virtual_interface_type in virtual_interface_choices:
|
|
2194
|
+
virtual_interface = Interface.objects.create(
|
|
2195
|
+
device=self.device1,
|
|
2196
|
+
name=f"V-{virtual_interface_type}",
|
|
2197
|
+
type=virtual_interface_type,
|
|
2198
|
+
status=interface_status,
|
|
2199
|
+
)
|
|
2200
|
+
with self.assertRaises(
|
|
2201
|
+
ValidationError,
|
|
2202
|
+
msg=f"Virtual interface type '{virtual_interface_choices[virtual_interface_type]}' should not accept a cable.",
|
|
2203
|
+
) as cm:
|
|
2204
|
+
Cable(
|
|
2205
|
+
termination_a=self.interface2,
|
|
2206
|
+
termination_b=virtual_interface,
|
|
2207
|
+
).clean()
|
|
2208
|
+
|
|
2209
|
+
self.assertIn(
|
|
2210
|
+
f"Cables cannot be terminated to {virtual_interface_choices[virtual_interface_type]} interfaces",
|
|
2211
|
+
str(cm.exception),
|
|
2212
|
+
)
|
|
2205
2213
|
|
|
2206
2214
|
def test_cable_cannot_terminate_to_a_wireless_interface(self):
|
|
2207
2215
|
"""
|
|
2208
2216
|
A cable cannot terminate to a wireless interface
|
|
2209
2217
|
"""
|
|
2218
|
+
wireless_interface_choices = self.interface_choices["Wireless"]
|
|
2219
|
+
|
|
2210
2220
|
self.cable.delete()
|
|
2211
2221
|
interface_status = Status.objects.get_for_model(Interface).first()
|
|
2212
|
-
wireless_interface = Interface.objects.create(
|
|
2213
|
-
device=self.device1,
|
|
2214
|
-
name="W1",
|
|
2215
|
-
type=InterfaceTypeChoices.TYPE_80211A,
|
|
2216
|
-
status=interface_status,
|
|
2217
|
-
)
|
|
2218
|
-
cable = Cable(termination_a=self.interface2, termination_b=wireless_interface)
|
|
2219
|
-
with self.assertRaises(ValidationError) as cm:
|
|
2220
|
-
cable.clean()
|
|
2221
2222
|
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2223
|
+
for wireless_interface_type in wireless_interface_choices:
|
|
2224
|
+
wireless_interface = Interface.objects.create(
|
|
2225
|
+
device=self.device1,
|
|
2226
|
+
name=f"W-{wireless_interface_type}",
|
|
2227
|
+
type=wireless_interface_type,
|
|
2228
|
+
status=interface_status,
|
|
2229
|
+
)
|
|
2230
|
+
|
|
2231
|
+
with self.assertRaises(
|
|
2232
|
+
ValidationError,
|
|
2233
|
+
msg=f"Wireless interface type '{wireless_interface_choices[wireless_interface_type]}' should not accept a cable.",
|
|
2234
|
+
) as cm:
|
|
2235
|
+
Cable(termination_a=self.interface2, termination_b=wireless_interface).clean()
|
|
2236
|
+
|
|
2237
|
+
self.assertIn(
|
|
2238
|
+
f"Cables cannot be terminated to {wireless_interface_choices[wireless_interface_type]} interfaces",
|
|
2239
|
+
str(cm.exception),
|
|
2240
|
+
)
|
|
2227
2241
|
|
|
2228
2242
|
def test_create_cable_with_missing_status_connected(self):
|
|
2229
2243
|
"""Test for https://github.com/nautobot/nautobot/issues/2081"""
|
nautobot/extras/forms/mixins.py
CHANGED
|
@@ -327,7 +327,7 @@ class RelationshipModelBulkEditFormMixin(BulkEditForm):
|
|
|
327
327
|
field_name,
|
|
328
328
|
instance,
|
|
329
329
|
)
|
|
330
|
-
if field_name in self.nullable_fields and field_name in nullified_fields:
|
|
330
|
+
if field_name in self.nullable_fields and nullified_fields and field_name in nullified_fields:
|
|
331
331
|
logger.debug("Deleting existing relationship associations for %s on %s", relationship, instance)
|
|
332
332
|
relationshipassociation_queryset.delete()
|
|
333
333
|
elif field_name in self.cleaned_data:
|
nautobot/extras/jobs.py
CHANGED
|
@@ -534,7 +534,10 @@ class BaseJob:
|
|
|
534
534
|
job_model = JobModel.objects.get(module_name=cls.__module__, job_class_name=cls.__name__)
|
|
535
535
|
is_singleton = job_model.is_singleton
|
|
536
536
|
except JobModel.DoesNotExist:
|
|
537
|
+
logger.warning("No Job instance found in the database corresponding to %s", cls.class_path)
|
|
538
|
+
job_model = None
|
|
537
539
|
is_singleton = cls.is_singleton
|
|
540
|
+
|
|
538
541
|
if is_singleton:
|
|
539
542
|
form.fields["_ignore_singleton_lock"] = forms.BooleanField(
|
|
540
543
|
required=False,
|
|
@@ -543,10 +546,12 @@ class BaseJob:
|
|
|
543
546
|
help_text="Allow this singleton job to run even when another instance is already running",
|
|
544
547
|
)
|
|
545
548
|
|
|
546
|
-
job_model
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
549
|
+
if job_model is not None:
|
|
550
|
+
job_queue_queryset = JobQueue.objects.filter(jobs=job_model)
|
|
551
|
+
job_queue_params = {"jobs": [job_model.pk]}
|
|
552
|
+
else:
|
|
553
|
+
job_queue_queryset = JobQueue.objects.all()
|
|
554
|
+
job_queue_params = {}
|
|
550
555
|
|
|
551
556
|
# Initialize job_queue choices
|
|
552
557
|
form.fields["_job_queue"] = DynamicModelChoiceField(
|
|
@@ -556,8 +561,12 @@ class BaseJob:
|
|
|
556
561
|
help_text="The job queue to route this job to",
|
|
557
562
|
label="Job queue",
|
|
558
563
|
)
|
|
559
|
-
|
|
560
|
-
|
|
564
|
+
|
|
565
|
+
dryrun_default = cls.dryrun_default
|
|
566
|
+
if job_model is not None:
|
|
567
|
+
form.fields["_job_queue"].initial = job_model.default_job_queue.pk
|
|
568
|
+
if job_model.dryrun_default_override:
|
|
569
|
+
dryrun_default = job_model.dryrun_default
|
|
561
570
|
|
|
562
571
|
if cls.supports_dryrun and (not initial or "dryrun" not in initial):
|
|
563
572
|
# Set initial "dryrun" checkbox state based on the Meta parameter
|
|
@@ -8,7 +8,7 @@ from django.utils.html import format_html
|
|
|
8
8
|
from django.utils.safestring import mark_safe
|
|
9
9
|
|
|
10
10
|
from nautobot.core.utils.data import render_jinja2
|
|
11
|
-
from nautobot.extras.models import Job, JobButton
|
|
11
|
+
from nautobot.extras.models import Job, JobButton, JobQueue
|
|
12
12
|
|
|
13
13
|
register = template.Library()
|
|
14
14
|
|
|
@@ -108,17 +108,17 @@ def _render_job_button_for_obj(job_button, obj, context, content_type):
|
|
|
108
108
|
# Disable buttons if the user doesn't have permission to run the underlying Job.
|
|
109
109
|
has_run_perm = Job.objects.check_perms(context["user"], instance=job_button.job, action="run")
|
|
110
110
|
try:
|
|
111
|
-
job_queues =
|
|
111
|
+
job_queues = job_button.job.job_queues.all()
|
|
112
112
|
_job_queue = job_queues[0]
|
|
113
113
|
except IndexError:
|
|
114
|
-
_job_queue = settings.CELERY_TASK_DEFAULT_QUEUE
|
|
114
|
+
_job_queue = JobQueue.objects.get(name=settings.CELERY_TASK_DEFAULT_QUEUE)
|
|
115
115
|
hidden_inputs = format_html(
|
|
116
116
|
HIDDEN_INPUTS,
|
|
117
117
|
csrf_token=context["csrf_token"],
|
|
118
118
|
object_pk=obj.pk,
|
|
119
119
|
object_model_name=f"{content_type.app_label}.{content_type.model}",
|
|
120
120
|
redirect_path=context["request"].path,
|
|
121
|
-
job_queue=_job_queue,
|
|
121
|
+
job_queue=_job_queue.pk,
|
|
122
122
|
)
|
|
123
123
|
template_args = {
|
|
124
124
|
"button_id": job_button.pk,
|
|
@@ -910,6 +910,19 @@ class RelationshipModelBulkEditFormMixinTestCase(TestCase):
|
|
|
910
910
|
ras = RelationshipAssociation.objects.filter(relationship=self.rel_mtom_s)
|
|
911
911
|
self.assertEqual(1, ras.count())
|
|
912
912
|
|
|
913
|
+
def test_form_save_relationship_with_nullified_fields_is_none(self):
|
|
914
|
+
"""Test save_relationships with nullified_fields=None."""
|
|
915
|
+
form = LocationBulkEditForm(
|
|
916
|
+
model=dcim_models.Location,
|
|
917
|
+
data={
|
|
918
|
+
"pks": [self.locations[0].pk],
|
|
919
|
+
"add_cr_multiplexing__destination": [ipaddress.pk for ipaddress in self.ipaddresses],
|
|
920
|
+
"add_cr_peer_locations__peer": [self.locations[1].pk],
|
|
921
|
+
},
|
|
922
|
+
)
|
|
923
|
+
form.is_valid()
|
|
924
|
+
form.save_relationships(instance=self.locations[0], nullified_fields=None)
|
|
925
|
+
|
|
913
926
|
def test_location_form_remove_mtom(self):
|
|
914
927
|
"""Test removal of relationship-associations for many-to-many relationships."""
|
|
915
928
|
RelationshipAssociation.objects.create(
|