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.

Files changed (132) hide show
  1. nautobot/circuits/templates/circuits/inc/circuit_termination.html +1 -1
  2. nautobot/circuits/tests/integration/test_circuit.py +135 -0
  3. nautobot/circuits/views.py +4 -1
  4. nautobot/cloud/api/views.py +3 -3
  5. nautobot/core/constants.py +0 -1
  6. nautobot/core/forms/__init__.py +2 -0
  7. nautobot/core/forms/forms.py +2 -1
  8. nautobot/core/forms/widgets.py +8 -0
  9. nautobot/core/management/commands/generate_performance_test_endpoints.py +268 -0
  10. nautobot/core/templates/generic/object_bulk_delete.html +1 -1
  11. nautobot/core/templates/generic/object_bulk_edit.html +1 -1
  12. nautobot/core/templates/generic/object_bulk_import.html +1 -1
  13. nautobot/core/templates/generic/object_create.html +5 -0
  14. nautobot/core/templates/generic/object_delete.html +1 -1
  15. nautobot/core/templates/generic/object_detail.html +1 -1
  16. nautobot/core/templates/generic/object_edit.html +1 -1
  17. nautobot/core/templates/inc/javascript.html +2 -0
  18. nautobot/core/templates/widgets/clearable_file.html +5 -0
  19. nautobot/core/templatetags/helpers.py +3 -3
  20. nautobot/core/testing/integration.py +37 -7
  21. nautobot/core/tests/test_commands.py +31 -0
  22. nautobot/core/tests/test_utils.py +17 -2
  23. nautobot/core/utils/lookup.py +12 -1
  24. nautobot/core/views/generic.py +9 -1
  25. nautobot/core/views/mixins.py +9 -1
  26. nautobot/dcim/api/views.py +11 -10
  27. nautobot/dcim/forms.py +3 -6
  28. nautobot/dcim/models/devices.py +1 -2
  29. nautobot/dcim/templates/dcim/cable_trace.html +4 -4
  30. nautobot/dcim/templates/dcim/consoleport.html +14 -4
  31. nautobot/dcim/templates/dcim/consoleserverport.html +14 -4
  32. nautobot/dcim/templates/dcim/device/lldp_neighbors.html +3 -3
  33. nautobot/dcim/templates/dcim/frontport.html +7 -2
  34. nautobot/dcim/templates/dcim/interface.html +9 -4
  35. nautobot/dcim/templates/dcim/powerfeed.html +8 -3
  36. nautobot/dcim/templates/dcim/poweroutlet.html +14 -4
  37. nautobot/dcim/templates/dcim/powerport.html +14 -4
  38. nautobot/dcim/templates/dcim/rearport.html +7 -2
  39. nautobot/dcim/tests/integration/test_fileinputpicker.py +87 -0
  40. nautobot/dcim/tests/test_models.py +1 -1
  41. nautobot/extras/api/views.py +2 -2
  42. nautobot/extras/forms/forms.py +4 -0
  43. nautobot/extras/jobs.py +8 -1
  44. nautobot/extras/templates/extras/job.html +1 -0
  45. nautobot/extras/tests/test_dynamicgroups.py +14 -0
  46. nautobot/extras/tests/test_views.py +197 -9
  47. nautobot/extras/utils.py +30 -0
  48. nautobot/extras/views.py +29 -14
  49. nautobot/ipam/api/views.py +3 -3
  50. nautobot/ipam/forms.py +2 -6
  51. nautobot/project-static/bootstrap-filestyle-1.2.3/bootstrap-filestyle.min.js +11 -0
  52. nautobot/project-static/docs/apps/index.html +1 -1
  53. nautobot/project-static/docs/apps/nautobot-apps.html +1 -1
  54. nautobot/project-static/docs/development/apps/api/models/graphql.html +9 -9
  55. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +2 -2
  56. nautobot/project-static/docs/development/apps/api/setup.html +1 -1
  57. nautobot/project-static/docs/development/apps/migration/code-updates.html +6 -5
  58. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +2 -2
  59. nautobot/project-static/docs/development/apps/migration/from-v1.html +3 -3
  60. nautobot/project-static/docs/development/core/best-practices.html +1 -1
  61. nautobot/project-static/docs/development/core/bootstrap-ui.html +1 -1
  62. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +7 -7
  63. nautobot/project-static/docs/development/core/getting-started.html +2 -2
  64. nautobot/project-static/docs/development/core/index.html +1 -1
  65. nautobot/project-static/docs/development/core/minikube-dev-environment-for-k8s-jobs.html +3 -3
  66. nautobot/project-static/docs/development/core/model-checklist.html +1 -1
  67. nautobot/project-static/docs/development/core/navigation-menu.html +1 -1
  68. nautobot/project-static/docs/development/core/release-checklist.html +1 -1
  69. nautobot/project-static/docs/development/core/settings.html +1 -1
  70. nautobot/project-static/docs/development/core/style-guide.html +4 -4
  71. nautobot/project-static/docs/development/jobs/index.html +8 -1
  72. nautobot/project-static/docs/development/jobs/migration/from-v1.html +3 -2
  73. nautobot/project-static/docs/index.html +3 -2
  74. nautobot/project-static/docs/objects.inv +0 -0
  75. nautobot/project-static/docs/overview/application_stack.html +2 -2
  76. nautobot/project-static/docs/release-notes/version-1.0.html +2 -2
  77. nautobot/project-static/docs/release-notes/version-1.1.html +2 -2
  78. nautobot/project-static/docs/release-notes/version-1.2.html +3 -3
  79. nautobot/project-static/docs/release-notes/version-1.3.html +1 -1
  80. nautobot/project-static/docs/release-notes/version-1.4.html +17 -17
  81. nautobot/project-static/docs/release-notes/version-1.5.html +8 -8
  82. nautobot/project-static/docs/release-notes/version-1.6.html +4 -4
  83. nautobot/project-static/docs/release-notes/version-2.0.html +10 -10
  84. nautobot/project-static/docs/release-notes/version-2.1.html +7 -7
  85. nautobot/project-static/docs/release-notes/version-2.2.html +1 -1
  86. nautobot/project-static/docs/release-notes/version-2.3.html +4 -4
  87. nautobot/project-static/docs/release-notes/version-2.4.html +188 -0
  88. nautobot/project-static/docs/search/search_index.json +1 -1
  89. nautobot/project-static/docs/sitemap.xml +290 -290
  90. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  91. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +3 -3
  92. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +4 -4
  93. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +1 -1
  94. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +3 -3
  95. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +5 -5
  96. nautobot/project-static/docs/user-guide/administration/guides/docker.html +3 -3
  97. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +1 -1
  98. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +4 -4
  99. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +15 -15
  100. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +2 -2
  101. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +1 -1
  102. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +1 -1
  103. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +6 -6
  104. nautobot/project-static/docs/user-guide/administration/installation/services.html +1 -1
  105. nautobot/project-static/docs/user-guide/administration/security/index.html +1 -1
  106. nautobot/project-static/docs/user-guide/administration/security/notices.html +1 -0
  107. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +1 -1
  108. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +11 -8
  109. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +12 -12
  110. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +1 -1
  111. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +1 -1
  112. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +3 -3
  113. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobqueue.html +2 -2
  114. nautobot/project-static/docs/user-guide/platform-functionality/jobs/kubernetes-job-support.html +6 -6
  115. nautobot/project-static/js/dropdown.js +28 -0
  116. nautobot/tenancy/forms.py +9 -0
  117. nautobot/tenancy/templates/tenancy/tenant_create.html +21 -0
  118. nautobot/tenancy/templates/tenancy/tenant_edit.html +2 -21
  119. nautobot/tenancy/templates/tenancy/tenantgroup.html +2 -44
  120. nautobot/tenancy/templates/tenancy/tenantgroup_retrieve.html +1 -0
  121. nautobot/tenancy/tests/test_views.py +5 -1
  122. nautobot/tenancy/urls.py +7 -79
  123. nautobot/tenancy/views.py +51 -80
  124. nautobot/wireless/api/serializers.py +6 -1
  125. nautobot/wireless/api/views.py +3 -3
  126. nautobot/wireless/tests/test_api.py +5 -0
  127. {nautobot-2.4.2.dist-info → nautobot-2.4.3.dist-info}/METADATA +8 -8
  128. {nautobot-2.4.2.dist-info → nautobot-2.4.3.dist-info}/RECORD +132 -123
  129. {nautobot-2.4.2.dist-info → nautobot-2.4.3.dist-info}/LICENSE.txt +0 -0
  130. {nautobot-2.4.2.dist-info → nautobot-2.4.3.dist-info}/NOTICE +0 -0
  131. {nautobot-2.4.2.dist-info → nautobot-2.4.3.dist-info}/WHEEL +0 -0
  132. {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
- search_box = self._fill_select2_field(field_name, value)
299
- search_box.first.type(Keys.ENTER)
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.browser.find_by_xpath(f"//li[contains(@class, 'select2-results__option') and text()='{value}']").click()
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
- with self.subTest("Test app view."):
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")
@@ -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
- model_name = model_name.split("_")[0] # device
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)
@@ -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:
@@ -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"
@@ -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(NautobotModelViewSet):
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(NautobotModelViewSet):
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(NautobotModelViewSet):
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(NautobotModelViewSet):
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(NautobotModelViewSet):
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(NautobotModelViewSet):
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(NautobotModelViewSet):
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(NautobotModelViewSet):
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(NautobotModelViewSet):
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(NautobotModelViewSet):
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": forms.ClearableFileInput(
854
- attrs={"accept": "image/bmp,image/gif,image/jpeg,image/png,image/tiff"}
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
 
@@ -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'Devices may not associate to locations of type "{self.location.location_type}".'}
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.device %}
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.device %}
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
- <td>Device</td>
12
- <td>{{ object.device|hyperlinked_object }}</td>
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
- <td>Device</td>
53
- <td>{{ object.connected_endpoint.device|hyperlinked_object }}</td>
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
- <td>Device</td>
12
- <td>{{ object.device|hyperlinked_object }}</td>
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
- <td>Device</td>
53
- <td>{{ object.connected_endpoint.device|hyperlinked_object }}</td>
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.device }}" data-chassis="{{ iface.connected_endpoint.device.virtual_chassis.name }}">
29
- {{ iface.connected_endpoint.device|hyperlinked_object }}
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
- <td>Device</td>
12
- <td>{{ object.device|hyperlinked_object }}</td>
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
- <td>Device</td>
108
- <td>{{ iface.device|hyperlinked_object }}</td>
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.device|hyperlinked_object }}</td>
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.device|hyperlinked_object }}
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
- <td>Device</td>
114
- <td>{{ object.connected_endpoint.device|hyperlinked_object }}</td>
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
- <td>Device</td>
12
- <td>{{ object.device|hyperlinked_object }}</td>
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
- <td>Device</td>
61
- <td>{{ object.connected_endpoint.device|hyperlinked_object }}</td>
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
- <td>Device</td>
12
- <td>{{ object.device|hyperlinked_object }}</td>
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
- <td>Device</td>
61
- <td>{{ object.connected_endpoint.device|hyperlinked_object }}</td>
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
- <td>Device</td>
12
- <td>{{ object.device|hyperlinked_object }}</td>
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>