nautobot 2.4.2__py3-none-any.whl → 2.4.3__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/circuits/templates/circuits/inc/circuit_termination.html +1 -1
- nautobot/circuits/tests/integration/test_circuit.py +135 -0
- nautobot/circuits/views.py +4 -1
- nautobot/cloud/api/views.py +3 -3
- nautobot/core/constants.py +0 -1
- nautobot/core/forms/__init__.py +2 -0
- nautobot/core/forms/forms.py +2 -1
- nautobot/core/forms/widgets.py +8 -0
- nautobot/core/management/commands/generate_performance_test_endpoints.py +268 -0
- nautobot/core/templates/generic/object_bulk_delete.html +1 -1
- nautobot/core/templates/generic/object_bulk_edit.html +1 -1
- nautobot/core/templates/generic/object_bulk_import.html +1 -1
- nautobot/core/templates/generic/object_create.html +5 -0
- nautobot/core/templates/generic/object_delete.html +1 -1
- nautobot/core/templates/generic/object_detail.html +1 -1
- nautobot/core/templates/generic/object_edit.html +1 -1
- nautobot/core/templates/inc/javascript.html +2 -0
- nautobot/core/templates/widgets/clearable_file.html +5 -0
- nautobot/core/templatetags/helpers.py +3 -3
- nautobot/core/testing/integration.py +37 -7
- nautobot/core/tests/test_commands.py +31 -0
- nautobot/core/tests/test_utils.py +17 -2
- nautobot/core/utils/lookup.py +12 -1
- nautobot/core/views/generic.py +9 -1
- nautobot/core/views/mixins.py +9 -1
- nautobot/dcim/api/views.py +11 -10
- nautobot/dcim/forms.py +3 -6
- nautobot/dcim/models/devices.py +1 -2
- nautobot/dcim/templates/dcim/cable_trace.html +4 -4
- nautobot/dcim/templates/dcim/consoleport.html +14 -4
- nautobot/dcim/templates/dcim/consoleserverport.html +14 -4
- nautobot/dcim/templates/dcim/device/lldp_neighbors.html +3 -3
- nautobot/dcim/templates/dcim/frontport.html +7 -2
- nautobot/dcim/templates/dcim/interface.html +9 -4
- nautobot/dcim/templates/dcim/powerfeed.html +8 -3
- nautobot/dcim/templates/dcim/poweroutlet.html +14 -4
- nautobot/dcim/templates/dcim/powerport.html +14 -4
- nautobot/dcim/templates/dcim/rearport.html +7 -2
- nautobot/dcim/tests/integration/test_fileinputpicker.py +87 -0
- nautobot/dcim/tests/test_models.py +1 -1
- nautobot/extras/api/views.py +2 -2
- nautobot/extras/forms/forms.py +4 -0
- nautobot/extras/jobs.py +8 -1
- nautobot/extras/templates/extras/job.html +1 -0
- nautobot/extras/tests/test_dynamicgroups.py +14 -0
- nautobot/extras/tests/test_views.py +197 -9
- nautobot/extras/utils.py +30 -0
- nautobot/extras/views.py +29 -14
- nautobot/ipam/api/views.py +3 -3
- nautobot/ipam/forms.py +2 -6
- nautobot/project-static/bootstrap-filestyle-1.2.3/bootstrap-filestyle.min.js +11 -0
- nautobot/project-static/docs/apps/index.html +1 -1
- nautobot/project-static/docs/apps/nautobot-apps.html +1 -1
- nautobot/project-static/docs/development/apps/api/models/graphql.html +9 -9
- nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +2 -2
- nautobot/project-static/docs/development/apps/api/setup.html +1 -1
- nautobot/project-static/docs/development/apps/migration/code-updates.html +6 -5
- nautobot/project-static/docs/development/apps/migration/dependency-updates.html +2 -2
- nautobot/project-static/docs/development/apps/migration/from-v1.html +3 -3
- nautobot/project-static/docs/development/core/best-practices.html +1 -1
- nautobot/project-static/docs/development/core/bootstrap-ui.html +1 -1
- nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +7 -7
- nautobot/project-static/docs/development/core/getting-started.html +2 -2
- nautobot/project-static/docs/development/core/index.html +1 -1
- nautobot/project-static/docs/development/core/minikube-dev-environment-for-k8s-jobs.html +3 -3
- nautobot/project-static/docs/development/core/model-checklist.html +1 -1
- nautobot/project-static/docs/development/core/navigation-menu.html +1 -1
- nautobot/project-static/docs/development/core/release-checklist.html +1 -1
- nautobot/project-static/docs/development/core/settings.html +1 -1
- nautobot/project-static/docs/development/core/style-guide.html +4 -4
- nautobot/project-static/docs/development/jobs/index.html +8 -1
- nautobot/project-static/docs/development/jobs/migration/from-v1.html +3 -2
- nautobot/project-static/docs/index.html +3 -2
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/overview/application_stack.html +2 -2
- nautobot/project-static/docs/release-notes/version-1.0.html +2 -2
- nautobot/project-static/docs/release-notes/version-1.1.html +2 -2
- nautobot/project-static/docs/release-notes/version-1.2.html +3 -3
- nautobot/project-static/docs/release-notes/version-1.3.html +1 -1
- nautobot/project-static/docs/release-notes/version-1.4.html +17 -17
- nautobot/project-static/docs/release-notes/version-1.5.html +8 -8
- nautobot/project-static/docs/release-notes/version-1.6.html +4 -4
- nautobot/project-static/docs/release-notes/version-2.0.html +10 -10
- nautobot/project-static/docs/release-notes/version-2.1.html +7 -7
- nautobot/project-static/docs/release-notes/version-2.2.html +1 -1
- nautobot/project-static/docs/release-notes/version-2.3.html +4 -4
- nautobot/project-static/docs/release-notes/version-2.4.html +188 -0
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +290 -290
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +3 -3
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +4 -4
- nautobot/project-static/docs/user-guide/administration/configuration/redis.html +1 -1
- nautobot/project-static/docs/user-guide/administration/configuration/settings.html +3 -3
- nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +5 -5
- nautobot/project-static/docs/user-guide/administration/guides/docker.html +3 -3
- nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +1 -1
- nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +4 -4
- nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +15 -15
- nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +2 -2
- nautobot/project-static/docs/user-guide/administration/installation/app-install.html +1 -1
- nautobot/project-static/docs/user-guide/administration/installation/install_system.html +1 -1
- nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +6 -6
- nautobot/project-static/docs/user-guide/administration/installation/services.html +1 -1
- nautobot/project-static/docs/user-guide/administration/security/index.html +1 -1
- nautobot/project-static/docs/user-guide/administration/security/notices.html +1 -0
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +1 -1
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +11 -8
- nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +12 -12
- nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobqueue.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/kubernetes-job-support.html +6 -6
- nautobot/project-static/js/dropdown.js +28 -0
- nautobot/tenancy/forms.py +9 -0
- nautobot/tenancy/templates/tenancy/tenant_create.html +21 -0
- nautobot/tenancy/templates/tenancy/tenant_edit.html +2 -21
- nautobot/tenancy/templates/tenancy/tenantgroup.html +2 -44
- nautobot/tenancy/templates/tenancy/tenantgroup_retrieve.html +1 -0
- nautobot/tenancy/tests/test_views.py +5 -1
- nautobot/tenancy/urls.py +7 -79
- nautobot/tenancy/views.py +51 -80
- nautobot/wireless/api/serializers.py +6 -1
- nautobot/wireless/api/views.py +3 -3
- nautobot/wireless/tests/test_api.py +5 -0
- {nautobot-2.4.2.dist-info → nautobot-2.4.3.dist-info}/METADATA +8 -8
- {nautobot-2.4.2.dist-info → nautobot-2.4.3.dist-info}/RECORD +132 -123
- {nautobot-2.4.2.dist-info → nautobot-2.4.3.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.4.2.dist-info → nautobot-2.4.3.dist-info}/NOTICE +0 -0
- {nautobot-2.4.2.dist-info → nautobot-2.4.3.dist-info}/WHEEL +0 -0
- {nautobot-2.4.2.dist-info → nautobot-2.4.3.dist-info}/entry_points.txt +0 -0
|
@@ -86,6 +86,10 @@ class ObjectsListMixin:
|
|
|
86
86
|
"""
|
|
87
87
|
self.click_button('#select_all_box button[name="_edit"]')
|
|
88
88
|
|
|
89
|
+
def click_table_link(self, row=1, column=2):
|
|
90
|
+
"""By default, tries to click column next to checkbox to go to the details page."""
|
|
91
|
+
self.browser.find_by_xpath(f'//*[@id="object_list_form"]//tbody/tr[{row}]/td[{column}]/a').click()
|
|
92
|
+
|
|
89
93
|
@property
|
|
90
94
|
def objects_list_visible_items(self):
|
|
91
95
|
"""
|
|
@@ -108,6 +112,22 @@ class ObjectsListMixin:
|
|
|
108
112
|
self.click_button('#default-filter button[type="submit"]')
|
|
109
113
|
|
|
110
114
|
|
|
115
|
+
class ObjectDetailsMixin:
|
|
116
|
+
def assertPanelValue(self, panel_label, field_label, expected_value, exact_match=False):
|
|
117
|
+
"""
|
|
118
|
+
Find the proper panel and asserts if given value match rendered field value.
|
|
119
|
+
By default, it's not using the exact match, because on the UI we're often adding
|
|
120
|
+
additional tags, relationships or units.
|
|
121
|
+
"""
|
|
122
|
+
panel_xpath = f'//*[@id="main"]//div[@class="panel-heading"][contains(normalize-space(), "{panel_label}")]/following-sibling::table'
|
|
123
|
+
value = self.browser.find_by_xpath(f'{panel_xpath}//td[text()="{field_label}"]/following-sibling::td[1]').text
|
|
124
|
+
|
|
125
|
+
if exact_match:
|
|
126
|
+
self.assertEqual(value, str(expected_value))
|
|
127
|
+
else:
|
|
128
|
+
self.assertIn(str(expected_value), value)
|
|
129
|
+
|
|
130
|
+
|
|
111
131
|
class BulkOperationsMixin:
|
|
112
132
|
def confirm_bulk_delete_operation(self):
|
|
113
133
|
"""
|
|
@@ -196,6 +216,7 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
|
|
|
196
216
|
|
|
197
217
|
host = "0.0.0.0" # noqa: S104 # hardcoded-bind-all-interfaces -- false positive
|
|
198
218
|
selenium_host = SELENIUM_HOST # Docker: `nautobot`; else `host.docker.internal`
|
|
219
|
+
logged_in = False
|
|
199
220
|
|
|
200
221
|
@classmethod
|
|
201
222
|
def setUpClass(cls):
|
|
@@ -219,6 +240,10 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
|
|
|
219
240
|
def live_server_url(cls): # pylint: disable=no-self-argument
|
|
220
241
|
return f"http://{cls.selenium_host}:{cls.server_thread.port}"
|
|
221
242
|
|
|
243
|
+
def tearDown(self):
|
|
244
|
+
if self.logged_in:
|
|
245
|
+
self.logout()
|
|
246
|
+
|
|
222
247
|
@classmethod
|
|
223
248
|
def tearDownClass(cls):
|
|
224
249
|
"""Close down the browser after tests are ran."""
|
|
@@ -291,19 +316,27 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
|
|
|
291
316
|
self.browser.is_element_not_present_by_css(".loading-results", wait_time=5)
|
|
292
317
|
return search_box
|
|
293
318
|
|
|
319
|
+
def _select_select2_result(self):
|
|
320
|
+
found_results = self.browser.find_by_css(".select2-results li.select2-results__option")
|
|
321
|
+
# click the first found item if it's not `None`: special value to nullify field
|
|
322
|
+
if found_results.first.text != "None":
|
|
323
|
+
found_results.first.click()
|
|
324
|
+
else:
|
|
325
|
+
found_results[1].click()
|
|
326
|
+
|
|
294
327
|
def fill_select2_field(self, field_name, value):
|
|
295
328
|
"""
|
|
296
329
|
Helper function to fill a Select2 single selection field on add/edit forms.
|
|
297
330
|
"""
|
|
298
|
-
|
|
299
|
-
|
|
331
|
+
self._fill_select2_field(field_name, value)
|
|
332
|
+
self._select_select2_result()
|
|
300
333
|
|
|
301
334
|
def fill_filters_select2_field(self, field_name, value):
|
|
302
335
|
"""
|
|
303
336
|
Helper function to fill a Select2 single selection field on filters modals.
|
|
304
337
|
"""
|
|
305
338
|
self._fill_select2_field(field_name, value, search_box_class="select2-search select2-search--inline")
|
|
306
|
-
self.
|
|
339
|
+
self._select_select2_result()
|
|
307
340
|
|
|
308
341
|
def fill_select2_multiselect_field(self, field_name, value):
|
|
309
342
|
"""
|
|
@@ -327,6 +360,7 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
|
|
|
327
360
|
self.user.is_superuser = True
|
|
328
361
|
self.user.save()
|
|
329
362
|
self.login(self.user.username, self.password)
|
|
363
|
+
self.logged_in = True
|
|
330
364
|
|
|
331
365
|
|
|
332
366
|
class BulkOperationsTestCases:
|
|
@@ -367,10 +401,6 @@ class BulkOperationsTestCases:
|
|
|
367
401
|
self.login_as_superuser()
|
|
368
402
|
self.go_to_model_list_page()
|
|
369
403
|
|
|
370
|
-
def tearDown(self):
|
|
371
|
-
self.logout()
|
|
372
|
-
super().tearDown()
|
|
373
|
-
|
|
374
404
|
def go_to_model_list_page(self):
|
|
375
405
|
self.click_navbar_entry(*self.model_menu_path)
|
|
376
406
|
self.assertEqual(self.browser.url, self.live_server_url + reverse(f"{self.model_base_viewname}_list"))
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from io import StringIO
|
|
2
|
+
|
|
3
|
+
from django.core.management import call_command
|
|
4
|
+
import yaml
|
|
5
|
+
|
|
6
|
+
from nautobot.core.testing import TestCase
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ManagementCommandTestCase(TestCase):
|
|
10
|
+
"""Test case for core management commands."""
|
|
11
|
+
|
|
12
|
+
def setUp(self):
|
|
13
|
+
"""Initialize user and client."""
|
|
14
|
+
super().setUpNautobot()
|
|
15
|
+
self.user.is_superuser = True
|
|
16
|
+
self.user.is_staff = True
|
|
17
|
+
self.user.save()
|
|
18
|
+
self.client.force_login(self.user)
|
|
19
|
+
|
|
20
|
+
def test_generate_performance_test_endpoints(self):
|
|
21
|
+
"""Test the generate_performance_test_endpoints management command."""
|
|
22
|
+
out = StringIO()
|
|
23
|
+
call_command("generate_performance_test_endpoints", stdout=out)
|
|
24
|
+
endpoints_dict = yaml.safe_load(out.getvalue())["endpoints"]
|
|
25
|
+
# status_code_to_endpoints = collections.defaultdict(list)
|
|
26
|
+
for view_name, value in endpoints_dict.items():
|
|
27
|
+
for endpoint in value:
|
|
28
|
+
response = self.client.get(endpoint, follow=True)
|
|
29
|
+
self.assertHttpStatus(
|
|
30
|
+
response, 200, f"{view_name}: {endpoint} returns status Code {response.status_code} instead of 200"
|
|
31
|
+
)
|
|
@@ -273,10 +273,25 @@ class GetFooForModelTest(TestCase):
|
|
|
273
273
|
"""
|
|
274
274
|
Test that `get_model_for_view_name` returns the appropriate Model, if the colon separated view name provided.
|
|
275
275
|
"""
|
|
276
|
-
with self.subTest("Test core view."):
|
|
276
|
+
with self.subTest("Test core UI view."):
|
|
277
277
|
self.assertEqual(lookup.get_model_for_view_name("dcim:device_list"), dcim_models.Device)
|
|
278
|
-
|
|
278
|
+
self.assertEqual(lookup.get_model_for_view_name("dcim:device"), dcim_models.Device)
|
|
279
|
+
with self.subTest("Test app UI view."):
|
|
279
280
|
self.assertEqual(lookup.get_model_for_view_name("plugins:example_app:examplemodel_list"), ExampleModel)
|
|
281
|
+
self.assertEqual(lookup.get_model_for_view_name("plugins:example_app:examplemodel"), ExampleModel)
|
|
282
|
+
with self.subTest("Test core API view."):
|
|
283
|
+
self.assertEqual(lookup.get_model_for_view_name("dcim-api:device-list"), dcim_models.Device)
|
|
284
|
+
self.assertEqual(lookup.get_model_for_view_name("dcim-api:device-detail"), dcim_models.Device)
|
|
285
|
+
with self.subTest("Test app API view."):
|
|
286
|
+
self.assertEqual(
|
|
287
|
+
lookup.get_model_for_view_name("plugins-api:example_app-api:examplemodel-detail"), ExampleModel
|
|
288
|
+
)
|
|
289
|
+
self.assertEqual(
|
|
290
|
+
lookup.get_model_for_view_name("plugins-api:example_app-api:examplemodel-list"), ExampleModel
|
|
291
|
+
)
|
|
292
|
+
with self.subTest("Test unconventional model views."):
|
|
293
|
+
self.assertEqual(lookup.get_model_for_view_name("extras-api:contenttype-detail"), ContentType)
|
|
294
|
+
self.assertEqual(lookup.get_model_for_view_name("users-api:group-detail"), Group)
|
|
280
295
|
with self.subTest("Test unexpected view."):
|
|
281
296
|
with self.assertRaises(ValueError) as err:
|
|
282
297
|
lookup.get_model_for_view_name("unknown:plugins:example_app:examplemodel_list")
|
nautobot/core/utils/lookup.py
CHANGED
|
@@ -250,6 +250,11 @@ def get_model_for_view_name(view_name):
|
|
|
250
250
|
Return the model class associated with the given view_name e.g. "circuits:circuit_detail", "dcim:device_list" and etc.
|
|
251
251
|
If the app_label or model_name contained by the given view_name is invalid, this will return `None`.
|
|
252
252
|
"""
|
|
253
|
+
if view_name == "users-api:group-detail":
|
|
254
|
+
return Group
|
|
255
|
+
if view_name == "extras-api:contenttype-detail":
|
|
256
|
+
return ContentType
|
|
257
|
+
|
|
253
258
|
split_view_name = view_name.split(":")
|
|
254
259
|
if len(split_view_name) == 2:
|
|
255
260
|
app_label, model_name = split_view_name # dcim, device_list
|
|
@@ -257,7 +262,13 @@ def get_model_for_view_name(view_name):
|
|
|
257
262
|
_, app_label, model_name = split_view_name # plugins, app_name, model_list
|
|
258
263
|
else:
|
|
259
264
|
raise ValueError(f"Unexpected View Name: {view_name}")
|
|
260
|
-
|
|
265
|
+
|
|
266
|
+
delimiter = "_"
|
|
267
|
+
if app_label.endswith("-api"):
|
|
268
|
+
app_label = app_label.replace("-api", "")
|
|
269
|
+
delimiter = "-"
|
|
270
|
+
|
|
271
|
+
model_name = model_name.split(delimiter)[0] # device
|
|
261
272
|
|
|
262
273
|
try:
|
|
263
274
|
model = apps.get_model(app_label=app_label, model_name=model_name)
|
nautobot/core/views/generic.py
CHANGED
|
@@ -214,8 +214,16 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
|
|
|
214
214
|
resolved_path = resolve(request.path)
|
|
215
215
|
list_url = f"{resolved_path.app_name}:{resolved_path.url_name}"
|
|
216
216
|
|
|
217
|
+
skip_user_and_global_default_saved_view = False
|
|
218
|
+
if self.filterset is not None:
|
|
219
|
+
skip_user_and_global_default_saved_view = get_filterable_params_from_filter_params(
|
|
220
|
+
request.GET.copy(),
|
|
221
|
+
self.non_filter_params,
|
|
222
|
+
self.filterset(),
|
|
223
|
+
)
|
|
224
|
+
|
|
217
225
|
# If the user clicks on the clear view button, we do not check for global or user defaults
|
|
218
|
-
if not clear_view and not request.GET.get("saved_view"):
|
|
226
|
+
if not skip_user_and_global_default_saved_view and not clear_view and not request.GET.get("saved_view"):
|
|
219
227
|
# Check if there is a default for this view for this specific user
|
|
220
228
|
if not isinstance(user, AnonymousUser):
|
|
221
229
|
try:
|
nautobot/core/views/mixins.py
CHANGED
|
@@ -723,8 +723,16 @@ class ObjectListViewMixin(NautobotViewSetMixin, mixins.ListModelMixin):
|
|
|
723
723
|
if response is not None:
|
|
724
724
|
return response
|
|
725
725
|
|
|
726
|
+
skip_user_and_global_default_saved_view = False
|
|
727
|
+
if self.filterset_class is not None:
|
|
728
|
+
skip_user_and_global_default_saved_view = get_filterable_params_from_filter_params(
|
|
729
|
+
request.GET.copy(),
|
|
730
|
+
self.non_filter_params,
|
|
731
|
+
self.filterset_class(),
|
|
732
|
+
)
|
|
733
|
+
|
|
726
734
|
# If the user clicks on the clear view button, we do not check for global or user defaults
|
|
727
|
-
if not clear_view and not request.GET.get("saved_view"):
|
|
735
|
+
if not skip_user_and_global_default_saved_view and not clear_view and not request.GET.get("saved_view"):
|
|
728
736
|
# Check if there is a default for this view for this specific user
|
|
729
737
|
app_label, model_name = queryset.model._meta.label.split(".")
|
|
730
738
|
view_name = f"{app_label}:{model_name.lower()}_list"
|
nautobot/dcim/api/views.py
CHANGED
|
@@ -74,6 +74,7 @@ from nautobot.dcim.models import (
|
|
|
74
74
|
)
|
|
75
75
|
from nautobot.extras.api.views import (
|
|
76
76
|
ConfigContextQuerySetMixin,
|
|
77
|
+
CustomFieldModelViewSet,
|
|
77
78
|
NautobotModelViewSet,
|
|
78
79
|
)
|
|
79
80
|
from nautobot.extras.choices import SecretsGroupAccessTypeChoices, SecretsGroupSecretTypeChoices
|
|
@@ -300,13 +301,13 @@ class DeviceTypeViewSet(NautobotModelViewSet):
|
|
|
300
301
|
#
|
|
301
302
|
|
|
302
303
|
|
|
303
|
-
class ConsolePortTemplateViewSet(
|
|
304
|
+
class ConsolePortTemplateViewSet(CustomFieldModelViewSet):
|
|
304
305
|
queryset = ConsolePortTemplate.objects.select_related("device_type__manufacturer", "module_type__manufacturer")
|
|
305
306
|
serializer_class = serializers.ConsolePortTemplateSerializer
|
|
306
307
|
filterset_class = filters.ConsolePortTemplateFilterSet
|
|
307
308
|
|
|
308
309
|
|
|
309
|
-
class ConsoleServerPortTemplateViewSet(
|
|
310
|
+
class ConsoleServerPortTemplateViewSet(CustomFieldModelViewSet):
|
|
310
311
|
queryset = ConsoleServerPortTemplate.objects.select_related(
|
|
311
312
|
"device_type__manufacturer", "module_type__manufacturer"
|
|
312
313
|
)
|
|
@@ -314,43 +315,43 @@ class ConsoleServerPortTemplateViewSet(NautobotModelViewSet):
|
|
|
314
315
|
filterset_class = filters.ConsoleServerPortTemplateFilterSet
|
|
315
316
|
|
|
316
317
|
|
|
317
|
-
class PowerPortTemplateViewSet(
|
|
318
|
+
class PowerPortTemplateViewSet(CustomFieldModelViewSet):
|
|
318
319
|
queryset = PowerPortTemplate.objects.select_related("device_type__manufacturer", "module_type__manufacturer")
|
|
319
320
|
serializer_class = serializers.PowerPortTemplateSerializer
|
|
320
321
|
filterset_class = filters.PowerPortTemplateFilterSet
|
|
321
322
|
|
|
322
323
|
|
|
323
|
-
class PowerOutletTemplateViewSet(
|
|
324
|
+
class PowerOutletTemplateViewSet(CustomFieldModelViewSet):
|
|
324
325
|
queryset = PowerOutletTemplate.objects.select_related("device_type__manufacturer", "module_type__manufacturer")
|
|
325
326
|
serializer_class = serializers.PowerOutletTemplateSerializer
|
|
326
327
|
filterset_class = filters.PowerOutletTemplateFilterSet
|
|
327
328
|
|
|
328
329
|
|
|
329
|
-
class InterfaceTemplateViewSet(
|
|
330
|
+
class InterfaceTemplateViewSet(CustomFieldModelViewSet):
|
|
330
331
|
queryset = InterfaceTemplate.objects.select_related("device_type__manufacturer", "module_type__manufacturer")
|
|
331
332
|
serializer_class = serializers.InterfaceTemplateSerializer
|
|
332
333
|
filterset_class = filters.InterfaceTemplateFilterSet
|
|
333
334
|
|
|
334
335
|
|
|
335
|
-
class FrontPortTemplateViewSet(
|
|
336
|
+
class FrontPortTemplateViewSet(CustomFieldModelViewSet):
|
|
336
337
|
queryset = FrontPortTemplate.objects.select_related("device_type__manufacturer", "module_type__manufacturer")
|
|
337
338
|
serializer_class = serializers.FrontPortTemplateSerializer
|
|
338
339
|
filterset_class = filters.FrontPortTemplateFilterSet
|
|
339
340
|
|
|
340
341
|
|
|
341
|
-
class RearPortTemplateViewSet(
|
|
342
|
+
class RearPortTemplateViewSet(CustomFieldModelViewSet):
|
|
342
343
|
queryset = RearPortTemplate.objects.select_related("device_type__manufacturer", "module_type__manufacturer")
|
|
343
344
|
serializer_class = serializers.RearPortTemplateSerializer
|
|
344
345
|
filterset_class = filters.RearPortTemplateFilterSet
|
|
345
346
|
|
|
346
347
|
|
|
347
|
-
class DeviceBayTemplateViewSet(
|
|
348
|
+
class DeviceBayTemplateViewSet(CustomFieldModelViewSet):
|
|
348
349
|
queryset = DeviceBayTemplate.objects.select_related("device_type__manufacturer")
|
|
349
350
|
serializer_class = serializers.DeviceBayTemplateSerializer
|
|
350
351
|
filterset_class = filters.DeviceBayTemplateFilterSet
|
|
351
352
|
|
|
352
353
|
|
|
353
|
-
class ModuleBayTemplateViewSet(
|
|
354
|
+
class ModuleBayTemplateViewSet(CustomFieldModelViewSet):
|
|
354
355
|
queryset = ModuleBayTemplate.objects.select_related("device_type__manufacturer", "module_type__manufacturer")
|
|
355
356
|
serializer_class = serializers.ModuleBayTemplateSerializer
|
|
356
357
|
filterset_class = filters.ModuleBayTemplateFilterSet
|
|
@@ -831,7 +832,7 @@ class VirtualDeviceContextViewSet(NautobotModelViewSet):
|
|
|
831
832
|
filterset_class = filters.VirtualDeviceContextFilterSet
|
|
832
833
|
|
|
833
834
|
|
|
834
|
-
class InterfaceVDCAssignmentViewSet(
|
|
835
|
+
class InterfaceVDCAssignmentViewSet(ModelViewSet):
|
|
835
836
|
queryset = InterfaceVDCAssignment.objects.all()
|
|
836
837
|
serializer_class = serializers.InterfaceVDCAssignmentSerializer
|
|
837
838
|
filterset_class = filters.InterfaceVDCAssignmentFilterSet
|
nautobot/dcim/forms.py
CHANGED
|
@@ -17,6 +17,7 @@ from nautobot.core.forms import (
|
|
|
17
17
|
AutoPositionPatternField,
|
|
18
18
|
BootstrapMixin,
|
|
19
19
|
BulkEditNullBooleanSelect,
|
|
20
|
+
ClearableFileInput,
|
|
20
21
|
ColorSelect,
|
|
21
22
|
CommentField,
|
|
22
23
|
DatePicker,
|
|
@@ -850,12 +851,8 @@ class DeviceTypeForm(NautobotModelForm):
|
|
|
850
851
|
widgets = {
|
|
851
852
|
"subdevice_role": StaticSelect2(),
|
|
852
853
|
# Exclude SVG images (unsupported by PIL)
|
|
853
|
-
"front_image":
|
|
854
|
-
|
|
855
|
-
),
|
|
856
|
-
"rear_image": forms.ClearableFileInput(
|
|
857
|
-
attrs={"accept": "image/bmp,image/gif,image/jpeg,image/png,image/tiff"}
|
|
858
|
-
),
|
|
854
|
+
"front_image": ClearableFileInput(attrs={"accept": "image/bmp,image/gif,image/jpeg,image/png,image/tiff"}),
|
|
855
|
+
"rear_image": ClearableFileInput(attrs={"accept": "image/bmp,image/gif,image/jpeg,image/png,image/tiff"}),
|
|
859
856
|
}
|
|
860
857
|
|
|
861
858
|
|
nautobot/dcim/models/devices.py
CHANGED
|
@@ -1419,11 +1419,10 @@ class Controller(PrimaryModel):
|
|
|
1419
1419
|
"controller_device": ("Cannot assign both a device and a device redundancy group to a controller."),
|
|
1420
1420
|
},
|
|
1421
1421
|
)
|
|
1422
|
-
|
|
1423
1422
|
if self.location:
|
|
1424
1423
|
if ContentType.objects.get_for_model(self) not in self.location.location_type.content_types.all():
|
|
1425
1424
|
raise ValidationError(
|
|
1426
|
-
{"location": f'
|
|
1425
|
+
{"location": f'Controllers may not associate to locations of type "{self.location.location_type}".'}
|
|
1427
1426
|
)
|
|
1428
1427
|
|
|
1429
1428
|
def get_capabilities_display(self):
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
{% for near_end, cable, far_end in traced_path %}
|
|
14
14
|
|
|
15
15
|
{# Near end #}
|
|
16
|
-
{% if near_end.device %}
|
|
17
|
-
{% include 'dcim/trace/device.html' with device=near_end.
|
|
16
|
+
{% if near_end.device or near_end.module %}
|
|
17
|
+
{% include 'dcim/trace/device.html' with device=near_end.parent %}
|
|
18
18
|
{% include 'dcim/trace/termination.html' with termination=near_end %}
|
|
19
19
|
{% elif near_end.power_panel %}
|
|
20
20
|
{% include 'dcim/trace/powerpanel.html' with powerpanel=near_end.power_panel %}
|
|
@@ -30,10 +30,10 @@
|
|
|
30
30
|
{% endif %}
|
|
31
31
|
|
|
32
32
|
{# Far end #}
|
|
33
|
-
{% if far_end.device %}
|
|
33
|
+
{% if far_end.device or far_end.module %}
|
|
34
34
|
{% include 'dcim/trace/termination.html' with termination=far_end %}
|
|
35
35
|
{% if forloop.last %}
|
|
36
|
-
{% include 'dcim/trace/device.html' with device=far_end.
|
|
36
|
+
{% include 'dcim/trace/device.html' with device=far_end.parent %}
|
|
37
37
|
{% endif %}
|
|
38
38
|
{% elif far_end.power_panel %}
|
|
39
39
|
{% include 'dcim/trace/termination.html' with termination=far_end %}
|
|
@@ -8,8 +8,13 @@
|
|
|
8
8
|
</div>
|
|
9
9
|
<table class="table table-hover panel-body attr-table">
|
|
10
10
|
<tr>
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
{% if object.device %}
|
|
12
|
+
<td>Device</td>
|
|
13
|
+
<td>{{ object.device|hyperlinked_object }}</td>
|
|
14
|
+
{% else %}
|
|
15
|
+
<td>Module</td>
|
|
16
|
+
<td>{{ object.module|hyperlinked_object }}</td>
|
|
17
|
+
{% endif %}
|
|
13
18
|
</tr>
|
|
14
19
|
<tr>
|
|
15
20
|
<td>Name</td>
|
|
@@ -49,8 +54,13 @@
|
|
|
49
54
|
</tr>
|
|
50
55
|
{% if object.connected_endpoint %}
|
|
51
56
|
<tr>
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
{% if object.connected_endpoint.device %}
|
|
58
|
+
<td>Device</td>
|
|
59
|
+
<td>{{ object.connected_endpoint.device|hyperlinked_object }}</td>
|
|
60
|
+
{% else %}
|
|
61
|
+
<td>Module</td>
|
|
62
|
+
<td>{{ object.connected_endpoint.module|hyperlinked_object }}</td>
|
|
63
|
+
{% endif %}
|
|
54
64
|
</tr>
|
|
55
65
|
<tr>
|
|
56
66
|
<td>Console Server Port</td>
|
|
@@ -8,8 +8,13 @@
|
|
|
8
8
|
</div>
|
|
9
9
|
<table class="table table-hover panel-body attr-table">
|
|
10
10
|
<tr>
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
{% if object.device %}
|
|
12
|
+
<td>Device</td>
|
|
13
|
+
<td>{{ object.device|hyperlinked_object }}</td>
|
|
14
|
+
{% else %}
|
|
15
|
+
<td>Module</td>
|
|
16
|
+
<td>{{ object.module|hyperlinked_object }}</td>
|
|
17
|
+
{% endif %}
|
|
13
18
|
</tr>
|
|
14
19
|
<tr>
|
|
15
20
|
<td>Name</td>
|
|
@@ -49,8 +54,13 @@
|
|
|
49
54
|
</tr>
|
|
50
55
|
{% if object.connected_endpoint %}
|
|
51
56
|
<tr>
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
{% if object.connected_endpoint.device %}
|
|
58
|
+
<td>Device</td>
|
|
59
|
+
<td>{{ object.connected_endpoint.device|hyperlinked_object }}</td>
|
|
60
|
+
{% else %}
|
|
61
|
+
<td>Module</td>
|
|
62
|
+
<td>{{ object.connected_endpoint.module|hyperlinked_object }}</td>
|
|
63
|
+
{% endif %}
|
|
54
64
|
</tr>
|
|
55
65
|
<tr>
|
|
56
66
|
<td>Console Port</td>
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
{% for iface in interfaces %}
|
|
25
25
|
<tr data-interface-name="{{ iface.name }}">
|
|
26
26
|
<td>{{ iface }}</td>
|
|
27
|
-
{% if iface.connected_endpoint.device %}
|
|
28
|
-
<td class="configured_device" data="{{ iface.connected_endpoint.
|
|
29
|
-
{{ iface.connected_endpoint.
|
|
27
|
+
{% if iface.connected_endpoint.device or iface.connected_endpoint.module %}
|
|
28
|
+
<td class="configured_device" data="{{ iface.connected_endpoint.parent }}" data-chassis="{{ iface.connected_endpoint.parent.virtual_chassis.name }}">
|
|
29
|
+
{{ iface.connected_endpoint.parent|hyperlinked_object }}
|
|
30
30
|
</td>
|
|
31
31
|
<td class="configured_interface" data-interface-name="{{ iface.connected_endpoint }}">
|
|
32
32
|
<span title="{{ iface.connected_endpoint.get_type_display }}">{{ iface.connected_endpoint }}</span>
|
|
@@ -8,8 +8,13 @@
|
|
|
8
8
|
</div>
|
|
9
9
|
<table class="table table-hover panel-body attr-table">
|
|
10
10
|
<tr>
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
{% if object.device %}
|
|
12
|
+
<td>Device</td>
|
|
13
|
+
<td>{{ object.device|hyperlinked_object }}</td>
|
|
14
|
+
{% else %}
|
|
15
|
+
<td>Module</td>
|
|
16
|
+
<td>{{ object.module|hyperlinked_object }}</td>
|
|
17
|
+
{% endif %}
|
|
13
18
|
</tr>
|
|
14
19
|
<tr>
|
|
15
20
|
<td>Name</td>
|
|
@@ -101,11 +101,16 @@
|
|
|
101
101
|
</a>
|
|
102
102
|
</td>
|
|
103
103
|
</tr>
|
|
104
|
-
{% if object.connected_endpoint.device %}
|
|
104
|
+
{% if object.connected_endpoint.device or object.connected_endpoint.module %}
|
|
105
105
|
{% with iface=object.connected_endpoint %}
|
|
106
106
|
<tr>
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
{% if iface.device %}
|
|
108
|
+
<td>Device</td>
|
|
109
|
+
<td>{{ iface.device|hyperlinked_object }}</td>
|
|
110
|
+
{% else %}
|
|
111
|
+
<td>Module</td>
|
|
112
|
+
<td>{{ iface.module|hyperlinked_object }}</td>
|
|
113
|
+
{% endif %}
|
|
109
114
|
</tr>
|
|
110
115
|
<tr>
|
|
111
116
|
<td>Interface</td>
|
|
@@ -201,7 +206,7 @@
|
|
|
201
206
|
<tbody>
|
|
202
207
|
{% for member in object.member_interfaces.all %}
|
|
203
208
|
<tr>
|
|
204
|
-
<td>{{ member.
|
|
209
|
+
<td>{{ member.parent|hyperlinked_object }}</td>
|
|
205
210
|
<td>{{ member|hyperlinked_object }}</td>
|
|
206
211
|
<td>
|
|
207
212
|
{{ member.get_type_display }}
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
<td>Connected Device</td>
|
|
40
40
|
<td>
|
|
41
41
|
{% if object.connected_endpoint %}
|
|
42
|
-
{{ object.connected_endpoint.
|
|
42
|
+
{{ object.connected_endpoint.parent|hyperlinked_object }}
|
|
43
43
|
({{ object.connected_endpoint }})
|
|
44
44
|
{% else %}
|
|
45
45
|
<span class="text-muted">None</span>
|
|
@@ -110,8 +110,13 @@
|
|
|
110
110
|
</tr>
|
|
111
111
|
{% if object.connected_endpoint %}
|
|
112
112
|
<tr>
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
{% if object.connected_endpoint.device %}
|
|
114
|
+
<td>Device</td>
|
|
115
|
+
<td>{{ object.connected_endpoint.device|hyperlinked_object }}</td>
|
|
116
|
+
{% else %}
|
|
117
|
+
<td>Module</td>
|
|
118
|
+
<td>{{ object.connected_endpoint.module|hyperlinked_object }}</td>
|
|
119
|
+
{% endif %}
|
|
115
120
|
</tr>
|
|
116
121
|
<tr>
|
|
117
122
|
<td>Power Port</td>
|
|
@@ -8,8 +8,13 @@
|
|
|
8
8
|
</div>
|
|
9
9
|
<table class="table table-hover panel-body attr-table">
|
|
10
10
|
<tr>
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
{% if object.device %}
|
|
12
|
+
<td>Device</td>
|
|
13
|
+
<td>{{ object.device|hyperlinked_object }}</td>
|
|
14
|
+
{% else %}
|
|
15
|
+
<td>Module</td>
|
|
16
|
+
<td>{{ object.module|hyperlinked_object }}</td>
|
|
17
|
+
{% endif %}
|
|
13
18
|
</tr>
|
|
14
19
|
<tr>
|
|
15
20
|
<td>Name</td>
|
|
@@ -57,8 +62,13 @@
|
|
|
57
62
|
</tr>
|
|
58
63
|
{% if object.connected_endpoint %}
|
|
59
64
|
<tr>
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
{% if object.connected_endpoint.device %}
|
|
66
|
+
<td>Device</td>
|
|
67
|
+
<td>{{ object.connected_endpoint.device|hyperlinked_object }}</td>
|
|
68
|
+
{% else %}
|
|
69
|
+
<td>Module</td>
|
|
70
|
+
<td>{{ object.connected_endpoint.module|hyperlinked_object }}</td>
|
|
71
|
+
{% endif %}
|
|
62
72
|
</tr>
|
|
63
73
|
<tr>
|
|
64
74
|
<td>Power Port</td>
|
|
@@ -8,8 +8,13 @@
|
|
|
8
8
|
</div>
|
|
9
9
|
<table class="table table-hover panel-body attr-table">
|
|
10
10
|
<tr>
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
{% if object.device %}
|
|
12
|
+
<td>Device</td>
|
|
13
|
+
<td>{{ object.device|hyperlinked_object }}</td>
|
|
14
|
+
{% else %}
|
|
15
|
+
<td>Module</td>
|
|
16
|
+
<td>{{ object.module|hyperlinked_object }}</td>
|
|
17
|
+
{% endif %}
|
|
13
18
|
</tr>
|
|
14
19
|
<tr>
|
|
15
20
|
<td>Name</td>
|
|
@@ -57,8 +62,13 @@
|
|
|
57
62
|
</tr>
|
|
58
63
|
{% if object.connected_endpoint %}
|
|
59
64
|
<tr>
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
{% if object.connected_endpoint.device %}
|
|
66
|
+
<td>Device</td>
|
|
67
|
+
<td>{{ object.connected_endpoint.device|hyperlinked_object }}</td>
|
|
68
|
+
{% else %}
|
|
69
|
+
<td>Module</td>
|
|
70
|
+
<td>{{ object.connected_endpoint.module|hyperlinked_object }}</td>
|
|
71
|
+
{% endif %}
|
|
62
72
|
</tr>
|
|
63
73
|
<tr>
|
|
64
74
|
<td>Power Outlet / Feed</td>
|
|
@@ -8,8 +8,13 @@
|
|
|
8
8
|
</div>
|
|
9
9
|
<table class="table table-hover panel-body attr-table">
|
|
10
10
|
<tr>
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
{% if object.device %}
|
|
12
|
+
<td>Device</td>
|
|
13
|
+
<td>{{ object.device|hyperlinked_object }}</td>
|
|
14
|
+
{% else %}
|
|
15
|
+
<td>Module</td>
|
|
16
|
+
<td>{{ object.module|hyperlinked_object }}</td>
|
|
17
|
+
{% endif %}
|
|
13
18
|
</tr>
|
|
14
19
|
<tr>
|
|
15
20
|
<td>Name</td>
|