nautobot 3.0.0a3__py3-none-any.whl → 3.0.0rc1__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.
Files changed (388) hide show
  1. nautobot/apps/choices.py +4 -0
  2. nautobot/apps/ui.py +4 -0
  3. nautobot/apps/utils.py +8 -0
  4. nautobot/circuits/tests/integration/test_circuits_bulk_operations.py +0 -3
  5. nautobot/circuits/views.py +6 -2
  6. nautobot/core/api/serializers.py +1 -1
  7. nautobot/core/api/urls.py +1 -0
  8. nautobot/core/api/views.py +4 -0
  9. nautobot/core/choices.py +1 -1
  10. nautobot/core/cli/bootstrap_v3_to_v5.py +36 -13
  11. nautobot/core/cli/migrate_deprecated_templates.py +36 -9
  12. nautobot/core/filters.py +4 -0
  13. nautobot/core/forms/__init__.py +2 -0
  14. nautobot/core/forms/widgets.py +21 -2
  15. nautobot/core/jobs/__init__.py +56 -0
  16. nautobot/core/management/commands/generate_test_data.py +3 -3
  17. nautobot/core/models/__init__.py +11 -0
  18. nautobot/core/models/utils.py +1 -1
  19. nautobot/core/settings.py +17 -7
  20. nautobot/core/settings.yaml +4 -26
  21. nautobot/core/templates/admin/base.html +1 -2
  22. nautobot/core/templates/admin/change_list.html +9 -12
  23. nautobot/core/templates/base_django.html +1 -2
  24. nautobot/core/templates/components/panel/header_extra_content_table.html +1 -1
  25. nautobot/core/templates/components/tab/content_wrapper.html +4 -4
  26. nautobot/core/templates/echarts/echarts.html +21 -8
  27. nautobot/core/templates/generic/object_bulk_create.html +2 -2
  28. nautobot/core/templates/generic/object_bulk_delete.html +1 -1
  29. nautobot/core/templates/generic/object_bulk_edit.html +1 -1
  30. nautobot/core/templates/generic/object_bulk_import.html +1 -1
  31. nautobot/core/templates/generic/object_delete.html +1 -1
  32. nautobot/core/templates/generic/object_detail.html +1 -1
  33. nautobot/core/templates/generic/object_edit.html +1 -1
  34. nautobot/core/templates/generic/object_retrieve.html +2 -2
  35. nautobot/core/templates/graphene/graphiql.html +0 -1
  36. nautobot/core/templates/inc/footer.html +3 -1
  37. nautobot/core/templates/inc/header.html +10 -0
  38. nautobot/core/templates/inc/media.html +14 -0
  39. nautobot/core/templates/inc/nav_menu.html +1 -8
  40. nautobot/core/templates/inc/object_details_advanced_panel.html +2 -2
  41. nautobot/core/templates/nautobot_config.py.j2 +0 -6
  42. nautobot/core/templates/rest_framework/api.html +103 -2
  43. nautobot/core/templates/utilities/templatetags/filter_form_drawer.html +33 -0
  44. nautobot/core/templates/utilities/theme_preview.html +3 -0
  45. nautobot/core/templates/widgets/number_input_with_choices.html +44 -0
  46. nautobot/core/templatetags/helpers.py +24 -12
  47. nautobot/core/testing/integration.py +24 -13
  48. nautobot/core/testing/utils.py +18 -4
  49. nautobot/core/testing/views.py +104 -17
  50. nautobot/core/tests/integration/test_filters.py +48 -11
  51. nautobot/core/tests/integration/test_theme.py +22 -21
  52. nautobot/core/tests/nautobot_config.py +3 -0
  53. nautobot/core/tests/runner.py +1 -2
  54. nautobot/core/tests/test_breadcrumbs.py +21 -21
  55. nautobot/core/tests/test_jobs.py +73 -6
  56. nautobot/core/tests/test_renderers.py +59 -0
  57. nautobot/core/tests/test_settings_schema.py +1 -0
  58. nautobot/core/tests/test_templatetags_helpers.py +9 -0
  59. nautobot/core/tests/test_titles.py +0 -16
  60. nautobot/core/tests/test_ui.py +122 -3
  61. nautobot/core/tests/test_utils.py +41 -1
  62. nautobot/core/ui/breadcrumbs.py +68 -17
  63. nautobot/core/ui/bulk_buttons.py +1 -1
  64. nautobot/core/ui/choices.py +49 -65
  65. nautobot/core/ui/echarts.py +15 -20
  66. nautobot/core/ui/object_detail.py +54 -46
  67. nautobot/core/ui/titles.py +3 -6
  68. nautobot/core/urls.py +8 -8
  69. nautobot/core/utils/filtering.py +11 -1
  70. nautobot/core/utils/lookup.py +46 -0
  71. nautobot/core/views/mixins.py +31 -20
  72. nautobot/core/views/renderers.py +2 -3
  73. nautobot/data_validation/migrations/0002_data_migration_from_app.py +3 -2
  74. nautobot/dcim/api/serializers.py +3 -0
  75. nautobot/dcim/choices.py +49 -0
  76. nautobot/dcim/constants.py +7 -0
  77. nautobot/dcim/factory.py +1 -1
  78. nautobot/dcim/filters.py +13 -1
  79. nautobot/dcim/forms.py +89 -3
  80. nautobot/dcim/migrations/0075_interface_duplex_interface_speed_and_more.py +32 -0
  81. nautobot/dcim/migrations/{0075_add_deviceclusterassignment.py → 0076_add_deviceclusterassignment.py} +1 -1
  82. nautobot/dcim/migrations/{0076_device_cluster_to_clusters_data_migration.py → 0077_device_cluster_to_clusters_data_migration.py} +1 -1
  83. nautobot/dcim/migrations/{0077_remove_device_cluster.py → 0078_remove_device_cluster.py} +1 -1
  84. nautobot/dcim/migrations/{0078_remove_device_location_tenant_name_uniqueness.py → 0079_remove_device_location_tenant_name_uniqueness.py} +1 -1
  85. nautobot/dcim/migrations/{0079_device_name_data_migration.py → 0080_device_name_data_migration.py} +1 -1
  86. nautobot/dcim/migrations/0081_alter_device_device_redundancy_group_priority_and_more.py +25 -0
  87. nautobot/dcim/models/device_component_templates.py +33 -1
  88. nautobot/dcim/models/device_components.py +22 -1
  89. nautobot/dcim/models/devices.py +17 -4
  90. nautobot/dcim/tables/devices.py +15 -0
  91. nautobot/dcim/tables/devicetypes.py +8 -1
  92. nautobot/dcim/tables/racks.py +0 -2
  93. nautobot/dcim/tables/template_code.py +1 -1
  94. nautobot/dcim/templates/dcim/cable_trace.html +0 -2
  95. nautobot/dcim/templates/dcim/consoleport.html +1 -1
  96. nautobot/dcim/templates/dcim/consoleserverport.html +1 -1
  97. nautobot/dcim/templates/dcim/devicebay.html +1 -1
  98. nautobot/dcim/templates/dcim/frontport.html +1 -1
  99. nautobot/dcim/templates/dcim/inc/devicetype_component_table.html +1 -1
  100. nautobot/dcim/templates/dcim/inc/moduletype_component_table.html +1 -1
  101. nautobot/dcim/templates/dcim/inc/rack_elevation.html +1 -1
  102. nautobot/dcim/templates/dcim/interface.html +9 -1
  103. nautobot/dcim/templates/dcim/interface_edit.html +2 -0
  104. nautobot/dcim/templates/dcim/inventoryitem.html +1 -1
  105. nautobot/dcim/templates/dcim/module_consoleports.html +1 -1
  106. nautobot/dcim/templates/dcim/module_consoleserverports.html +1 -1
  107. nautobot/dcim/templates/dcim/module_frontports.html +1 -1
  108. nautobot/dcim/templates/dcim/module_interfaces.html +1 -1
  109. nautobot/dcim/templates/dcim/module_modulebays.html +1 -1
  110. nautobot/dcim/templates/dcim/module_poweroutlets.html +1 -1
  111. nautobot/dcim/templates/dcim/module_powerports.html +1 -1
  112. nautobot/dcim/templates/dcim/module_rearports.html +1 -1
  113. nautobot/dcim/templates/dcim/moduletype_list.html +2 -2
  114. nautobot/dcim/templates/dcim/poweroutlet.html +1 -1
  115. nautobot/dcim/templates/dcim/powerport.html +1 -1
  116. nautobot/dcim/templates/dcim/rack_elevation_list.html +1 -1
  117. nautobot/dcim/templates/dcim/rack_retrieve.html +0 -11
  118. nautobot/dcim/templates/dcim/rearport.html +1 -1
  119. nautobot/dcim/templates/dcim/trace/cable.html +1 -1
  120. nautobot/dcim/templates/dcim/virtualchassis_update.html +1 -1
  121. nautobot/dcim/tests/integration/test_controller.py +3 -6
  122. nautobot/dcim/tests/integration/test_controller_managed_device_group.py +1 -5
  123. nautobot/dcim/tests/integration/test_create_device.py +0 -2
  124. nautobot/dcim/tests/integration/test_device_bulk_operations.py +1 -3
  125. nautobot/dcim/tests/integration/test_fileinputpicker.py +6 -10
  126. nautobot/dcim/tests/integration/test_location_bulk_operations.py +0 -2
  127. nautobot/dcim/tests/integration/test_module_bay_position.py +3 -4
  128. nautobot/dcim/tests/test_api.py +186 -6
  129. nautobot/dcim/tests/test_filters.py +43 -1
  130. nautobot/dcim/tests/test_forms.py +110 -8
  131. nautobot/dcim/tests/test_graphql.py +44 -1
  132. nautobot/dcim/tests/test_models.py +265 -0
  133. nautobot/dcim/tests/test_tables.py +160 -0
  134. nautobot/dcim/tests/test_views.py +69 -7
  135. nautobot/dcim/views.py +232 -126
  136. nautobot/extras/api/views.py +51 -44
  137. nautobot/extras/datasources/git.py +3 -1
  138. nautobot/extras/filters.py +19 -2
  139. nautobot/extras/forms/forms.py +9 -2
  140. nautobot/extras/jobs.py +2 -0
  141. nautobot/extras/jobs_ui.py +4 -3
  142. nautobot/extras/management/__init__.py +2 -0
  143. nautobot/extras/management/commands/refresh_dynamic_group_member_caches.py +4 -1
  144. nautobot/extras/migrations/0131_configcontext_device_families.py +18 -0
  145. nautobot/extras/models/approvals.py +11 -1
  146. nautobot/extras/models/change_logging.py +4 -0
  147. nautobot/extras/models/jobs.py +1 -3
  148. nautobot/extras/models/models.py +10 -2
  149. nautobot/extras/plugins/marketplace_manifest.yml +49 -1
  150. nautobot/extras/plugins/views.py +0 -5
  151. nautobot/extras/querysets.py +8 -0
  152. nautobot/extras/tables.py +12 -0
  153. nautobot/extras/templates/django_ajax_tables/ajax_wrapper.html +2 -0
  154. nautobot/extras/templates/extras/configcontext_update.html +1 -0
  155. nautobot/extras/templates/extras/dynamicgroup_update.html +1 -1
  156. nautobot/extras/templates/extras/objectchange_retrieve.html +0 -2
  157. nautobot/extras/templates/extras/plugin_detail.html +3 -3
  158. nautobot/extras/templates/extras/secret_create.html +1 -1
  159. nautobot/extras/tests/integration/test_computedfields.py +8 -9
  160. nautobot/extras/tests/integration/test_customfields.py +1 -3
  161. nautobot/extras/tests/integration/test_dynamicgroups.py +7 -8
  162. nautobot/extras/tests/integration/test_relationships.py +0 -2
  163. nautobot/extras/tests/test_api.py +63 -0
  164. nautobot/extras/tests/test_changelog.py +24 -2
  165. nautobot/extras/tests/test_filters.py +36 -3
  166. nautobot/extras/tests/test_models.py +38 -2
  167. nautobot/extras/tests/test_utils.py +3 -4
  168. nautobot/extras/tests/test_views.py +22 -83
  169. nautobot/extras/urls.py +0 -14
  170. nautobot/extras/views.py +83 -52
  171. nautobot/ipam/filters.py +26 -0
  172. nautobot/ipam/tables.py +6 -0
  173. nautobot/ipam/templates/ipam/namespace_ip_addresses.html +1 -1
  174. nautobot/ipam/templates/ipam/namespace_prefixes.html +1 -1
  175. nautobot/ipam/templates/ipam/namespace_vrfs.html +1 -1
  176. nautobot/ipam/tests/test_filters.py +26 -1
  177. nautobot/ipam/tests/test_models.py +1 -1
  178. nautobot/ipam/views.py +9 -7
  179. nautobot/load_balancers/__init__.py +0 -0
  180. nautobot/load_balancers/api/__init__.py +1 -0
  181. nautobot/load_balancers/api/serializers.py +75 -0
  182. nautobot/load_balancers/api/urls.py +23 -0
  183. nautobot/load_balancers/api/views.py +61 -0
  184. nautobot/load_balancers/apps.py +17 -0
  185. nautobot/load_balancers/choices.py +167 -0
  186. nautobot/load_balancers/filters.py +225 -0
  187. nautobot/load_balancers/forms.py +532 -0
  188. nautobot/load_balancers/management/commands/__init__.py +0 -0
  189. nautobot/load_balancers/management/commands/generate_load_balancer_models_test_data.py +38 -0
  190. nautobot/load_balancers/migrations/0001_initial.py +465 -0
  191. nautobot/load_balancers/migrations/0002_create_default_statuses_pool_members.py +31 -0
  192. nautobot/load_balancers/migrations/__init__.py +0 -0
  193. nautobot/load_balancers/models.py +423 -0
  194. nautobot/load_balancers/navigation.py +80 -0
  195. nautobot/load_balancers/tables.py +255 -0
  196. nautobot/load_balancers/tests/__init__.py +474 -0
  197. nautobot/load_balancers/tests/test_api.py +353 -0
  198. nautobot/load_balancers/tests/test_filters.py +134 -0
  199. nautobot/load_balancers/tests/test_forms.py +266 -0
  200. nautobot/load_balancers/tests/test_models.py +195 -0
  201. nautobot/load_balancers/tests/test_views.py +229 -0
  202. nautobot/load_balancers/urls.py +17 -0
  203. nautobot/load_balancers/views.py +248 -0
  204. nautobot/project-static/dist/css/github-dark.min.css +10 -0
  205. nautobot/project-static/dist/css/github.min.css +10 -0
  206. nautobot/project-static/dist/css/nautobot.css +1 -11
  207. nautobot/project-static/dist/css/nautobot.css.map +1 -1
  208. nautobot/project-static/dist/js/libraries.js +1 -1
  209. nautobot/project-static/dist/js/libraries.js.map +1 -1
  210. nautobot/project-static/dist/js/nautobot.js +1 -1
  211. nautobot/project-static/dist/js/nautobot.js.map +1 -1
  212. nautobot/project-static/js/forms.js +13 -0
  213. nautobot/project-static/nautobot-icons/bus-globe.svg +3 -0
  214. nautobot/project-static/nautobot-icons/bus-shield-check.svg +3 -0
  215. nautobot/project-static/nautobot-icons/bus-shield.svg +3 -0
  216. nautobot/ui/package-lock.json +87 -4
  217. nautobot/ui/package.json +2 -1
  218. nautobot/ui/src/js/nautobot.js +0 -1
  219. nautobot/ui/src/js/select2.js +53 -2
  220. nautobot/ui/src/scss/nautobot.scss +51 -2
  221. nautobot/ui/webpack.config.js +13 -0
  222. nautobot/users/templates/users/preferences.html +11 -2
  223. nautobot/virtualization/filters.py +6 -1
  224. nautobot/virtualization/tests/test_filters.py +10 -1
  225. nautobot/virtualization/tests/test_models.py +1 -0
  226. nautobot/virtualization/views.py +4 -1
  227. nautobot/vpn/factory.py +25 -15
  228. nautobot/vpn/filters.py +1 -0
  229. nautobot/vpn/forms.py +1 -0
  230. nautobot/vpn/migrations/0001_initial.py +1 -1
  231. nautobot/vpn/models.py +16 -8
  232. nautobot/vpn/tables.py +5 -2
  233. nautobot/vpn/tests/test_api.py +0 -5
  234. nautobot/vpn/tests/test_forms.py +1 -2
  235. nautobot/vpn/tests/test_models.py +57 -7
  236. nautobot/vpn/tests/test_views.py +22 -3
  237. nautobot/vpn/views.py +78 -20
  238. {nautobot-3.0.0a3.dist-info → nautobot-3.0.0rc1.dist-info}/METADATA +4 -4
  239. {nautobot-3.0.0a3.dist-info → nautobot-3.0.0rc1.dist-info}/RECORD +243 -352
  240. nautobot/circuits/templates/circuits/circuit.html +0 -2
  241. nautobot/circuits/templates/circuits/circuit_edit.html +0 -2
  242. nautobot/circuits/templates/circuits/circuit_retrieve.html +0 -2
  243. nautobot/circuits/templates/circuits/circuit_update.html +0 -1
  244. nautobot/circuits/templates/circuits/circuittermination.html +0 -2
  245. nautobot/circuits/templates/circuits/circuittermination_edit.html +0 -2
  246. nautobot/circuits/templates/circuits/circuittermination_retrieve.html +0 -2
  247. nautobot/circuits/templates/circuits/circuittermination_update.html +0 -1
  248. nautobot/circuits/templates/circuits/circuittype.html +0 -2
  249. nautobot/circuits/templates/circuits/circuittype_retrieve.html +0 -2
  250. nautobot/circuits/templates/circuits/inc/circuit_termination.html +0 -85
  251. nautobot/circuits/templates/circuits/provider.html +0 -2
  252. nautobot/circuits/templates/circuits/provider_edit.html +0 -2
  253. nautobot/circuits/templates/circuits/provider_retrieve.html +0 -1
  254. nautobot/circuits/templates/circuits/provider_update.html +0 -1
  255. nautobot/circuits/templates/circuits/providernetwork.html +0 -2
  256. nautobot/circuits/templates/circuits/providernetwork_retrieve.html +0 -2
  257. nautobot/cloud/templates/cloud/cloudaccount_retrieve.html +0 -2
  258. nautobot/cloud/templates/cloud/cloudnetwork_retrieve.html +0 -2
  259. nautobot/cloud/templates/cloud/cloudresourcetype_retrieve.html +0 -2
  260. nautobot/cloud/templates/cloud/cloudservice_retrieve.html +0 -2
  261. nautobot/core/templates/buttons/import.html +0 -9
  262. nautobot/data_validation/templates/data_validation/datacompliance_retrieve.html +0 -1
  263. nautobot/dcim/templates/dcim/cable.html +0 -2
  264. nautobot/dcim/templates/dcim/cable_edit.html +0 -2
  265. nautobot/dcim/templates/dcim/controller/base.html +0 -2
  266. nautobot/dcim/templates/dcim/controller_retrieve.html +0 -2
  267. nautobot/dcim/templates/dcim/controller_wirelessnetworks.html +0 -2
  268. nautobot/dcim/templates/dcim/controllermanageddevicegroup_retrieve.html +0 -2
  269. nautobot/dcim/templates/dcim/device/base.html +0 -2
  270. nautobot/dcim/templates/dcim/device/consoleports.html +0 -2
  271. nautobot/dcim/templates/dcim/device/consoleserverports.html +0 -2
  272. nautobot/dcim/templates/dcim/device/devicebays.html +0 -2
  273. nautobot/dcim/templates/dcim/device/frontports.html +0 -2
  274. nautobot/dcim/templates/dcim/device/interfaces.html +0 -2
  275. nautobot/dcim/templates/dcim/device/inventory.html +0 -2
  276. nautobot/dcim/templates/dcim/device/modulebays.html +0 -2
  277. nautobot/dcim/templates/dcim/device/poweroutlets.html +0 -2
  278. nautobot/dcim/templates/dcim/device/powerports.html +0 -2
  279. nautobot/dcim/templates/dcim/device/rearports.html +0 -2
  280. nautobot/dcim/templates/dcim/device/wireless.html +0 -2
  281. nautobot/dcim/templates/dcim/device_component.html +0 -2
  282. nautobot/dcim/templates/dcim/device_edit.html +0 -2
  283. nautobot/dcim/templates/dcim/devicefamily_retrieve.html +0 -2
  284. nautobot/dcim/templates/dcim/deviceredundancygroup_retrieve.html +0 -2
  285. nautobot/dcim/templates/dcim/devicetype.html +0 -2
  286. nautobot/dcim/templates/dcim/devicetype_edit.html +0 -2
  287. nautobot/dcim/templates/dcim/devicetype_retrieve.html +0 -2
  288. nautobot/dcim/templates/dcim/inc/device_napalm_tabs.html +0 -1
  289. nautobot/dcim/templates/dcim/interfaceredundancygroup_retrieve.html +0 -2
  290. nautobot/dcim/templates/dcim/location.html +0 -2
  291. nautobot/dcim/templates/dcim/location_edit.html +0 -2
  292. nautobot/dcim/templates/dcim/location_retrieve.html +0 -2
  293. nautobot/dcim/templates/dcim/locationtype.html +0 -2
  294. nautobot/dcim/templates/dcim/locationtype_retrieve.html +0 -2
  295. nautobot/dcim/templates/dcim/manufacturer.html +0 -2
  296. nautobot/dcim/templates/dcim/modulebay_retrieve.html +0 -1
  297. nautobot/dcim/templates/dcim/platform.html +0 -2
  298. nautobot/dcim/templates/dcim/powerfeed.html +0 -2
  299. nautobot/dcim/templates/dcim/powerfeed_retrieve.html +0 -2
  300. nautobot/dcim/templates/dcim/powerpanel.html +0 -2
  301. nautobot/dcim/templates/dcim/powerpanel_edit.html +0 -2
  302. nautobot/dcim/templates/dcim/powerpanel_retrieve.html +0 -2
  303. nautobot/dcim/templates/dcim/rack.html +0 -2
  304. nautobot/dcim/templates/dcim/rack_edit.html +0 -2
  305. nautobot/dcim/templates/dcim/rackgroup.html +0 -2
  306. nautobot/dcim/templates/dcim/rackreservation.html +0 -2
  307. nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +0 -2
  308. nautobot/dcim/templates/dcim/softwareversion_retrieve.html +0 -2
  309. nautobot/dcim/templates/dcim/virtualchassis.html +0 -2
  310. nautobot/dcim/templates/dcim/virtualchassis_add.html +0 -2
  311. nautobot/dcim/templates/dcim/virtualchassis_edit.html +0 -2
  312. nautobot/dcim/templates/dcim/virtualchassis_retrieve.html +0 -2
  313. nautobot/dcim/templates/dcim/virtualdevicecontext_retrieve.html +0 -2
  314. nautobot/dcim/ui.py +0 -29
  315. nautobot/extras/templates/extras/computedfield.html +0 -2
  316. nautobot/extras/templates/extras/computedfield_retrieve.html +0 -2
  317. nautobot/extras/templates/extras/configcontext.html +0 -2
  318. nautobot/extras/templates/extras/configcontext_edit.html +0 -2
  319. nautobot/extras/templates/extras/configcontext_retrieve.html +0 -2
  320. nautobot/extras/templates/extras/configcontextschema.html +0 -2
  321. nautobot/extras/templates/extras/configcontextschema_edit.html +0 -2
  322. nautobot/extras/templates/extras/contact_retrieve.html +0 -2
  323. nautobot/extras/templates/extras/customfield.html +0 -2
  324. nautobot/extras/templates/extras/customfield_edit.html +0 -2
  325. nautobot/extras/templates/extras/customfield_retrieve.html +0 -2
  326. nautobot/extras/templates/extras/customlink.html +0 -2
  327. nautobot/extras/templates/extras/dynamicgroup.html +0 -2
  328. nautobot/extras/templates/extras/dynamicgroup_edit.html +0 -2
  329. nautobot/extras/templates/extras/exporttemplate.html +0 -2
  330. nautobot/extras/templates/extras/gitrepository.html +0 -2
  331. nautobot/extras/templates/extras/gitrepository_object_edit.html +0 -2
  332. nautobot/extras/templates/extras/graphqlquery.html +0 -2
  333. nautobot/extras/templates/extras/graphqlquery_list.html +0 -1
  334. nautobot/extras/templates/extras/graphqlquery_retrieve.html +0 -2
  335. nautobot/extras/templates/extras/job_detail.html +0 -2
  336. nautobot/extras/templates/extras/jobbutton_retrieve.html +0 -2
  337. nautobot/extras/templates/extras/jobhook.html +0 -2
  338. nautobot/extras/templates/extras/jobqueue_retrieve.html +0 -2
  339. nautobot/extras/templates/extras/jobresult.html +0 -2
  340. nautobot/extras/templates/extras/metadatatype_retrieve.html +0 -2
  341. nautobot/extras/templates/extras/note.html +0 -2
  342. nautobot/extras/templates/extras/note_retrieve.html +0 -1
  343. nautobot/extras/templates/extras/object_changelog.html +0 -2
  344. nautobot/extras/templates/extras/object_notes.html +0 -2
  345. nautobot/extras/templates/extras/objectchange.html +0 -2
  346. nautobot/extras/templates/extras/objectchange_list.html +0 -3
  347. nautobot/extras/templates/extras/relationship.html +0 -1
  348. nautobot/extras/templates/extras/secret.html +0 -1
  349. nautobot/extras/templates/extras/secret_edit.html +0 -1
  350. nautobot/extras/templates/extras/secretsgroup.html +0 -2
  351. nautobot/extras/templates/extras/secretsgroup_edit.html +0 -2
  352. nautobot/extras/templates/extras/secretsgroup_retrieve.html +0 -2
  353. nautobot/extras/templates/extras/status.html +0 -2
  354. nautobot/extras/templates/extras/tag.html +0 -2
  355. nautobot/extras/templates/extras/tag_edit.html +0 -2
  356. nautobot/extras/templates/extras/tag_retrieve.html +0 -2
  357. nautobot/extras/templates/extras/team_retrieve.html +0 -2
  358. nautobot/ipam/templates/ipam/namespace_retrieve.html +0 -1
  359. nautobot/ipam/templates/ipam/prefix.html +0 -2
  360. nautobot/ipam/templates/ipam/prefix_edit.html +0 -1
  361. nautobot/ipam/templates/ipam/prefix_retrieve.html +0 -2
  362. nautobot/ipam/templates/ipam/rir.html +0 -2
  363. nautobot/ipam/templates/ipam/routetarget.html +0 -1
  364. nautobot/ipam/templates/ipam/service.html +0 -2
  365. nautobot/ipam/templates/ipam/service_edit.html +0 -2
  366. nautobot/ipam/templates/ipam/service_retrieve.html +0 -2
  367. nautobot/ipam/templates/ipam/vlan.html +0 -2
  368. nautobot/ipam/templates/ipam/vlan_edit.html +0 -2
  369. nautobot/ipam/templates/ipam/vlan_retrieve.html +0 -2
  370. nautobot/ipam/templates/ipam/vlangroup.html +0 -2
  371. nautobot/ipam/templates/ipam/vrf.html +0 -1
  372. nautobot/tenancy/templates/tenancy/tenant.html +0 -2
  373. nautobot/tenancy/templates/tenancy/tenant_edit.html +0 -2
  374. nautobot/tenancy/templates/tenancy/tenantgroup.html +0 -2
  375. nautobot/tenancy/templates/tenancy/tenantgroup_retrieve.html +0 -1
  376. nautobot/virtualization/templates/virtualization/clustergroup.html +0 -2
  377. nautobot/virtualization/templates/virtualization/clustertype.html +0 -2
  378. nautobot/virtualization/templates/virtualization/virtualmachine.html +0 -2
  379. nautobot/virtualization/templates/virtualization/virtualmachine_edit.html +0 -2
  380. nautobot/virtualization/templates/virtualization/virtualmachine_retrieve.html +0 -2
  381. nautobot/vpn/templates/vpn/vpnprofile.html +0 -2
  382. nautobot/wireless/templates/wireless/radioprofile_retrieve.html +0 -2
  383. nautobot/wireless/templates/wireless/supporteddatarate_retrieve.html +0 -2
  384. nautobot/wireless/templates/wireless/wirelessnetwork_retrieve.html +0 -2
  385. {nautobot-3.0.0a3.dist-info → nautobot-3.0.0rc1.dist-info}/LICENSE.txt +0 -0
  386. {nautobot-3.0.0a3.dist-info → nautobot-3.0.0rc1.dist-info}/NOTICE +0 -0
  387. {nautobot-3.0.0a3.dist-info → nautobot-3.0.0rc1.dist-info}/WHEEL +0 -0
  388. {nautobot-3.0.0a3.dist-info → nautobot-3.0.0rc1.dist-info}/entry_points.txt +0 -0
nautobot/dcim/views.py CHANGED
@@ -36,10 +36,10 @@ from nautobot.core.exceptions import AbortTransaction
36
36
  from nautobot.core.forms import BulkRenameForm, ConfirmationForm, ImportForm, restrict_form_fields
37
37
  from nautobot.core.models.querysets import count_related
38
38
  from nautobot.core.templatetags import helpers
39
- from nautobot.core.templatetags.helpers import has_perms
39
+ from nautobot.core.templatetags.helpers import bettertitle, has_perms
40
40
  from nautobot.core.ui import object_detail
41
41
  from nautobot.core.ui.breadcrumbs import (
42
- AncestorsBreadcrumbs,
42
+ AncestorsInstanceBreadcrumbItem,
43
43
  BaseBreadcrumbItem,
44
44
  Breadcrumbs,
45
45
  context_object_attr,
@@ -77,7 +77,6 @@ from nautobot.core.views.utils import common_detail_view_context, get_obj_from_c
77
77
  from nautobot.core.views.viewsets import NautobotUIViewSet
78
78
  from nautobot.dcim.choices import LocationDataToContactActionChoices
79
79
  from nautobot.dcim.forms import LocationMigrateDataToContactForm
80
- from nautobot.dcim.ui import RackBreadcrumbs
81
80
  from nautobot.dcim.utils import get_all_network_driver_mappings, render_software_version_and_image_files
82
81
  from nautobot.extras.models import ConfigContext, Contact, ContactAssociation, Role, Status, Team
83
82
  from nautobot.extras.tables import DynamicGroupTable, ImageAttachmentTable
@@ -249,7 +248,14 @@ class LocationTypeUIViewSet(NautobotUIViewSet):
249
248
  form_class = forms.LocationTypeForm
250
249
  bulk_update_form_class = forms.LocationTypeBulkEditForm
251
250
  serializer_class = serializers.LocationSerializer
252
- breadcrumbs = AncestorsBreadcrumbs(detail_item_label=context_object_attr("name"))
251
+ breadcrumbs = Breadcrumbs(
252
+ items={
253
+ "detail": [
254
+ ModelBreadcrumbItem(),
255
+ AncestorsInstanceBreadcrumbItem(),
256
+ ]
257
+ }
258
+ )
253
259
 
254
260
  object_detail_content = object_detail.ObjectDetailContent(
255
261
  panels=(
@@ -393,7 +399,14 @@ class LocationUIViewSet(NautobotUIViewSet):
393
399
  form_class = forms.LocationForm
394
400
  bulk_update_form_class = forms.LocationBulkEditForm
395
401
  serializer_class = serializers.LocationSerializer
396
- breadcrumbs = AncestorsBreadcrumbs(detail_item_label=context_object_attr("name"))
402
+ breadcrumbs = Breadcrumbs(
403
+ items={
404
+ "detail": [
405
+ ModelBreadcrumbItem(),
406
+ AncestorsInstanceBreadcrumbItem(),
407
+ ]
408
+ }
409
+ )
397
410
  view_titles = Titles(titles={"detail": "{{ object.name }}"})
398
411
 
399
412
  object_detail_content = object_detail.ObjectDetailContent(
@@ -471,6 +484,7 @@ class LocationUIViewSet(NautobotUIViewSet):
471
484
  table_attribute="images",
472
485
  related_field_name="location",
473
486
  show_table_config_button=False,
487
+ enable_related_link=False,
474
488
  ),
475
489
  object_detail.ObjectsTablePanel(
476
490
  section=SectionChoices.FULL_WIDTH,
@@ -486,25 +500,31 @@ class LocationUIViewSet(NautobotUIViewSet):
486
500
  )
487
501
 
488
502
  def get_extra_context(self, request, instance):
489
- if instance is None:
490
- return super().get_extra_context(request, instance)
503
+ context = super().get_extra_context(request, instance)
491
504
 
492
- # This query can get really expensive when there are big location trees in the DB. By casting it to a list we
493
- # ensure it is only performed once rather than as a subquery for each of the different count stats.
494
- related_locations = list(
495
- instance.descendants(include_self=True).restrict(request.user, "view").values_list("pk", flat=True)
496
- )
505
+ if self.action == "retrieve":
506
+ # This query can get really expensive when there are big location trees in the DB. By casting it to a list we
507
+ # ensure it is only performed once rather than as a subquery for each of the different count stats.
508
+ related_locations = list(
509
+ instance.descendants(include_self=True).restrict(request.user, "view").values_list("pk", flat=True)
510
+ )
497
511
 
498
- rack_groups = (
499
- RackGroup.objects.annotate(rack_count=count_related(Rack, "rack_group"))
500
- .restrict(request.user, "view")
501
- .filter(location__in=related_locations)
502
- )
512
+ rack_groups = (
513
+ RackGroup.objects.annotate(rack_count=count_related(Rack, "rack_group"))
514
+ .restrict(request.user, "view")
515
+ .filter(location__in=related_locations)
516
+ )
503
517
 
504
- return {
505
- "rack_groups": rack_groups,
506
- "rack_count": Rack.objects.restrict(request.user, "view").filter(location__in=related_locations).count(),
507
- }
518
+ context.update(
519
+ {
520
+ "rack_groups": rack_groups,
521
+ "rack_count": Rack.objects.restrict(request.user, "view")
522
+ .filter(location__in=related_locations)
523
+ .count(),
524
+ }
525
+ )
526
+
527
+ return context
508
528
 
509
529
 
510
530
  class MigrateLocationDataToContactView(generic.ObjectEditView):
@@ -654,6 +674,14 @@ class RackGroupUIViewSet(NautobotUIViewSet):
654
674
  serializer_class = serializers.RackGroupSerializer
655
675
  table_class = tables.RackGroupTable
656
676
  queryset = RackGroup.objects.all()
677
+ breadcrumbs = Breadcrumbs(
678
+ items={
679
+ "detail": [
680
+ ModelBreadcrumbItem(),
681
+ AncestorsInstanceBreadcrumbItem(),
682
+ ]
683
+ }
684
+ )
657
685
 
658
686
  object_detail_content = object_detail.ObjectDetailContent(
659
687
  panels=[
@@ -713,7 +741,26 @@ class RackUIViewSet(NautobotUIViewSet):
713
741
  serializer_class = serializers.RackSerializer
714
742
  table_class = tables.RackDetailTable
715
743
  queryset = Rack.objects.select_related("location", "tenant__tenant_group", "rack_group", "role")
716
- breadcrumbs = RackBreadcrumbs()
744
+ breadcrumbs = Breadcrumbs(
745
+ items={
746
+ "detail": [
747
+ ModelBreadcrumbItem(),
748
+ AncestorsInstanceBreadcrumbItem(
749
+ instance=context_object_attr("location"),
750
+ ancestor_item=lambda ancestor: InstanceParentBreadcrumbItem(parent_key="location", parent=ancestor),
751
+ include_self=True,
752
+ ),
753
+ AncestorsInstanceBreadcrumbItem(
754
+ instance=context_object_attr("rack_group"),
755
+ ancestor_item=lambda ancestor: InstanceParentBreadcrumbItem(
756
+ parent_key="rack_group", parent=ancestor
757
+ ),
758
+ include_self=True,
759
+ should_render=context_object_attr("rack_group"),
760
+ ),
761
+ ]
762
+ }
763
+ )
717
764
 
718
765
  def get_extra_context(self, request, instance):
719
766
  context = super().get_extra_context(request, instance)
@@ -759,9 +806,6 @@ class RackElevationListView(generic.ObjectListView):
759
806
  action_buttons = []
760
807
  template_name = "dcim/rack_elevation_list.html"
761
808
  view_titles = Titles(titles={"list": "Rack Elevation"})
762
- breadcrumbs = Breadcrumbs(
763
- items={"list": [ViewNameBreadcrumbItem(view_name="dcim:rack_elevation_list", label="Rack Elevation")]}
764
- )
765
809
 
766
810
  def extra_context(self):
767
811
  racks = self.queryset
@@ -1018,6 +1062,7 @@ class DeviceTypeUIViewSet(NautobotUIViewSet):
1018
1062
  model=InterfaceTemplate,
1019
1063
  ),
1020
1064
  include_paginator=True,
1065
+ enable_related_link=False,
1021
1066
  ),
1022
1067
  ),
1023
1068
  ),
@@ -1043,6 +1088,7 @@ class DeviceTypeUIViewSet(NautobotUIViewSet):
1043
1088
  model=FrontPortTemplate,
1044
1089
  ),
1045
1090
  include_paginator=True,
1091
+ enable_related_link=False,
1046
1092
  ),
1047
1093
  ),
1048
1094
  ),
@@ -1068,6 +1114,7 @@ class DeviceTypeUIViewSet(NautobotUIViewSet):
1068
1114
  model=RearPortTemplate,
1069
1115
  ),
1070
1116
  include_paginator=True,
1117
+ enable_related_link=False,
1071
1118
  ),
1072
1119
  ),
1073
1120
  ),
@@ -1093,6 +1140,7 @@ class DeviceTypeUIViewSet(NautobotUIViewSet):
1093
1140
  model=ConsolePortTemplate,
1094
1141
  ),
1095
1142
  include_paginator=True,
1143
+ enable_related_link=False,
1096
1144
  ),
1097
1145
  ),
1098
1146
  ),
@@ -1118,6 +1166,7 @@ class DeviceTypeUIViewSet(NautobotUIViewSet):
1118
1166
  model=ConsoleServerPortTemplate,
1119
1167
  ),
1120
1168
  include_paginator=True,
1169
+ enable_related_link=False,
1121
1170
  ),
1122
1171
  ),
1123
1172
  ),
@@ -1143,6 +1192,7 @@ class DeviceTypeUIViewSet(NautobotUIViewSet):
1143
1192
  model=PowerPortTemplate,
1144
1193
  ),
1145
1194
  include_paginator=True,
1195
+ enable_related_link=False,
1146
1196
  ),
1147
1197
  ),
1148
1198
  ),
@@ -1168,6 +1218,7 @@ class DeviceTypeUIViewSet(NautobotUIViewSet):
1168
1218
  model=PowerOutletTemplate,
1169
1219
  ),
1170
1220
  include_paginator=True,
1221
+ enable_related_link=False,
1171
1222
  ),
1172
1223
  ),
1173
1224
  ),
@@ -1193,6 +1244,7 @@ class DeviceTypeUIViewSet(NautobotUIViewSet):
1193
1244
  model=DeviceBayTemplate,
1194
1245
  ),
1195
1246
  include_paginator=True,
1247
+ enable_related_link=False,
1196
1248
  ),
1197
1249
  ),
1198
1250
  ),
@@ -1218,6 +1270,7 @@ class DeviceTypeUIViewSet(NautobotUIViewSet):
1218
1270
  model=ModuleBayTemplate,
1219
1271
  ),
1220
1272
  include_paginator=True,
1273
+ enable_related_link=False,
1221
1274
  ),
1222
1275
  ),
1223
1276
  ),
@@ -1433,6 +1486,7 @@ class ModuleTypeUIViewSet(
1433
1486
  ObjectDestroyViewMixin,
1434
1487
  ObjectBulkDestroyViewMixin,
1435
1488
  ObjectBulkUpdateViewMixin,
1489
+ # ObjectDataComplianceViewMixin, # TODO: enable once converted to UI framework
1436
1490
  ObjectChangeLogViewMixin,
1437
1491
  ObjectNotesViewMixin,
1438
1492
  ):
@@ -1481,65 +1535,68 @@ class ModuleTypeUIViewSet(
1481
1535
  return super().get_required_permission()
1482
1536
 
1483
1537
  def get_extra_context(self, request, instance):
1484
- if not instance:
1485
- return {}
1538
+ context = super().get_extra_context(request, instance)
1539
+ if self.action == "retrieve":
1540
+ instance_count = Module.objects.restrict(request.user).filter(module_type=instance).count()
1486
1541
 
1487
- instance_count = Module.objects.restrict(request.user).filter(module_type=instance).count()
1542
+ # Component tables
1543
+ consoleport_table = tables.ConsolePortTemplateTable(
1544
+ ConsolePortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1545
+ orderable=False,
1546
+ )
1547
+ consoleserverport_table = tables.ConsoleServerPortTemplateTable(
1548
+ ConsoleServerPortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1549
+ orderable=False,
1550
+ )
1551
+ powerport_table = tables.PowerPortTemplateTable(
1552
+ PowerPortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1553
+ orderable=False,
1554
+ )
1555
+ poweroutlet_table = tables.PowerOutletTemplateTable(
1556
+ PowerOutletTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1557
+ orderable=False,
1558
+ )
1559
+ interface_table = tables.InterfaceTemplateTable(
1560
+ list(InterfaceTemplate.objects.restrict(request.user, "view").filter(module_type=instance)),
1561
+ orderable=False,
1562
+ )
1563
+ front_port_table = tables.FrontPortTemplateTable(
1564
+ FrontPortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1565
+ orderable=False,
1566
+ )
1567
+ rear_port_table = tables.RearPortTemplateTable(
1568
+ RearPortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1569
+ orderable=False,
1570
+ )
1571
+ modulebay_table = tables.ModuleBayTemplateTable(
1572
+ ModuleBayTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1573
+ orderable=False,
1574
+ )
1575
+ if request.user.has_perm("dcim.change_moduletype"):
1576
+ consoleport_table.columns.show("pk")
1577
+ consoleserverport_table.columns.show("pk")
1578
+ powerport_table.columns.show("pk")
1579
+ poweroutlet_table.columns.show("pk")
1580
+ interface_table.columns.show("pk")
1581
+ front_port_table.columns.show("pk")
1582
+ rear_port_table.columns.show("pk")
1583
+ modulebay_table.columns.show("pk")
1488
1584
 
1489
- # Component tables
1490
- consoleport_table = tables.ConsolePortTemplateTable(
1491
- ConsolePortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1492
- orderable=False,
1493
- )
1494
- consoleserverport_table = tables.ConsoleServerPortTemplateTable(
1495
- ConsoleServerPortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1496
- orderable=False,
1497
- )
1498
- powerport_table = tables.PowerPortTemplateTable(
1499
- PowerPortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1500
- orderable=False,
1501
- )
1502
- poweroutlet_table = tables.PowerOutletTemplateTable(
1503
- PowerOutletTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1504
- orderable=False,
1505
- )
1506
- interface_table = tables.InterfaceTemplateTable(
1507
- list(InterfaceTemplate.objects.restrict(request.user, "view").filter(module_type=instance)),
1508
- orderable=False,
1509
- )
1510
- front_port_table = tables.FrontPortTemplateTable(
1511
- FrontPortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1512
- orderable=False,
1513
- )
1514
- rear_port_table = tables.RearPortTemplateTable(
1515
- RearPortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1516
- orderable=False,
1517
- )
1518
- modulebay_table = tables.ModuleBayTemplateTable(
1519
- ModuleBayTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1520
- orderable=False,
1521
- )
1522
- if request.user.has_perm("dcim.change_moduletype"):
1523
- consoleport_table.columns.show("pk")
1524
- consoleserverport_table.columns.show("pk")
1525
- powerport_table.columns.show("pk")
1526
- poweroutlet_table.columns.show("pk")
1527
- interface_table.columns.show("pk")
1528
- front_port_table.columns.show("pk")
1529
- rear_port_table.columns.show("pk")
1530
- modulebay_table.columns.show("pk")
1585
+ context.update(
1586
+ {
1587
+ "instance_count": instance_count,
1588
+ "consoleport_table": consoleport_table,
1589
+ "consoleserverport_table": consoleserverport_table,
1590
+ "powerport_table": powerport_table,
1591
+ "poweroutlet_table": poweroutlet_table,
1592
+ "interface_table": interface_table,
1593
+ "front_port_table": front_port_table,
1594
+ "rear_port_table": rear_port_table,
1595
+ "modulebay_table": modulebay_table,
1596
+ }
1597
+ )
1531
1598
 
1532
- return {
1533
- "instance_count": instance_count,
1534
- "consoleport_table": consoleport_table,
1535
- "consoleserverport_table": consoleserverport_table,
1536
- "powerport_table": powerport_table,
1537
- "poweroutlet_table": poweroutlet_table,
1538
- "interface_table": interface_table,
1539
- "front_port_table": front_port_table,
1540
- "rear_port_table": rear_port_table,
1541
- "modulebay_table": modulebay_table,
1542
- }
1599
+ return context
1543
1600
 
1544
1601
  @action(
1545
1602
  detail=False,
@@ -2242,32 +2299,43 @@ class DeviceComponentPageMixin:
2242
2299
  - Console Port assigned to the device: Devices / <Device name and link to details> / Console Ports (dcim/devices/<id>/console-ports/) / <Console Port name>
2243
2300
  """
2244
2301
 
2245
- breadcrumbs = Breadcrumbs(
2246
- items={
2247
- "detail": [
2248
- ModelBreadcrumbItem(model=Device, should_render=lambda c: c["object"].device is not None),
2249
- InstanceBreadcrumbItem(
2250
- instance=lambda c: c["object"].device, should_render=lambda c: c["object"].device is not None
2251
- ),
2302
+ def __init_subclass__(cls, **kwargs):
2303
+ super().__init_subclass__(**kwargs)
2304
+
2305
+ device_breadcrumbs = [
2306
+ ModelBreadcrumbItem(model=Device, should_render=lambda c: c["object"].device is not None),
2307
+ InstanceBreadcrumbItem(
2308
+ instance=lambda c: c["object"].device, should_render=lambda c: c["object"].device is not None
2309
+ ),
2310
+ ]
2311
+ module_breadcrumbs = [
2312
+ ModelBreadcrumbItem(model=Module, should_render=lambda c: c["object"].device is None),
2313
+ InstanceBreadcrumbItem(
2314
+ instance=lambda c: c["object"].module, should_render=lambda c: c["object"].device is None
2315
+ ),
2316
+ ]
2317
+ if device_breadcrumb_url := getattr(cls, "device_breadcrumb_url", None):
2318
+ device_breadcrumbs.append(
2252
2319
  ViewNameBreadcrumbItem(
2253
- view_name_key="device_breadcrumb_url",
2254
- should_render=lambda c: c["object"].device is not None and c.get("device_breadcrumb_url"),
2320
+ view_name=device_breadcrumb_url,
2321
+ should_render=lambda c: c["object"].device is not None,
2255
2322
  reverse_kwargs=lambda c: {"pk": c["object"].device.pk},
2256
- label=lambda c: c["object"]._meta.verbose_name_plural,
2257
- ),
2258
- ModelBreadcrumbItem(model=Module, should_render=lambda c: c["object"].device is None),
2259
- InstanceBreadcrumbItem(
2260
- instance=lambda c: c["object"].module, should_render=lambda c: c["object"].device is None
2323
+ label=lambda c: bettertitle(c["object"]._meta.verbose_name_plural),
2261
2324
  ),
2325
+ )
2326
+
2327
+ if module_breadcrumb_url := getattr(cls, "module_breadcrumb_url", None):
2328
+ module_breadcrumbs.append(
2262
2329
  ViewNameBreadcrumbItem(
2263
- view_name_key="module_breadcrumb_url",
2264
- should_render=lambda c: c["object"].device is None and c.get("module_breadcrumb_url"),
2330
+ view_name=module_breadcrumb_url,
2331
+ should_render=lambda c: c["object"].device is None,
2265
2332
  reverse_kwargs=lambda c: {"pk": c["object"].module.pk},
2266
- label=lambda c: c["object"]._meta.verbose_name_plural,
2333
+ label=lambda c: bettertitle(c["object"]._meta.verbose_name_plural),
2267
2334
  ),
2268
- ]
2269
- }
2270
- )
2335
+ )
2336
+
2337
+ cls.breadcrumbs = Breadcrumbs(items={"detail": [*device_breadcrumbs, *module_breadcrumbs]})
2338
+
2271
2339
  view_titles = Titles(
2272
2340
  titles={
2273
2341
  "detail": "{% if object.device %}{{ object.device }}{% else %}{{ object.module.display }}{% endif %} / {{ object }}"
@@ -2290,7 +2358,11 @@ class DeviceUIViewSet(NautobotUIViewSet):
2290
2358
  items={
2291
2359
  "detail": [
2292
2360
  ModelBreadcrumbItem(model=Device),
2293
- InstanceParentBreadcrumbItem(parent_key="location"),
2361
+ AncestorsInstanceBreadcrumbItem(
2362
+ instance=context_object_attr("location"),
2363
+ include_self=True,
2364
+ ancestor_item=lambda ancestor: InstanceParentBreadcrumbItem(parent_key="location", parent=ancestor),
2365
+ ),
2294
2366
  InstanceBreadcrumbItem(
2295
2367
  instance=lambda c: c["object"].parent_bay.device,
2296
2368
  should_render=lambda c: hasattr(c["object"], "parent_bay"),
@@ -2304,7 +2376,7 @@ class DeviceUIViewSet(NautobotUIViewSet):
2304
2376
 
2305
2377
  def get_queryset(self):
2306
2378
  queryset = super().get_queryset()
2307
- if self.detail: # TODO: change to self.action == "retrieve" as a part of addressing NAUTOBOT-1051
2379
+ if self.action == "retrieve":
2308
2380
  queryset = queryset.select_related(
2309
2381
  "controller_managed_device_group__controller",
2310
2382
  "device_redundancy_group",
@@ -2698,6 +2770,7 @@ class DeviceUIViewSet(NautobotUIViewSet):
2698
2770
  table_class=VRFDeviceAssignmentTable,
2699
2771
  table_filter="device",
2700
2772
  exclude_columns=["related_object_type", "related_object_name"],
2773
+ related_list_url_name="ipam:vrf_list",
2701
2774
  show_table_config_button=False,
2702
2775
  ),
2703
2776
  object_detail.ObjectsTablePanel(
@@ -2727,6 +2800,7 @@ class DeviceUIViewSet(NautobotUIViewSet):
2727
2800
  table_attribute="images",
2728
2801
  related_field_name="device",
2729
2802
  show_table_config_button=False,
2803
+ enable_related_link=False,
2730
2804
  ),
2731
2805
  object_detail.ObjectsTablePanel(
2732
2806
  weight=100,
@@ -3094,7 +3168,7 @@ class DeviceUIViewSet(NautobotUIViewSet):
3094
3168
  def get_extra_context(self, request, instance):
3095
3169
  extra_context = super().get_extra_context(request, instance)
3096
3170
 
3097
- if self.detail: # TODO: change to `if self.action == "retrieve"` as a part of addressing NAUTOBOT-1051
3171
+ if self.action == "retrieve":
3098
3172
  # VirtualChassis members
3099
3173
  if instance.virtual_chassis is not None:
3100
3174
  vc_members = (
@@ -3802,11 +3876,13 @@ class ConsolePortListView(generic.ObjectListView):
3802
3876
 
3803
3877
  class ConsolePortView(DeviceComponentPageMixin, generic.ObjectView):
3804
3878
  queryset = ConsolePort.objects.all()
3879
+ device_breadcrumb_url = "dcim:device_consoleports"
3880
+ module_breadcrumb_url = "dcim:module_consoleports"
3805
3881
 
3806
3882
  def get_extra_context(self, request, instance):
3807
3883
  return {
3808
- "device_breadcrumb_url": "dcim:device_consoleports",
3809
- "module_breadcrumb_url": "dcim:module_consoleports",
3884
+ "device_breadcrumb_url": self.device_breadcrumb_url,
3885
+ "module_breadcrumb_url": self.module_breadcrumb_url,
3810
3886
  **super().get_extra_context(request, instance),
3811
3887
  }
3812
3888
 
@@ -3868,11 +3944,13 @@ class ConsoleServerPortListView(generic.ObjectListView):
3868
3944
 
3869
3945
  class ConsoleServerPortView(DeviceComponentPageMixin, generic.ObjectView):
3870
3946
  queryset = ConsoleServerPort.objects.all()
3947
+ device_breadcrumb_url = "dcim:device_consoleserverports"
3948
+ module_breadcrumb_url = "dcim:module_consoleserverports"
3871
3949
 
3872
3950
  def get_extra_context(self, request, instance):
3873
3951
  return {
3874
- "device_breadcrumb_url": "dcim:device_consoleserverports",
3875
- "module_breadcrumb_url": "dcim:module_consoleserverports",
3952
+ "device_breadcrumb_url": self.device_breadcrumb_url,
3953
+ "module_breadcrumb_url": self.module_breadcrumb_url,
3876
3954
  **super().get_extra_context(request, instance),
3877
3955
  }
3878
3956
 
@@ -3934,11 +4012,13 @@ class PowerPortListView(generic.ObjectListView):
3934
4012
 
3935
4013
  class PowerPortView(DeviceComponentPageMixin, generic.ObjectView):
3936
4014
  queryset = PowerPort.objects.all()
4015
+ device_breadcrumb_url = "dcim:device_powerports"
4016
+ module_breadcrumb_url = "dcim:module_powerports"
3937
4017
 
3938
4018
  def get_extra_context(self, request, instance):
3939
4019
  return {
3940
- "device_breadcrumb_url": "dcim:device_powerports",
3941
- "module_breadcrumb_url": "dcim:module_powerports",
4020
+ "device_breadcrumb_url": self.device_breadcrumb_url,
4021
+ "module_breadcrumb_url": self.module_breadcrumb_url,
3942
4022
  **super().get_extra_context(request, instance),
3943
4023
  }
3944
4024
 
@@ -4000,11 +4080,13 @@ class PowerOutletListView(generic.ObjectListView):
4000
4080
 
4001
4081
  class PowerOutletView(DeviceComponentPageMixin, generic.ObjectView):
4002
4082
  queryset = PowerOutlet.objects.all()
4083
+ device_breadcrumb_url = "dcim:device_poweroutlets"
4084
+ module_breadcrumb_url = "dcim:module_poweroutlets"
4003
4085
 
4004
4086
  def get_extra_context(self, request, instance):
4005
4087
  return {
4006
- "device_breadcrumb_url": "dcim:device_poweroutlets",
4007
- "module_breadcrumb_url": "dcim:module_poweroutlets",
4088
+ "device_breadcrumb_url": self.device_breadcrumb_url,
4089
+ "module_breadcrumb_url": self.module_breadcrumb_url,
4008
4090
  **super().get_extra_context(request, instance),
4009
4091
  }
4010
4092
 
@@ -4069,6 +4151,8 @@ class InterfaceView(
4069
4151
  generic.ObjectView,
4070
4152
  ):
4071
4153
  queryset = Interface.objects.all()
4154
+ device_breadcrumb_url = "dcim:device_interfaces"
4155
+ module_breadcrumb_url = "dcim:module_interfaces"
4072
4156
 
4073
4157
  def get_extra_context(self, request, instance):
4074
4158
  # Get assigned IP addresses
@@ -4109,8 +4193,8 @@ class InterfaceView(
4109
4193
  return {
4110
4194
  "ipaddress_table": ipaddress_table,
4111
4195
  "vlan_table": vlan_table,
4112
- "device_breadcrumb_url": "dcim:device_interfaces",
4113
- "module_breadcrumb_url": "dcim:module_interfaces",
4196
+ "device_breadcrumb_url": self.device_breadcrumb_url,
4197
+ "module_breadcrumb_url": self.module_breadcrumb_url,
4114
4198
  "child_interfaces_table": child_interfaces_tables,
4115
4199
  "redundancy_table": redundancy_table,
4116
4200
  "virtual_device_contexts_table": virtual_device_contexts_table,
@@ -4199,11 +4283,13 @@ class FrontPortListView(generic.ObjectListView):
4199
4283
 
4200
4284
  class FrontPortView(DeviceComponentPageMixin, generic.ObjectView):
4201
4285
  queryset = FrontPort.objects.all()
4286
+ device_breadcrumb_url = "dcim:device_frontports"
4287
+ module_breadcrumb_url = "dcim:module_frontports"
4202
4288
 
4203
4289
  def get_extra_context(self, request, instance):
4204
4290
  return {
4205
- "device_breadcrumb_url": "dcim:device_frontports",
4206
- "module_breadcrumb_url": "dcim:module_frontports",
4291
+ "device_breadcrumb_url": self.device_breadcrumb_url,
4292
+ "module_breadcrumb_url": self.module_breadcrumb_url,
4207
4293
  **super().get_extra_context(request, instance),
4208
4294
  }
4209
4295
 
@@ -4265,11 +4351,13 @@ class RearPortListView(generic.ObjectListView):
4265
4351
 
4266
4352
  class RearPortView(DeviceComponentPageMixin, generic.ObjectView):
4267
4353
  queryset = RearPort.objects.all()
4354
+ device_breadcrumb_url = "dcim:device_rearports"
4355
+ module_breadcrumb_url = "dcim:module_rearports"
4268
4356
 
4269
4357
  def get_extra_context(self, request, instance):
4270
4358
  return {
4271
- "device_breadcrumb_url": "dcim:device_rearports",
4272
- "module_breadcrumb_url": "dcim:module_rearports",
4359
+ "device_breadcrumb_url": self.device_breadcrumb_url,
4360
+ "module_breadcrumb_url": self.module_breadcrumb_url,
4273
4361
  **super().get_extra_context(request, instance),
4274
4362
  }
4275
4363
 
@@ -4331,9 +4419,13 @@ class DeviceBayListView(generic.ObjectListView):
4331
4419
 
4332
4420
  class DeviceBayView(DeviceComponentPageMixin, generic.ObjectView):
4333
4421
  queryset = DeviceBay.objects.all()
4422
+ device_breadcrumb_url = "dcim:device_devicebays"
4334
4423
 
4335
4424
  def get_extra_context(self, request, instance):
4336
- return {"device_breadcrumb_url": "dcim:device_devicebays", **super().get_extra_context(request, instance)}
4425
+ return {
4426
+ "device_breadcrumb_url": self.device_breadcrumb_url,
4427
+ **super().get_extra_context(request, instance),
4428
+ }
4337
4429
 
4338
4430
 
4339
4431
  class DeviceBayCreateView(generic.ComponentCreateView):
@@ -4568,6 +4660,7 @@ class InventoryItemListView(generic.ObjectListView):
4568
4660
 
4569
4661
  class InventoryItemView(DeviceComponentPageMixin, generic.ObjectView):
4570
4662
  queryset = InventoryItem.objects.all().select_related("device", "manufacturer", "software_version")
4663
+ device_breadcrumb_url = "dcim:device_inventory"
4571
4664
 
4572
4665
  def get_extra_context(self, request, instance):
4573
4666
  # Software images
@@ -4577,7 +4670,7 @@ class InventoryItemView(DeviceComponentPageMixin, generic.ObjectView):
4577
4670
  software_version_images = []
4578
4671
 
4579
4672
  return {
4580
- "device_breadcrumb_url": "dcim:device_inventory",
4673
+ "device_breadcrumb_url": self.device_breadcrumb_url,
4581
4674
  "software_version_images": software_version_images,
4582
4675
  **super().get_extra_context(request, instance),
4583
4676
  }
@@ -4767,6 +4860,7 @@ class PathTraceView(generic.ObjectView):
4767
4860
 
4768
4861
  additional_permissions = ["dcim.view_cable"]
4769
4862
  template_name = "dcim/cable_trace.html"
4863
+ view_titles = Titles(titles={"detail": "Cable Trace for {{ object }}"})
4770
4864
 
4771
4865
  def dispatch(self, request, *args, **kwargs):
4772
4866
  model = kwargs.pop("model")
@@ -4803,6 +4897,7 @@ class PathTraceView(generic.ObjectView):
4803
4897
  "path": path,
4804
4898
  "related_paths": related_paths,
4805
4899
  "total_length": path.get_total_length() if path else None,
4900
+ "view_titles": self.get_view_titles(),
4806
4901
  **super().get_extra_context(request, instance),
4807
4902
  }
4808
4903
 
@@ -5089,7 +5184,6 @@ class VirtualChassisUIViewSet(NautobotUIViewSet):
5089
5184
  url_path="add-member",
5090
5185
  url_name="add_member",
5091
5186
  custom_view_base_action="change",
5092
- custom_view_additional_permissions=["dcim.change_virtualchassis"],
5093
5187
  )
5094
5188
  def add_member(self, request, pk=None):
5095
5189
  virtual_chassis = self.get_object()
@@ -5286,7 +5380,9 @@ class PowerFeedUIViewSet(NautobotUIViewSet):
5286
5380
  items={
5287
5381
  "detail": [
5288
5382
  ModelBreadcrumbItem(),
5289
- InstanceBreadcrumbItem(instance=context_object_attr("power_panel.location")),
5383
+ AncestorsInstanceBreadcrumbItem(
5384
+ instance=context_object_attr("power_panel.location"), include_self=True
5385
+ ),
5290
5386
  InstanceBreadcrumbItem(instance=context_object_attr("power_panel")),
5291
5387
  InstanceBreadcrumbItem(
5292
5388
  instance=context_object_attr("rack"),
@@ -5522,7 +5618,8 @@ class InterfaceRedundancyGroupUIViewSet(NautobotUIViewSet):
5522
5618
  prefetch_related_fields=["interface"],
5523
5619
  order_by_fields=["priority"],
5524
5620
  table_title="Interfaces",
5525
- related_field_name="interface_redundancy_group",
5621
+ related_field_name="interface_redundancy_groups",
5622
+ related_list_url_name="dcim:interface_list",
5526
5623
  include_columns=[
5527
5624
  "interface__device",
5528
5625
  "interface",
@@ -5775,6 +5872,14 @@ class SoftwareVersionUIViewSet(NautobotUIViewSet):
5775
5872
  queryset = SoftwareVersion.objects.all()
5776
5873
  serializer_class = serializers.SoftwareVersionSerializer
5777
5874
  table_class = tables.SoftwareVersionTable
5875
+ breadcrumbs = Breadcrumbs(
5876
+ items={
5877
+ "detail": [
5878
+ ModelBreadcrumbItem(),
5879
+ InstanceBreadcrumbItem(instance=context_object_attr("platform")),
5880
+ ]
5881
+ }
5882
+ )
5778
5883
  object_detail_content = object_detail.ObjectDetailContent(
5779
5884
  panels=(
5780
5885
  object_detail.ObjectFieldsPanel(
@@ -5852,7 +5957,7 @@ class ControllerUIViewSet(NautobotUIViewSet):
5852
5957
  object_detail.DistinctViewTab(
5853
5958
  weight=700,
5854
5959
  tab_id="wireless_networks",
5855
- url_name="dcim:controller_wirelessnetworks",
5960
+ url_name="dcim:controller_wireless_networks",
5856
5961
  label="Wireless Networks",
5857
5962
  related_object_attribute="wireless_network_assignments",
5858
5963
  panels=(
@@ -5867,6 +5972,7 @@ class ControllerUIViewSet(NautobotUIViewSet):
5867
5972
  select_related_fields=["wireless_network"],
5868
5973
  exclude_columns=["controller"],
5869
5974
  include_paginator=True,
5975
+ enable_related_link=False,
5870
5976
  ),
5871
5977
  ),
5872
5978
  ),
@@ -5876,12 +5982,12 @@ class ControllerUIViewSet(NautobotUIViewSet):
5876
5982
  @action(
5877
5983
  detail=True,
5878
5984
  url_path="wireless-networks",
5879
- url_name="wirelessnetworks",
5985
+ url_name="wireless_networks",
5880
5986
  methods=["get"],
5881
5987
  custom_view_base_action="view",
5882
5988
  custom_view_additional_permissions=["wireless.view_controllermanageddevicegroupwirelessnetworkassignment"],
5883
5989
  )
5884
- def wirelessnetworks(self, request, *args, **kwargs):
5990
+ def wireless_networks(self, request, *args, **kwargs):
5885
5991
  return Response({})
5886
5992
 
5887
5993