nautobot 3.0.0a2__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 (659) hide show
  1. nautobot/apps/choices.py +4 -2
  2. nautobot/apps/filters.py +7 -9
  3. nautobot/apps/models.py +2 -2
  4. nautobot/apps/ui.py +13 -1
  5. nautobot/apps/utils.py +8 -0
  6. nautobot/circuits/filters.py +3 -2
  7. nautobot/circuits/navigation.py +3 -2
  8. nautobot/circuits/templates/circuits/circuit_create.html +3 -3
  9. nautobot/circuits/templates/circuits/circuittermination_create.html +9 -24
  10. nautobot/circuits/templates/circuits/inc/circuit_termination_cable_fragment.html +6 -6
  11. nautobot/circuits/templates/circuits/inc/speed_widget.html +12 -12
  12. nautobot/circuits/tests/integration/test_circuit.py +10 -13
  13. nautobot/circuits/tests/integration/test_circuits_bulk_operations.py +0 -3
  14. nautobot/circuits/views.py +6 -2
  15. nautobot/cloud/filters.py +1 -1
  16. nautobot/cloud/navigation.py +3 -2
  17. nautobot/core/api/schema.py +1 -1
  18. nautobot/core/api/serializers.py +6 -1
  19. nautobot/core/api/urls.py +2 -0
  20. nautobot/core/api/views.py +12 -0
  21. nautobot/core/apps/__init__.py +11 -10
  22. nautobot/core/celery/__init__.py +3 -5
  23. nautobot/core/checks.py +46 -0
  24. nautobot/core/choices.py +1 -1
  25. nautobot/core/cli/bootstrap_v3_to_v5.py +105 -13
  26. nautobot/core/cli/migrate_deprecated_templates.py +227 -0
  27. nautobot/core/constants.py +3 -0
  28. nautobot/core/context_processors.py +9 -1
  29. nautobot/core/filters.py +4 -0
  30. nautobot/core/forms/__init__.py +2 -0
  31. nautobot/core/forms/forms.py +1 -1
  32. nautobot/core/forms/widgets.py +21 -2
  33. nautobot/core/jobs/__init__.py +62 -3
  34. nautobot/core/jobs/groups.py +31 -1
  35. nautobot/core/management/commands/generate_test_data.py +28 -9
  36. nautobot/core/models/__init__.py +11 -0
  37. nautobot/core/models/generics.py +9 -1
  38. nautobot/core/models/tree_queries.py +10 -5
  39. nautobot/core/models/utils.py +1 -1
  40. nautobot/core/settings.py +35 -19
  41. nautobot/core/settings.yaml +17 -33
  42. nautobot/core/signals.py +12 -1
  43. nautobot/core/tables.py +13 -6
  44. nautobot/core/templates/40x.html +1 -1
  45. nautobot/core/templates/500.html +2 -2
  46. nautobot/core/templates/admin/base.html +1 -2
  47. nautobot/core/templates/admin/change_list.html +9 -12
  48. nautobot/core/templates/admin/config/config.html +12 -12
  49. nautobot/core/templates/admin/index.html +3 -3
  50. nautobot/core/templates/base_django.html +1 -2
  51. nautobot/core/templates/buttons/export.html +1 -1
  52. nautobot/core/templates/components/button/dropdown.html +5 -3
  53. nautobot/core/templates/components/panel/body_wrapper_generic_table.html +1 -1
  54. nautobot/core/templates/components/panel/header_extra_content_table.html +1 -1
  55. nautobot/core/templates/components/panel/panel.html +3 -3
  56. nautobot/core/templates/components/tab/content_wrapper.html +6 -7
  57. nautobot/core/templates/components/tab/label_wrapper_distinct_view.html +1 -1
  58. nautobot/core/templates/echarts/echarts.html +22 -9
  59. nautobot/core/templates/generic/object_bulk_add_component.html +2 -1
  60. nautobot/core/templates/generic/object_bulk_create.html +6 -5
  61. nautobot/core/templates/generic/object_bulk_delete.html +1 -1
  62. nautobot/core/templates/generic/object_bulk_destroy.html +3 -3
  63. nautobot/core/templates/generic/object_bulk_edit.html +1 -1
  64. nautobot/core/templates/generic/object_bulk_import.html +1 -1
  65. nautobot/core/templates/generic/object_bulk_remove.html +2 -2
  66. nautobot/core/templates/generic/object_bulk_update.html +5 -4
  67. nautobot/core/templates/generic/object_create.html +5 -4
  68. nautobot/core/templates/generic/object_delete.html +1 -1
  69. nautobot/core/templates/generic/object_detail.html +1 -1
  70. nautobot/core/templates/generic/object_edit.html +1 -1
  71. nautobot/core/templates/generic/object_import.html +2 -1
  72. nautobot/core/templates/generic/object_list.html +12 -4
  73. nautobot/core/templates/generic/object_notes.html +5 -3
  74. nautobot/core/templates/generic/object_retrieve.html +4 -5
  75. nautobot/core/templates/graphene/graphiql.html +7 -8
  76. nautobot/core/templates/home.html +1 -1
  77. nautobot/core/templates/import_success.html +2 -1
  78. nautobot/core/templates/inc/computed_fields/panel_data.html +1 -1
  79. nautobot/core/templates/inc/created_updated.html +7 -3
  80. nautobot/core/templates/inc/custom_fields/panel_data.html +1 -1
  81. nautobot/core/templates/inc/footer.html +3 -1
  82. nautobot/core/templates/inc/form_static_field.html +6 -0
  83. nautobot/core/templates/inc/header.html +11 -1
  84. nautobot/core/templates/inc/image_attachments.html +2 -1
  85. nautobot/core/templates/inc/media.html +14 -0
  86. nautobot/core/templates/inc/nav_menu.html +3 -9
  87. nautobot/core/templates/inc/object_details_advanced_panel.html +2 -2
  88. nautobot/core/templates/inc/search_panel.html +4 -4
  89. nautobot/core/templates/login.html +4 -2
  90. nautobot/core/templates/nautobot_config.py.j2 +6 -11
  91. nautobot/core/templates/redoc_ui.html +7 -0
  92. nautobot/core/templates/rest_framework/api.html +103 -2
  93. nautobot/core/templates/search.html +1 -1
  94. nautobot/core/templates/swagger_ui.html +17 -3
  95. nautobot/core/templates/system_jobs/import_objects.html +1 -2
  96. nautobot/core/templates/utilities/confirmation_form.html +2 -2
  97. nautobot/core/templates/utilities/obj_table.html +10 -2
  98. nautobot/core/templates/utilities/render_field.html +7 -7
  99. nautobot/core/templates/utilities/render_jinja2.html +2 -2
  100. nautobot/core/templates/utilities/templatetags/filter_form_drawer.html +37 -4
  101. nautobot/core/templates/utilities/theme_preview.html +19 -3
  102. nautobot/core/templates/widgets/number_input_with_choices.html +44 -0
  103. nautobot/core/templates/widgets/selectwithdisabled_option.html +3 -1
  104. nautobot/core/templatetags/helpers.py +76 -18
  105. nautobot/core/testing/api.py +68 -9
  106. nautobot/core/testing/filters.py +0 -23
  107. nautobot/core/testing/integration.py +41 -17
  108. nautobot/core/testing/mixins.py +2 -0
  109. nautobot/core/testing/utils.py +18 -4
  110. nautobot/core/testing/views.py +104 -13
  111. nautobot/core/tests/integration/test_app_home.py +34 -30
  112. nautobot/core/tests/integration/test_app_navbar.py +3 -0
  113. nautobot/core/tests/integration/test_filters.py +48 -11
  114. nautobot/core/tests/integration/test_theme.py +22 -21
  115. nautobot/core/tests/nautobot_config.py +3 -0
  116. nautobot/core/tests/nautobot_config_without_example_apps.py +4 -0
  117. nautobot/core/tests/runner.py +8 -1
  118. nautobot/core/tests/test_api.py +5 -3
  119. nautobot/core/tests/test_breadcrumbs.py +27 -28
  120. nautobot/core/tests/test_checks.py +28 -0
  121. nautobot/core/tests/test_cli.py +40 -0
  122. nautobot/core/tests/test_config.py +2 -1
  123. nautobot/core/tests/test_forms.py +55 -13
  124. nautobot/core/tests/test_jobs.py +144 -3
  125. nautobot/core/tests/test_nautobot_server.py +2 -0
  126. nautobot/core/tests/test_navigations.py +76 -1
  127. nautobot/core/tests/test_patch_social_django.py +42 -0
  128. nautobot/core/tests/test_renderers.py +59 -0
  129. nautobot/core/tests/test_settings_schema.py +1 -0
  130. nautobot/core/tests/test_tables.py +3 -1
  131. nautobot/core/tests/test_templatetags_helpers.py +62 -13
  132. nautobot/core/tests/test_templatetags_ui_framework.py +4 -4
  133. nautobot/core/tests/test_titles.py +0 -16
  134. nautobot/core/tests/test_tree_queries.py +14 -1
  135. nautobot/core/tests/test_ui.py +123 -4
  136. nautobot/core/tests/test_utils.py +72 -5
  137. nautobot/core/tests/test_views.py +159 -31
  138. nautobot/core/ui/breadcrumbs.py +70 -29
  139. nautobot/core/ui/bulk_buttons.py +1 -1
  140. nautobot/core/ui/choices.py +143 -27
  141. nautobot/core/ui/constants.py +76 -12
  142. nautobot/core/ui/echarts.py +15 -20
  143. nautobot/core/ui/object_detail.py +143 -55
  144. nautobot/core/ui/titles.py +3 -6
  145. nautobot/core/urls.py +20 -9
  146. nautobot/core/utils/cache.py +2 -1
  147. nautobot/core/utils/filtering.py +28 -18
  148. nautobot/core/utils/lookup.py +49 -8
  149. nautobot/core/utils/module_loading.py +21 -0
  150. nautobot/core/utils/patch_social_django.py +128 -0
  151. nautobot/core/views/__init__.py +38 -1
  152. nautobot/core/views/generic.py +3 -3
  153. nautobot/core/views/mixins.py +45 -22
  154. nautobot/core/views/renderers.py +4 -3
  155. nautobot/core/views/viewsets.py +2 -1
  156. nautobot/data_validation/apps.py +1 -5
  157. nautobot/data_validation/custom_validators.py +4 -4
  158. nautobot/data_validation/filters.py +1 -1
  159. nautobot/data_validation/forms.py +40 -0
  160. nautobot/data_validation/migrations/0001_initial.py +0 -7
  161. nautobot/data_validation/migrations/0002_data_migration_from_app.py +3 -14
  162. nautobot/data_validation/models.py +16 -7
  163. nautobot/data_validation/navigation.py +8 -1
  164. nautobot/data_validation/tables.py +12 -5
  165. nautobot/data_validation/templates/data_validation/datacompliance_tab.html +1 -0
  166. nautobot/data_validation/templates/data_validation/device_constraints.html +61 -0
  167. nautobot/data_validation/tests/__init__.py +2 -2
  168. nautobot/data_validation/tests/migrations/test_migrations.py +83 -3
  169. nautobot/data_validation/tests/test_data_compliance_rules.py +12 -7
  170. nautobot/data_validation/tests/test_filters.py +8 -6
  171. nautobot/data_validation/tests/test_models.py +15 -0
  172. nautobot/data_validation/tests/test_views.py +190 -32
  173. nautobot/data_validation/urls.py +2 -5
  174. nautobot/data_validation/views.py +73 -40
  175. nautobot/dcim/api/serializers.py +3 -13
  176. nautobot/dcim/apps.py +4 -0
  177. nautobot/dcim/choices.py +65 -0
  178. nautobot/dcim/constants.py +7 -0
  179. nautobot/dcim/custom_validators.py +84 -0
  180. nautobot/dcim/factory.py +1 -1
  181. nautobot/dcim/filter_mixins.py +353 -4
  182. nautobot/dcim/{filters/__init__.py → filters.py} +15 -36
  183. nautobot/dcim/forms.py +90 -4
  184. nautobot/dcim/migrations/0075_interface_duplex_interface_speed_and_more.py +32 -0
  185. nautobot/dcim/migrations/{0075_add_deviceclusterassignment.py → 0076_add_deviceclusterassignment.py} +1 -1
  186. nautobot/dcim/migrations/{0076_device_cluster_to_clusters_data_migration.py → 0077_device_cluster_to_clusters_data_migration.py} +1 -1
  187. nautobot/dcim/migrations/{0077_remove_device_cluster.py → 0078_remove_device_cluster.py} +1 -1
  188. nautobot/dcim/migrations/0079_remove_device_location_tenant_name_uniqueness.py +16 -0
  189. nautobot/dcim/migrations/0080_device_name_data_migration.py +59 -0
  190. nautobot/dcim/migrations/0081_alter_device_device_redundancy_group_priority_and_more.py +25 -0
  191. nautobot/dcim/models/device_component_templates.py +33 -1
  192. nautobot/dcim/models/device_components.py +98 -64
  193. nautobot/dcim/models/devices.py +30 -20
  194. nautobot/dcim/navigation.py +7 -6
  195. nautobot/dcim/tables/devices.py +18 -0
  196. nautobot/dcim/tables/devicetypes.py +8 -1
  197. nautobot/dcim/tables/racks.py +0 -2
  198. nautobot/dcim/tables/template_code.py +15 -15
  199. nautobot/dcim/templates/dcim/cable_connect.html +28 -112
  200. nautobot/dcim/templates/dcim/cable_trace.html +0 -4
  201. nautobot/dcim/templates/dcim/{cable_edit.html → cable_update.html} +1 -1
  202. nautobot/dcim/templates/dcim/consoleport.html +7 -6
  203. nautobot/dcim/templates/dcim/consoleserverport.html +7 -6
  204. nautobot/dcim/templates/dcim/device/config.html +2 -2
  205. nautobot/dcim/templates/dcim/device/lldp_neighbors.html +1 -1
  206. nautobot/dcim/templates/dcim/device/status.html +8 -8
  207. nautobot/dcim/templates/dcim/device.html +1 -1
  208. nautobot/dcim/templates/dcim/device_component_add.html +2 -2
  209. nautobot/dcim/templates/dcim/device_create.html +5 -3
  210. nautobot/dcim/templates/dcim/device_interface_delete.html +1 -1
  211. nautobot/dcim/templates/dcim/device_list.html +73 -10
  212. nautobot/dcim/templates/dcim/devicebay.html +1 -1
  213. nautobot/dcim/templates/dcim/devicebay_populate.html +2 -2
  214. nautobot/dcim/templates/dcim/devicetype_component_add.html +2 -2
  215. nautobot/dcim/templates/dcim/footer_convert_to_contact_or_team_record.html +14 -0
  216. nautobot/dcim/templates/dcim/frontport.html +10 -9
  217. nautobot/dcim/templates/dcim/inc/devicetype_component_table.html +1 -1
  218. nautobot/dcim/templates/dcim/inc/edit_form_softwareversion_js.html +2 -2
  219. nautobot/dcim/templates/dcim/inc/moduletype_component_table.html +1 -1
  220. nautobot/dcim/templates/dcim/inc/rack_elevation.html +1 -1
  221. nautobot/dcim/templates/dcim/interface.html +35 -7
  222. nautobot/dcim/templates/dcim/interface_bulk_delete.html +1 -1
  223. nautobot/dcim/templates/dcim/interface_edit.html +2 -0
  224. nautobot/dcim/templates/dcim/inventoryitem.html +1 -1
  225. nautobot/dcim/templates/dcim/inventoryitem_add.html +3 -1
  226. nautobot/dcim/templates/dcim/inventoryitem_bulk_delete.html +1 -1
  227. nautobot/dcim/templates/dcim/inventoryitem_edit.html +3 -1
  228. nautobot/dcim/templates/dcim/module/base.html +49 -9
  229. nautobot/dcim/templates/dcim/module_consoleports.html +1 -1
  230. nautobot/dcim/templates/dcim/module_consoleserverports.html +1 -1
  231. nautobot/dcim/templates/dcim/module_frontports.html +1 -1
  232. nautobot/dcim/templates/dcim/module_interfaces.html +1 -1
  233. nautobot/dcim/templates/dcim/module_list.html +57 -8
  234. nautobot/dcim/templates/dcim/module_modulebays.html +1 -1
  235. nautobot/dcim/templates/dcim/module_poweroutlets.html +1 -1
  236. nautobot/dcim/templates/dcim/module_powerports.html +1 -1
  237. nautobot/dcim/templates/dcim/module_rearports.html +1 -1
  238. nautobot/dcim/templates/dcim/modulefamily_retrieve.html +1 -1
  239. nautobot/dcim/templates/dcim/moduletype_list.html +2 -2
  240. nautobot/dcim/templates/dcim/moduletype_retrieve.html +49 -9
  241. nautobot/dcim/templates/dcim/platform_create.html +1 -1
  242. nautobot/dcim/templates/dcim/poweroutlet.html +1 -1
  243. nautobot/dcim/templates/dcim/powerport.html +6 -5
  244. nautobot/dcim/templates/dcim/rack_elevation_list.html +17 -5
  245. nautobot/dcim/templates/dcim/rack_retrieve.html +22 -15
  246. nautobot/dcim/templates/dcim/rearport.html +8 -7
  247. nautobot/dcim/templates/dcim/trace/cable.html +1 -1
  248. nautobot/dcim/templates/dcim/virtualchassis_add_member.html +16 -14
  249. nautobot/dcim/templates/dcim/virtualchassis_update.html +15 -7
  250. nautobot/dcim/tests/integration/test_controller.py +4 -6
  251. nautobot/dcim/tests/integration/test_controller_managed_device_group.py +1 -5
  252. nautobot/dcim/tests/integration/test_create_device.py +0 -2
  253. nautobot/dcim/tests/integration/test_device_bulk_operations.py +1 -3
  254. nautobot/dcim/tests/integration/test_fileinputpicker.py +6 -10
  255. nautobot/dcim/tests/integration/test_location_bulk_operations.py +0 -2
  256. nautobot/dcim/tests/integration/test_module_bay_position.py +3 -4
  257. nautobot/dcim/tests/test_api.py +194 -6
  258. nautobot/dcim/tests/test_custom_validators.py +229 -0
  259. nautobot/dcim/tests/test_filters.py +55 -7
  260. nautobot/dcim/tests/test_forms.py +110 -8
  261. nautobot/dcim/tests/test_graphql.py +44 -1
  262. nautobot/dcim/tests/test_models.py +328 -4
  263. nautobot/dcim/tests/test_tables.py +160 -0
  264. nautobot/dcim/tests/test_views.py +132 -29
  265. nautobot/dcim/urls.py +64 -21
  266. nautobot/dcim/utils.py +3 -3
  267. nautobot/dcim/views.py +777 -397
  268. nautobot/extras/api/views.py +60 -45
  269. nautobot/extras/choices.py +2 -13
  270. nautobot/extras/datasources/git.py +3 -1
  271. nautobot/extras/{filters/mixins.py → filter_mixins.py} +1 -1
  272. nautobot/extras/{filters/customfields.py → filter_mixins_customfields.py} +42 -6
  273. nautobot/extras/{filters/__init__.py → filters.py} +33 -48
  274. nautobot/extras/forms/forms.py +14 -15
  275. nautobot/extras/forms/mixins.py +0 -41
  276. nautobot/extras/jobs.py +2 -0
  277. nautobot/extras/jobs_ui.py +4 -3
  278. nautobot/extras/management/__init__.py +11 -0
  279. nautobot/extras/management/commands/refresh_dynamic_group_member_caches.py +4 -1
  280. nautobot/extras/migrations/0127_approval_workflow_models.py +6 -6
  281. nautobot/extras/migrations/0129_jobresult_debug_log_count_jobresult_error_log_count_and_more.py +37 -0
  282. nautobot/extras/migrations/0130_jobresult_generate_log_entry_counts.py +42 -0
  283. nautobot/extras/migrations/0131_configcontext_device_families.py +18 -0
  284. nautobot/extras/models/__init__.py +1 -2
  285. nautobot/extras/models/approvals.py +33 -14
  286. nautobot/extras/models/change_logging.py +4 -0
  287. nautobot/extras/models/contacts.py +2 -0
  288. nautobot/extras/models/groups.py +44 -5
  289. nautobot/extras/models/jobs.py +60 -4
  290. nautobot/extras/models/mixins.py +28 -0
  291. nautobot/extras/models/models.py +23 -2
  292. nautobot/extras/models/secrets.py +1 -0
  293. nautobot/extras/models/statuses.py +0 -15
  294. nautobot/extras/navigation.py +13 -9
  295. nautobot/extras/plugins/__init__.py +33 -55
  296. nautobot/extras/plugins/marketplace_manifest.yml +49 -1
  297. nautobot/extras/plugins/tables.py +3 -3
  298. nautobot/extras/plugins/urls.py +2 -21
  299. nautobot/extras/plugins/utils.py +1 -33
  300. nautobot/extras/plugins/views.py +0 -9
  301. nautobot/extras/querysets.py +8 -0
  302. nautobot/extras/signals.py +20 -19
  303. nautobot/extras/tables.py +64 -68
  304. nautobot/extras/templates/django_ajax_tables/ajax_wrapper.html +2 -0
  305. nautobot/extras/templates/extras/approval_dashboard.html +7 -5
  306. nautobot/extras/templates/extras/approvalworkflowdefinition_update.html +4 -2
  307. nautobot/extras/templates/extras/approvalworkflowstage_retrieve.html +20 -12
  308. nautobot/extras/templates/extras/configcontext_update.html +1 -0
  309. nautobot/extras/templates/extras/configcontextschema_validation.html +2 -2
  310. nautobot/extras/templates/extras/dynamicgroup_retrieve.html +11 -5
  311. nautobot/extras/templates/extras/dynamicgroup_update.html +1 -1
  312. nautobot/extras/templates/extras/gitrepository_result.html +0 -2
  313. nautobot/extras/templates/extras/inc/approval_buttons_column.html +20 -6
  314. nautobot/extras/templates/extras/inc/bulk_edit_overridable_field.html +8 -7
  315. nautobot/extras/templates/extras/inc/configcontext_format.html +10 -3
  316. nautobot/extras/templates/extras/inc/graphqlquery_execute.html +71 -0
  317. nautobot/extras/templates/extras/inc/job_tiles.html +15 -3
  318. nautobot/extras/templates/extras/inc/json_format.html +10 -3
  319. nautobot/extras/templates/extras/inc/overridable_field.html +13 -12
  320. nautobot/extras/templates/extras/job.html +29 -12
  321. nautobot/extras/templates/extras/job_bulk_edit.html +18 -0
  322. nautobot/extras/templates/extras/job_edit.html +52 -46
  323. nautobot/extras/templates/extras/job_list.html +29 -25
  324. nautobot/extras/templates/extras/marketplace.html +5 -9
  325. nautobot/extras/templates/extras/object_configcontext.html +1 -1
  326. nautobot/extras/templates/extras/object_dynamicgroups.html +2 -2
  327. nautobot/extras/templates/extras/objectchange_retrieve.html +19 -39
  328. nautobot/extras/templates/extras/plugin_detail.html +29 -24
  329. nautobot/extras/templates/extras/plugins_list.html +16 -26
  330. nautobot/extras/templates/extras/role_retrieve.html +64 -0
  331. nautobot/extras/templates/extras/scheduledjob.html +4 -2
  332. nautobot/extras/templates/extras/secret_create.html +1 -1
  333. nautobot/extras/templatetags/custom_links.py +12 -12
  334. nautobot/extras/templatetags/job_buttons.py +14 -12
  335. nautobot/extras/test_jobs/invalid_import.py +9 -0
  336. nautobot/extras/test_jobs/log_counts_by_level.py +23 -0
  337. nautobot/extras/test_jobs/missing_import.py +11 -0
  338. nautobot/extras/tests/integration/test_computedfields.py +8 -9
  339. nautobot/extras/tests/integration/test_configcontextschema.py +27 -26
  340. nautobot/extras/tests/integration/test_customfields.py +9 -10
  341. nautobot/extras/tests/integration/test_dynamicgroups.py +12 -9
  342. nautobot/extras/tests/integration/test_plugin_banner.py +3 -0
  343. nautobot/extras/tests/integration/test_plugins.py +18 -6
  344. nautobot/extras/tests/integration/test_relationships.py +0 -2
  345. nautobot/extras/tests/test_api.py +90 -18
  346. nautobot/extras/tests/test_approvals.py +38 -38
  347. nautobot/extras/tests/test_changelog.py +59 -5
  348. nautobot/extras/tests/test_customfields.py +22 -13
  349. nautobot/extras/tests/test_customfields_filters.py +479 -0
  350. nautobot/extras/tests/test_dynamicgroups.py +39 -1
  351. nautobot/extras/tests/test_filters.py +57 -22
  352. nautobot/extras/tests/test_forms.py +18 -21
  353. nautobot/extras/tests/test_jobs.py +25 -4
  354. nautobot/extras/tests/test_migrations.py +1 -0
  355. nautobot/extras/tests/test_models.py +51 -33
  356. nautobot/extras/tests/test_plugins.py +36 -10
  357. nautobot/extras/tests/test_utils.py +3 -4
  358. nautobot/extras/tests/test_views.py +52 -112
  359. nautobot/extras/urls.py +0 -14
  360. nautobot/extras/views.py +164 -71
  361. nautobot/ipam/factory.py +7 -0
  362. nautobot/ipam/filter_mixins.py +38 -0
  363. nautobot/ipam/filters.py +53 -38
  364. nautobot/ipam/formfields.py +1 -1
  365. nautobot/ipam/forms.py +6 -3
  366. nautobot/ipam/migrations/0030_ipam__namespaces.py +13 -0
  367. nautobot/ipam/migrations/0031_ipam___data_migrations.py +4 -1
  368. nautobot/ipam/migrations/0054_namespace_tenant.py +25 -0
  369. nautobot/ipam/models.py +29 -2
  370. nautobot/ipam/navigation.py +3 -2
  371. nautobot/ipam/signals.py +71 -0
  372. nautobot/ipam/tables.py +19 -6
  373. nautobot/ipam/templates/ipam/inc/toggle_available.html +10 -10
  374. nautobot/ipam/templates/ipam/inc/vlangroup_header.html +1 -0
  375. nautobot/ipam/templates/ipam/ipaddress.html +14 -0
  376. nautobot/ipam/templates/ipam/ipaddress_merge.html +3 -3
  377. nautobot/ipam/templates/ipam/ipaddresstointerface_retrieve.html +1 -0
  378. nautobot/ipam/templates/ipam/namespace_ip_addresses.html +1 -1
  379. nautobot/ipam/templates/ipam/namespace_prefixes.html +1 -1
  380. nautobot/ipam/templates/ipam/namespace_update.html +15 -0
  381. nautobot/ipam/templates/ipam/namespace_vrfs.html +1 -1
  382. nautobot/ipam/templates/ipam/prefix_delete.html +1 -1
  383. nautobot/ipam/templates/ipam/prefix_list.html +14 -13
  384. nautobot/ipam/templates/ipam/vlan_interfaces.html +1 -1
  385. nautobot/ipam/templates/ipam/vlan_vminterfaces.html +1 -1
  386. nautobot/ipam/tests/migration/test_migrations.py +89 -0
  387. nautobot/ipam/tests/test_api.py +13 -6
  388. nautobot/ipam/tests/test_filters.py +36 -1
  389. nautobot/ipam/tests/test_forms.py +1 -1
  390. nautobot/ipam/tests/test_models.py +44 -2
  391. nautobot/ipam/tests/test_tables.py +1 -2
  392. nautobot/ipam/tests/test_utils.py +1 -1
  393. nautobot/ipam/tests/test_views.py +13 -14
  394. nautobot/ipam/ui.py +0 -17
  395. nautobot/ipam/utils/migrations.py +16 -2
  396. nautobot/ipam/utils/testing.py +9 -3
  397. nautobot/ipam/views.py +53 -11
  398. nautobot/load_balancers/__init__.py +0 -0
  399. nautobot/load_balancers/api/__init__.py +1 -0
  400. nautobot/load_balancers/api/serializers.py +75 -0
  401. nautobot/load_balancers/api/urls.py +23 -0
  402. nautobot/load_balancers/api/views.py +61 -0
  403. nautobot/load_balancers/apps.py +17 -0
  404. nautobot/load_balancers/choices.py +167 -0
  405. nautobot/load_balancers/filters.py +225 -0
  406. nautobot/load_balancers/forms.py +532 -0
  407. nautobot/load_balancers/management/commands/__init__.py +0 -0
  408. nautobot/load_balancers/management/commands/generate_load_balancer_models_test_data.py +38 -0
  409. nautobot/load_balancers/migrations/0001_initial.py +465 -0
  410. nautobot/load_balancers/migrations/0002_create_default_statuses_pool_members.py +31 -0
  411. nautobot/load_balancers/migrations/__init__.py +0 -0
  412. nautobot/load_balancers/models.py +423 -0
  413. nautobot/load_balancers/navigation.py +80 -0
  414. nautobot/load_balancers/tables.py +255 -0
  415. nautobot/load_balancers/tests/__init__.py +474 -0
  416. nautobot/load_balancers/tests/test_api.py +353 -0
  417. nautobot/load_balancers/tests/test_filters.py +134 -0
  418. nautobot/load_balancers/tests/test_forms.py +266 -0
  419. nautobot/load_balancers/tests/test_models.py +195 -0
  420. nautobot/load_balancers/tests/test_views.py +229 -0
  421. nautobot/load_balancers/urls.py +17 -0
  422. nautobot/load_balancers/views.py +248 -0
  423. nautobot/project-static/dist/css/github-dark.min.css +10 -0
  424. nautobot/project-static/dist/css/github.min.css +10 -0
  425. nautobot/project-static/dist/css/nautobot.css +1 -11
  426. nautobot/project-static/dist/css/nautobot.css.map +1 -1
  427. nautobot/project-static/dist/js/libraries.js +1 -1
  428. nautobot/project-static/dist/js/libraries.js.map +1 -1
  429. nautobot/project-static/dist/js/nautobot.js +1 -1
  430. nautobot/project-static/dist/js/nautobot.js.map +1 -1
  431. nautobot/project-static/js/cabletrace.js +1 -1
  432. nautobot/project-static/js/forms.js +13 -0
  433. nautobot/project-static/js/interface_filtering.js +20 -16
  434. nautobot/project-static/nautobot-icons/battery-3.svg +3 -0
  435. nautobot/project-static/nautobot-icons/bus-globe.svg +3 -0
  436. nautobot/project-static/nautobot-icons/bus-shield-check.svg +3 -0
  437. nautobot/project-static/nautobot-icons/bus-shield.svg +3 -0
  438. nautobot/project-static/nautobot-icons/cloud.svg +1 -1
  439. nautobot/project-static/nautobot-icons/control-panel.svg +1 -1
  440. nautobot/project-static/nautobot-icons/device-lifecycle.svg +1 -1
  441. nautobot/project-static/nautobot-icons/elements.svg +1 -1
  442. nautobot/project-static/nautobot-icons/extensibility.svg +3 -0
  443. nautobot/project-static/nautobot-icons/hammer.svg +1 -1
  444. nautobot/project-static/nautobot-icons/organization.svg +3 -0
  445. nautobot/project-static/nautobot-icons/secrets.svg +1 -1
  446. nautobot/project-static/nautobot-icons/security.svg +3 -0
  447. nautobot/project-static/nautobot-icons/server.svg +1 -1
  448. nautobot/project-static/nautobot-icons/star-filled.svg +1 -1
  449. nautobot/project-static/nautobot-icons/star.svg +1 -1
  450. nautobot/tenancy/api/serializers.py +1 -0
  451. nautobot/tenancy/api/views.py +2 -1
  452. nautobot/tenancy/{filters/__init__.py → filters.py} +2 -10
  453. nautobot/tenancy/navigation.py +3 -1
  454. nautobot/tenancy/tests/test_filters.py +0 -2
  455. nautobot/tenancy/views.py +2 -1
  456. nautobot/ui/package-lock.json +87 -4
  457. nautobot/ui/package.json +2 -1
  458. nautobot/ui/src/js/collapse.js +3 -3
  459. nautobot/ui/src/js/nautobot.js +16 -1
  460. nautobot/ui/src/js/select2.js +53 -2
  461. nautobot/ui/src/scss/colors.scss +1 -1
  462. nautobot/ui/src/scss/nautobot.scss +112 -30
  463. nautobot/ui/webpack.config.js +13 -0
  464. nautobot/users/templates/users/preferences.html +11 -2
  465. nautobot/users/templates/users/profile.html +45 -12
  466. nautobot/users/templates/users/sessionkey_delete.html +1 -1
  467. nautobot/users/tests/test_api.py +4 -0
  468. nautobot/users/views.py +4 -2
  469. nautobot/virtualization/filters.py +6 -1
  470. nautobot/virtualization/models.py +1 -68
  471. nautobot/virtualization/navigation.py +3 -2
  472. nautobot/virtualization/templates/virtualization/virtual_machine_vminterface_delete.html +1 -1
  473. nautobot/virtualization/templates/virtualization/virtualmachine_list.html +2 -2
  474. nautobot/virtualization/templates/virtualization/virtualmachine_update.html +3 -1
  475. nautobot/virtualization/tests/test_api.py +3 -0
  476. nautobot/virtualization/tests/test_filters.py +10 -1
  477. nautobot/virtualization/tests/test_models.py +45 -4
  478. nautobot/virtualization/views.py +4 -1
  479. nautobot/vpn/__init__.py +0 -0
  480. nautobot/vpn/api/serializers.py +113 -0
  481. nautobot/vpn/api/urls.py +19 -0
  482. nautobot/vpn/api/views.py +70 -0
  483. nautobot/vpn/apps.py +8 -0
  484. nautobot/vpn/choices.py +171 -0
  485. nautobot/vpn/factory.py +219 -0
  486. nautobot/vpn/filters.py +234 -0
  487. nautobot/vpn/forms.py +487 -0
  488. nautobot/vpn/homepage.py +19 -0
  489. nautobot/vpn/migrations/0001_initial.py +541 -0
  490. nautobot/vpn/migrations/0002_populate_defaults.py +199 -0
  491. nautobot/vpn/migrations/__init__.py +0 -0
  492. nautobot/vpn/models.py +535 -0
  493. nautobot/vpn/navigation.py +98 -0
  494. nautobot/vpn/tables.py +383 -0
  495. nautobot/vpn/templates/vpn/vpnprofile_create.html +150 -0
  496. nautobot/vpn/tests/__init__.py +0 -0
  497. nautobot/vpn/tests/test_api.py +336 -0
  498. nautobot/vpn/tests/test_filters.py +139 -0
  499. nautobot/vpn/tests/test_forms.py +293 -0
  500. nautobot/vpn/tests/test_models.py +147 -0
  501. nautobot/vpn/tests/test_views.py +300 -0
  502. nautobot/vpn/urls.py +16 -0
  503. nautobot/vpn/views.py +495 -0
  504. nautobot/wireless/navigation.py +3 -2
  505. nautobot/wireless/tests/integration/test_radio_profile.py +1 -5
  506. nautobot/wireless/tests/test_api.py +1 -1
  507. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/METADATA +15 -15
  508. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/RECORD +514 -572
  509. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/entry_points.txt +1 -0
  510. nautobot/circuits/templates/circuits/circuit.html +0 -2
  511. nautobot/circuits/templates/circuits/circuit_edit.html +0 -2
  512. nautobot/circuits/templates/circuits/circuit_retrieve.html +0 -2
  513. nautobot/circuits/templates/circuits/circuit_update.html +0 -1
  514. nautobot/circuits/templates/circuits/circuittermination.html +0 -2
  515. nautobot/circuits/templates/circuits/circuittermination_edit.html +0 -2
  516. nautobot/circuits/templates/circuits/circuittermination_retrieve.html +0 -2
  517. nautobot/circuits/templates/circuits/circuittermination_update.html +0 -1
  518. nautobot/circuits/templates/circuits/circuittype.html +0 -2
  519. nautobot/circuits/templates/circuits/circuittype_retrieve.html +0 -2
  520. nautobot/circuits/templates/circuits/inc/circuit_termination.html +0 -85
  521. nautobot/circuits/templates/circuits/provider.html +0 -2
  522. nautobot/circuits/templates/circuits/provider_edit.html +0 -2
  523. nautobot/circuits/templates/circuits/provider_retrieve.html +0 -1
  524. nautobot/circuits/templates/circuits/provider_update.html +0 -1
  525. nautobot/circuits/templates/circuits/providernetwork.html +0 -2
  526. nautobot/circuits/templates/circuits/providernetwork_retrieve.html +0 -2
  527. nautobot/cloud/templates/cloud/cloudaccount_retrieve.html +0 -2
  528. nautobot/cloud/templates/cloud/cloudnetwork_retrieve.html +0 -2
  529. nautobot/cloud/templates/cloud/cloudresourcetype_retrieve.html +0 -2
  530. nautobot/cloud/templates/cloud/cloudservice_retrieve.html +0 -2
  531. nautobot/core/templates/buttons/import.html +0 -9
  532. nautobot/data_validation/template_content.py +0 -42
  533. nautobot/data_validation/templates/data_validation/datacompliance_retrieve.html +0 -1
  534. nautobot/dcim/filters/mixins.py +0 -354
  535. nautobot/dcim/templates/dcim/controller/base.html +0 -2
  536. nautobot/dcim/templates/dcim/controller_retrieve.html +0 -2
  537. nautobot/dcim/templates/dcim/controller_wirelessnetworks.html +0 -2
  538. nautobot/dcim/templates/dcim/controllermanageddevicegroup_retrieve.html +0 -2
  539. nautobot/dcim/templates/dcim/device/base.html +0 -2
  540. nautobot/dcim/templates/dcim/device/consoleports.html +0 -2
  541. nautobot/dcim/templates/dcim/device/consoleserverports.html +0 -2
  542. nautobot/dcim/templates/dcim/device/devicebays.html +0 -2
  543. nautobot/dcim/templates/dcim/device/frontports.html +0 -2
  544. nautobot/dcim/templates/dcim/device/interfaces.html +0 -2
  545. nautobot/dcim/templates/dcim/device/inventory.html +0 -2
  546. nautobot/dcim/templates/dcim/device/modulebays.html +0 -2
  547. nautobot/dcim/templates/dcim/device/poweroutlets.html +0 -2
  548. nautobot/dcim/templates/dcim/device/powerports.html +0 -2
  549. nautobot/dcim/templates/dcim/device/rearports.html +0 -2
  550. nautobot/dcim/templates/dcim/device/wireless.html +0 -2
  551. nautobot/dcim/templates/dcim/device_component.html +0 -2
  552. nautobot/dcim/templates/dcim/device_edit.html +0 -2
  553. nautobot/dcim/templates/dcim/devicefamily_retrieve.html +0 -2
  554. nautobot/dcim/templates/dcim/deviceredundancygroup_retrieve.html +0 -2
  555. nautobot/dcim/templates/dcim/devicetype.html +0 -2
  556. nautobot/dcim/templates/dcim/devicetype_edit.html +0 -2
  557. nautobot/dcim/templates/dcim/devicetype_retrieve.html +0 -2
  558. nautobot/dcim/templates/dcim/inc/device_napalm_tabs.html +0 -1
  559. nautobot/dcim/templates/dcim/interfaceredundancygroup_retrieve.html +0 -2
  560. nautobot/dcim/templates/dcim/location.html +0 -2
  561. nautobot/dcim/templates/dcim/location_edit.html +0 -2
  562. nautobot/dcim/templates/dcim/location_retrieve.html +0 -243
  563. nautobot/dcim/templates/dcim/locationtype.html +0 -2
  564. nautobot/dcim/templates/dcim/locationtype_retrieve.html +0 -2
  565. nautobot/dcim/templates/dcim/manufacturer.html +0 -2
  566. nautobot/dcim/templates/dcim/modulebay_retrieve.html +0 -1
  567. nautobot/dcim/templates/dcim/platform.html +0 -2
  568. nautobot/dcim/templates/dcim/powerfeed.html +0 -2
  569. nautobot/dcim/templates/dcim/powerfeed_retrieve.html +0 -2
  570. nautobot/dcim/templates/dcim/powerpanel.html +0 -2
  571. nautobot/dcim/templates/dcim/powerpanel_edit.html +0 -2
  572. nautobot/dcim/templates/dcim/powerpanel_retrieve.html +0 -2
  573. nautobot/dcim/templates/dcim/rack.html +0 -2
  574. nautobot/dcim/templates/dcim/rack_edit.html +0 -2
  575. nautobot/dcim/templates/dcim/rackgroup.html +0 -2
  576. nautobot/dcim/templates/dcim/rackreservation.html +0 -2
  577. nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +0 -2
  578. nautobot/dcim/templates/dcim/softwareversion_retrieve.html +0 -2
  579. nautobot/dcim/templates/dcim/virtualchassis.html +0 -2
  580. nautobot/dcim/templates/dcim/virtualchassis_add.html +0 -2
  581. nautobot/dcim/templates/dcim/virtualchassis_edit.html +0 -2
  582. nautobot/dcim/templates/dcim/virtualchassis_retrieve.html +0 -2
  583. nautobot/dcim/templates/dcim/virtualdevicecontext_retrieve.html +0 -2
  584. nautobot/dcim/ui.py +0 -29
  585. nautobot/extras/templates/extras/computedfield.html +0 -2
  586. nautobot/extras/templates/extras/computedfield_retrieve.html +0 -2
  587. nautobot/extras/templates/extras/configcontext.html +0 -2
  588. nautobot/extras/templates/extras/configcontext_edit.html +0 -2
  589. nautobot/extras/templates/extras/configcontext_retrieve.html +0 -2
  590. nautobot/extras/templates/extras/configcontextschema.html +0 -2
  591. nautobot/extras/templates/extras/configcontextschema_edit.html +0 -2
  592. nautobot/extras/templates/extras/contact_retrieve.html +0 -2
  593. nautobot/extras/templates/extras/customfield.html +0 -2
  594. nautobot/extras/templates/extras/customfield_edit.html +0 -2
  595. nautobot/extras/templates/extras/customfield_retrieve.html +0 -2
  596. nautobot/extras/templates/extras/customlink.html +0 -2
  597. nautobot/extras/templates/extras/dynamicgroup.html +0 -2
  598. nautobot/extras/templates/extras/dynamicgroup_edit.html +0 -2
  599. nautobot/extras/templates/extras/exporttemplate.html +0 -2
  600. nautobot/extras/templates/extras/gitrepository.html +0 -2
  601. nautobot/extras/templates/extras/gitrepository_object_edit.html +0 -2
  602. nautobot/extras/templates/extras/graphqlquery.html +0 -2
  603. nautobot/extras/templates/extras/graphqlquery_list.html +0 -1
  604. nautobot/extras/templates/extras/graphqlquery_retrieve.html +0 -97
  605. nautobot/extras/templates/extras/job_detail.html +0 -2
  606. nautobot/extras/templates/extras/jobbutton_retrieve.html +0 -2
  607. nautobot/extras/templates/extras/jobhook.html +0 -2
  608. nautobot/extras/templates/extras/jobqueue_retrieve.html +0 -2
  609. nautobot/extras/templates/extras/jobresult.html +0 -2
  610. nautobot/extras/templates/extras/metadatatype_retrieve.html +0 -2
  611. nautobot/extras/templates/extras/note.html +0 -2
  612. nautobot/extras/templates/extras/note_retrieve.html +0 -1
  613. nautobot/extras/templates/extras/object_changelog.html +0 -2
  614. nautobot/extras/templates/extras/object_notes.html +0 -2
  615. nautobot/extras/templates/extras/objectchange.html +0 -2
  616. nautobot/extras/templates/extras/objectchange_list.html +0 -3
  617. nautobot/extras/templates/extras/relationship.html +0 -1
  618. nautobot/extras/templates/extras/secret.html +0 -1
  619. nautobot/extras/templates/extras/secret_edit.html +0 -1
  620. nautobot/extras/templates/extras/secretsgroup.html +0 -2
  621. nautobot/extras/templates/extras/secretsgroup_edit.html +0 -2
  622. nautobot/extras/templates/extras/secretsgroup_retrieve.html +0 -2
  623. nautobot/extras/templates/extras/status.html +0 -2
  624. nautobot/extras/templates/extras/tag.html +0 -2
  625. nautobot/extras/templates/extras/tag_edit.html +0 -2
  626. nautobot/extras/templates/extras/tag_retrieve.html +0 -2
  627. nautobot/extras/templates/extras/team_retrieve.html +0 -2
  628. nautobot/ipam/templates/ipam/inc/prefix_header_extra_content_table.html +0 -4
  629. nautobot/ipam/templates/ipam/namespace_retrieve.html +0 -1
  630. nautobot/ipam/templates/ipam/prefix.html +0 -2
  631. nautobot/ipam/templates/ipam/prefix_edit.html +0 -1
  632. nautobot/ipam/templates/ipam/prefix_retrieve.html +0 -2
  633. nautobot/ipam/templates/ipam/rir.html +0 -2
  634. nautobot/ipam/templates/ipam/routetarget.html +0 -1
  635. nautobot/ipam/templates/ipam/service.html +0 -2
  636. nautobot/ipam/templates/ipam/service_edit.html +0 -2
  637. nautobot/ipam/templates/ipam/service_retrieve.html +0 -2
  638. nautobot/ipam/templates/ipam/vlan.html +0 -2
  639. nautobot/ipam/templates/ipam/vlan_edit.html +0 -2
  640. nautobot/ipam/templates/ipam/vlan_retrieve.html +0 -2
  641. nautobot/ipam/templates/ipam/vlangroup.html +0 -2
  642. nautobot/ipam/templates/ipam/vrf.html +0 -1
  643. nautobot/tenancy/templates/tenancy/tenant.html +0 -2
  644. nautobot/tenancy/templates/tenancy/tenant_edit.html +0 -2
  645. nautobot/tenancy/templates/tenancy/tenantgroup.html +0 -2
  646. nautobot/tenancy/templates/tenancy/tenantgroup_retrieve.html +0 -1
  647. nautobot/virtualization/templates/virtualization/clustergroup.html +0 -2
  648. nautobot/virtualization/templates/virtualization/clustertype.html +0 -2
  649. nautobot/virtualization/templates/virtualization/virtualmachine.html +0 -2
  650. nautobot/virtualization/templates/virtualization/virtualmachine_edit.html +0 -2
  651. nautobot/virtualization/templates/virtualization/virtualmachine_retrieve.html +0 -2
  652. nautobot/wireless/templates/wireless/radioprofile_retrieve.html +0 -2
  653. nautobot/wireless/templates/wireless/supporteddatarate_retrieve.html +0 -2
  654. nautobot/wireless/templates/wireless/wirelessnetwork_retrieve.html +0 -2
  655. /nautobot/dcim/templates/dcim/{cable.html → cable_retrieve.html} +0 -0
  656. /nautobot/tenancy/{filters/mixins.py → filter_mixins.py} +0 -0
  657. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/LICENSE.txt +0 -0
  658. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/NOTICE +0 -0
  659. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/WHEEL +0 -0
nautobot/dcim/views.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from collections import OrderedDict
2
2
  from copy import deepcopy
3
+ from functools import partial
3
4
  import logging
4
5
  import re
5
6
  import uuid
@@ -20,7 +21,7 @@ from django.template import Context
20
21
  from django.template.loader import render_to_string
21
22
  from django.urls import reverse
22
23
  from django.utils.encoding import iri_to_uri
23
- from django.utils.html import format_html, mark_safe
24
+ from django.utils.html import format_html, format_html_join, mark_safe
24
25
  from django.utils.http import url_has_allowed_host_and_scheme, urlencode
25
26
  from django.views.generic import View
26
27
  from django_tables2 import RequestConfig
@@ -35,10 +36,10 @@ from nautobot.core.exceptions import AbortTransaction
35
36
  from nautobot.core.forms import BulkRenameForm, ConfirmationForm, ImportForm, restrict_form_fields
36
37
  from nautobot.core.models.querysets import count_related
37
38
  from nautobot.core.templatetags import helpers
38
- from nautobot.core.templatetags.helpers import has_perms
39
+ from nautobot.core.templatetags.helpers import bettertitle, has_perms
39
40
  from nautobot.core.ui import object_detail
40
41
  from nautobot.core.ui.breadcrumbs import (
41
- AncestorsBreadcrumbs,
42
+ AncestorsInstanceBreadcrumbItem,
42
43
  BaseBreadcrumbItem,
43
44
  Breadcrumbs,
44
45
  context_object_attr,
@@ -76,7 +77,6 @@ from nautobot.core.views.utils import common_detail_view_context, get_obj_from_c
76
77
  from nautobot.core.views.viewsets import NautobotUIViewSet
77
78
  from nautobot.dcim.choices import LocationDataToContactActionChoices
78
79
  from nautobot.dcim.forms import LocationMigrateDataToContactForm
79
- from nautobot.dcim.ui import RackBreadcrumbs
80
80
  from nautobot.dcim.utils import get_all_network_driver_mappings, render_software_version_and_image_files
81
81
  from nautobot.extras.models import ConfigContext, Contact, ContactAssociation, Role, Status, Team
82
82
  from nautobot.extras.tables import DynamicGroupTable, ImageAttachmentTable
@@ -91,6 +91,7 @@ from nautobot.ipam.tables import (
91
91
  from nautobot.ipam.utils import render_ip_with_nat
92
92
  from nautobot.virtualization.models import VirtualMachine
93
93
  from nautobot.virtualization.tables import ClusterTable, VirtualMachineTable
94
+ from nautobot.vpn.tables import VPNTunnelEndpointTable
94
95
  from nautobot.wireless.forms import ControllerManagedDeviceGroupWirelessNetworkFormSet
95
96
  from nautobot.wireless.tables import (
96
97
  BaseControllerManagedDeviceGroupWirelessNetworkAssignmentTable,
@@ -247,7 +248,14 @@ class LocationTypeUIViewSet(NautobotUIViewSet):
247
248
  form_class = forms.LocationTypeForm
248
249
  bulk_update_form_class = forms.LocationTypeBulkEditForm
249
250
  serializer_class = serializers.LocationSerializer
250
- breadcrumbs = AncestorsBreadcrumbs(detail_item_label=context_object_attr("name"))
251
+ breadcrumbs = Breadcrumbs(
252
+ items={
253
+ "detail": [
254
+ ModelBreadcrumbItem(),
255
+ AncestorsInstanceBreadcrumbItem(),
256
+ ]
257
+ }
258
+ )
251
259
 
252
260
  object_detail_content = object_detail.ObjectDetailContent(
253
261
  panels=(
@@ -278,6 +286,106 @@ class LocationTypeUIViewSet(NautobotUIViewSet):
278
286
  #
279
287
 
280
288
 
289
+ class LocationGeographicalInfoFieldsPanel(object_detail.ObjectFieldsPanel):
290
+ def get_data(self, context):
291
+ data = super().get_data(context)
292
+ obj = get_obj_from_context(context, self.context_object_key)
293
+
294
+ if obj and obj.latitude and obj.longitude:
295
+ data["GPS Coordinates"] = f"{obj.latitude}, {obj.longitude}"
296
+ else:
297
+ data["GPS Coordinates"] = None
298
+
299
+ return data
300
+
301
+ def render_value(self, key, value, context):
302
+ if key == "GPS Coordinates":
303
+ if value is not None:
304
+ return helpers.render_address(value)
305
+ return helpers.HTML_NONE
306
+
307
+ return super().render_value(key, value, context)
308
+
309
+
310
+ class LocationRackGroupsPanel(object_detail.Panel):
311
+ def render_rack_row(self, indent_px, url, name, count, elevation_url):
312
+ """Render a single <tr> for a rack group or summary row."""
313
+ return format_html(
314
+ """
315
+ <tr>
316
+ <td style="padding-left: {}px">
317
+ <span class="mdi mdi-folder-open"></span>
318
+ <a href="{}">{}</a>
319
+ </td>
320
+ <td>{}</td>
321
+ <td class="float-end d-print-none">
322
+ <a href="{}" class="btn btn-sm btn-primary" title="View elevations">
323
+ <span class="mdi mdi-server"></span>
324
+ </a>
325
+ </td>
326
+ </tr>
327
+ """,
328
+ indent_px,
329
+ url,
330
+ name,
331
+ count,
332
+ elevation_url,
333
+ )
334
+
335
+ def render_body_content(self, context):
336
+ """Render the <tbody> content for the Rack Groups table."""
337
+ obj = get_obj_from_context(context)
338
+ if not obj:
339
+ return ""
340
+
341
+ rack_groups = context.get("rack_groups", [])
342
+ rack_count = context.get("rack_count", 0)
343
+
344
+ rows = []
345
+
346
+ # Render each rack group row
347
+ for rack_group in rack_groups:
348
+ rows.append(
349
+ self.render_rack_row(
350
+ getattr(rack_group, "tree_depth", 0) * 8,
351
+ rack_group.get_absolute_url(),
352
+ str(rack_group),
353
+ rack_group.rack_count,
354
+ f"{reverse('dcim:rack_elevation_list')}?rack_group={rack_group.pk}",
355
+ )
356
+ )
357
+
358
+ # Add "All racks" row
359
+ rows.append(
360
+ self.render_rack_row(
361
+ 10,
362
+ "#",
363
+ "All racks",
364
+ rack_count,
365
+ f"{reverse('dcim:rack_elevation_list')}?location={obj.pk}",
366
+ )
367
+ )
368
+
369
+ return format_html_join("", "{}", ((row,) for row in rows))
370
+
371
+
372
+ class LocationImageAttachmentsTablePanel(object_detail.ObjectsTablePanel):
373
+ """
374
+ ObjectsTablePanel with a custom _get_table_add_url() implementation.
375
+
376
+ Needed because the URL is `/dcim/devices/<pk>/images/add/`, not `extras/image-attachments/add?location=<pk>`.
377
+ """
378
+
379
+ def _get_table_add_url(self, context):
380
+ obj = get_obj_from_context(context)
381
+ request = context["request"]
382
+ return_url = context.get("return_url", obj.get_absolute_url())
383
+
384
+ if not request.user.has_perms(["extras.add_imageattachment"]):
385
+ return None
386
+ return reverse("dcim:location_add_image", kwargs={"object_id": obj.pk}) + f"?return_url={return_url}"
387
+
388
+
281
389
  class LocationUIViewSet(NautobotUIViewSet):
282
390
  # We are only accessing the tree fields from the list view, where `with_tree_fields` is called dynamically
283
391
  # depending on whether the hierarchy is shown in the UI (note that `parent` itself is a normal foreign key, not a
@@ -291,72 +399,132 @@ class LocationUIViewSet(NautobotUIViewSet):
291
399
  form_class = forms.LocationForm
292
400
  bulk_update_form_class = forms.LocationBulkEditForm
293
401
  serializer_class = serializers.LocationSerializer
294
- breadcrumbs = AncestorsBreadcrumbs(detail_item_label=context_object_attr("name"))
295
-
296
- def get_extra_context(self, request, instance):
297
- if instance is None:
298
- return super().get_extra_context(request, instance)
299
- # This query can get really expensive when there are big location trees in the DB. By casting it to a list we
300
- # ensure it is only performed once rather than as a subquery for each of the different count stats.
301
- related_locations = list(
302
- instance.descendants(include_self=True).restrict(request.user, "view").values_list("pk", flat=True)
303
- )
304
- prefix_count_queryset = Prefix.objects.restrict(request.user, "view").filter(locations__in=related_locations)
305
- vlan_count_queryset = VLAN.objects.restrict(request.user, "view").filter(locations__in=related_locations)
306
- circuit_count_queryset = Circuit.objects.restrict(request.user, "view").filter(
307
- circuit_terminations__location__in=related_locations
308
- )
309
- # When there is more than one location, the models that can be assigned to more then one location at the same
310
- # time need to be queried with `distinct`. We are avoiding `distinct` when this is not the case, as it incurs
311
- # a performance penalty.
312
- if len(related_locations) > 1:
313
- prefix_count_queryset = prefix_count_queryset.distinct()
314
- vlan_count_queryset = vlan_count_queryset.distinct()
315
- circuit_count_queryset = circuit_count_queryset.distinct()
316
- stats = {
317
- "prefix_count": prefix_count_queryset.count(),
318
- "vlan_count": vlan_count_queryset.count(),
319
- "circuit_count": circuit_count_queryset.count(),
320
- "rack_count": Rack.objects.restrict(request.user, "view").filter(location__in=related_locations).count(),
321
- "device_count": Device.objects.restrict(request.user, "view")
322
- .filter(location__in=related_locations)
323
- .count(),
324
- "vm_count": VirtualMachine.objects.restrict(request.user, "view")
325
- .filter(cluster__location__in=related_locations)
326
- .count(),
402
+ breadcrumbs = Breadcrumbs(
403
+ items={
404
+ "detail": [
405
+ ModelBreadcrumbItem(),
406
+ AncestorsInstanceBreadcrumbItem(),
407
+ ]
327
408
  }
328
- rack_groups = (
329
- RackGroup.objects.annotate(rack_count=count_related(Rack, "rack_group"))
330
- .restrict(request.user, "view")
331
- .filter(location__in=related_locations)
332
- )
333
- children = (
334
- Location.objects.restrict(request.user, "view")
335
- # We aren't accessing tree fields anywhere so this is safe (note that `parent` itself is a normal foreign
336
- # key, not a tree field). If we ever do access tree fields, this will perform worse, because django will
337
- # automatically issue a second query (similar to behavior for
338
- # https://docs.djangoproject.com/en/3.2/ref/models/querysets/#django.db.models.query.QuerySet.only)
339
- .without_tree_fields()
340
- .filter(parent=instance)
341
- .select_related("parent", "location_type")
409
+ )
410
+ view_titles = Titles(titles={"detail": "{{ object.name }}"})
411
+
412
+ object_detail_content = object_detail.ObjectDetailContent(
413
+ panels=(
414
+ object_detail.ObjectFieldsPanel(
415
+ weight=100,
416
+ section=SectionChoices.LEFT_HALF,
417
+ fields=[
418
+ "location_type",
419
+ "status",
420
+ "parent",
421
+ "tenant",
422
+ "facility",
423
+ "asn",
424
+ "time_zone",
425
+ "description",
426
+ ],
427
+ value_transforms={
428
+ "location_type": [partial(helpers.hyperlinked_object, field="name")],
429
+ "time_zone": [helpers.format_timezone],
430
+ },
431
+ key_transforms={
432
+ "asn": "AS Number",
433
+ },
434
+ ),
435
+ LocationGeographicalInfoFieldsPanel(
436
+ weight=110,
437
+ section=SectionChoices.LEFT_HALF,
438
+ label="Geographical Info",
439
+ fields=[
440
+ "physical_address",
441
+ "shipping_address",
442
+ ],
443
+ value_transforms={
444
+ "physical_address": [helpers.render_address],
445
+ "shipping_address": [helpers.render_address],
446
+ },
447
+ ),
448
+ object_detail.ObjectFieldsPanel(
449
+ weight=120,
450
+ section=SectionChoices.LEFT_HALF,
451
+ label="Contact Info",
452
+ fields=["contact_name", "contact_phone", "contact_email"],
453
+ value_transforms={
454
+ "contact_phone": [helpers.hyperlinked_phone_number],
455
+ "contact_email": [helpers.hyperlinked_email],
456
+ },
457
+ footer_content_template_path="dcim/footer_convert_to_contact_or_team_record.html",
458
+ ),
459
+ object_detail.StatsPanel(
460
+ weight=100,
461
+ label="Stats",
462
+ section=SectionChoices.RIGHT_HALF,
463
+ filter_name="location",
464
+ related_models=[
465
+ Rack,
466
+ Device,
467
+ Prefix,
468
+ VLAN,
469
+ (Circuit, "circuit_terminations__location__in"),
470
+ (VirtualMachine, "cluster__location__in"),
471
+ ],
472
+ ),
473
+ LocationRackGroupsPanel(
474
+ label="Rack Groups",
475
+ section=SectionChoices.RIGHT_HALF,
476
+ weight=200,
477
+ body_wrapper_template_path="components/panel/body_wrapper_generic_table.html",
478
+ ),
479
+ LocationImageAttachmentsTablePanel(
480
+ weight=300,
481
+ section=SectionChoices.RIGHT_HALF,
482
+ table_title="Images",
483
+ table_class=ImageAttachmentTable,
484
+ table_attribute="images",
485
+ related_field_name="location",
486
+ show_table_config_button=False,
487
+ enable_related_link=False,
488
+ ),
489
+ object_detail.ObjectsTablePanel(
490
+ section=SectionChoices.FULL_WIDTH,
491
+ weight=100,
492
+ table_title="Children",
493
+ table_class=tables.LocationTable,
494
+ table_attribute="children",
495
+ related_field_name="parent",
496
+ order_by_fields=["name"],
497
+ hide_hierarchy_ui=True,
498
+ ),
342
499
  )
500
+ )
343
501
 
344
- children_table = tables.LocationTable(children, hide_hierarchy_ui=True)
345
- paginate = {
346
- "paginator_class": EnhancedPaginator,
347
- "per_page": get_paginate_count(request),
348
- }
349
- RequestConfig(request, paginate).configure(children_table)
502
+ def get_extra_context(self, request, instance):
503
+ context = super().get_extra_context(request, instance)
350
504
 
351
- return {
352
- "children_table": children_table,
353
- "rack_groups": rack_groups,
354
- "stats": stats,
355
- "contact_association_permission": ["extras.add_contactassociation"],
356
- # show the button if any of these fields have non-empty value.
357
- "show_convert_to_contact_button": instance.contact_name or instance.contact_phone or instance.contact_email,
358
- **super().get_extra_context(request, instance),
359
- }
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
+ )
511
+
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
+ )
517
+
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
360
528
 
361
529
 
362
530
  class MigrateLocationDataToContactView(generic.ObjectEditView):
@@ -506,6 +674,14 @@ class RackGroupUIViewSet(NautobotUIViewSet):
506
674
  serializer_class = serializers.RackGroupSerializer
507
675
  table_class = tables.RackGroupTable
508
676
  queryset = RackGroup.objects.all()
677
+ breadcrumbs = Breadcrumbs(
678
+ items={
679
+ "detail": [
680
+ ModelBreadcrumbItem(),
681
+ AncestorsInstanceBreadcrumbItem(),
682
+ ]
683
+ }
684
+ )
509
685
 
510
686
  object_detail_content = object_detail.ObjectDetailContent(
511
687
  panels=[
@@ -565,7 +741,26 @@ class RackUIViewSet(NautobotUIViewSet):
565
741
  serializer_class = serializers.RackSerializer
566
742
  table_class = tables.RackDetailTable
567
743
  queryset = Rack.objects.select_related("location", "tenant__tenant_group", "rack_group", "role")
568
- 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
+ )
569
764
 
570
765
  def get_extra_context(self, request, instance):
571
766
  context = super().get_extra_context(request, instance)
@@ -611,9 +806,6 @@ class RackElevationListView(generic.ObjectListView):
611
806
  action_buttons = []
612
807
  template_name = "dcim/rack_elevation_list.html"
613
808
  view_titles = Titles(titles={"list": "Rack Elevation"})
614
- breadcrumbs = Breadcrumbs(
615
- items={"list": [ViewNameBreadcrumbItem(view_name="dcim:rack_elevation_list", label="Rack Elevation")]}
616
- )
617
809
 
618
810
  def extra_context(self):
619
811
  racks = self.queryset
@@ -801,139 +993,6 @@ def bulk_cable_termination_footer_buttons(form_id: str, model):
801
993
  ]
802
994
 
803
995
 
804
- # --- Tab Configuration ---
805
- TAB_CONFIGS = [
806
- (
807
- 100,
808
- "interfaces",
809
- "Interfaces",
810
- "dcim:devicetype_interfaces",
811
- "interface_templates",
812
- tables.InterfaceTemplateTable,
813
- InterfaceTemplate,
814
- ),
815
- (
816
- 200,
817
- "frontports",
818
- "Front Ports",
819
- "dcim:devicetype_frontports",
820
- "front_port_templates",
821
- tables.FrontPortTemplateTable,
822
- FrontPortTemplate,
823
- ),
824
- (
825
- 300,
826
- "rearports",
827
- "Rear Ports",
828
- "dcim:devicetype_rearports",
829
- "rear_port_templates",
830
- tables.RearPortTemplateTable,
831
- RearPortTemplate,
832
- ),
833
- (
834
- 400,
835
- "consoleports",
836
- "Console Ports",
837
- "dcim:devicetype_consoleports",
838
- "console_port_templates",
839
- tables.ConsolePortTemplateTable,
840
- ConsolePortTemplate,
841
- ),
842
- (
843
- 500,
844
- "consoleserverports",
845
- "Console Server Ports",
846
- "dcim:devicetype_consoleserverports",
847
- "console_server_port_templates",
848
- tables.ConsoleServerPortTemplateTable,
849
- ConsoleServerPortTemplate,
850
- ),
851
- (
852
- 600,
853
- "powerports",
854
- "Power Ports",
855
- "dcim:devicetype_powerports",
856
- "power_port_templates",
857
- tables.PowerPortTemplateTable,
858
- PowerPortTemplate,
859
- ),
860
- (
861
- 700,
862
- "poweroutlets",
863
- "Power Outlets",
864
- "dcim:devicetype_poweroutlets",
865
- "power_outlet_templates",
866
- tables.PowerOutletTemplateTable,
867
- PowerOutletTemplate,
868
- ),
869
- (
870
- 800,
871
- "devicebays",
872
- "Device Bays",
873
- "dcim:devicetype_devicebays",
874
- "device_bay_templates",
875
- tables.DeviceBayTemplateTable,
876
- DeviceBayTemplate,
877
- ),
878
- (
879
- 900,
880
- "modulebays",
881
- "Module Bays",
882
- "dcim:devicetype_modulebays",
883
- "module_bay_templates",
884
- tables.ModuleBayTemplateTable,
885
- ModuleBayTemplate,
886
- ),
887
- ]
888
-
889
-
890
- # --- Add Components Button Config ---
891
- ADD_COMPONENTS_CONFIG = [
892
- (100, "dcim:consoleporttemplate_add", "Console Ports", "mdi-console", ["dcim.add_consoleporttemplate"]),
893
- (
894
- 200,
895
- "dcim:consoleserverporttemplate_add",
896
- "Console Server Ports",
897
- "mdi-console-network-outline",
898
- ["dcim.add_consoleserverporttemplate"],
899
- ),
900
- (300, "dcim:powerporttemplate_add", "Power Ports", "mdi-power-plug-outline", ["dcim.add_powerporttemplate"]),
901
- (400, "dcim:poweroutlettemplate_add", "Power Outlets", "mdi-power-socket", ["dcim.add_poweroutlettemplate"]),
902
- (500, "dcim:interfacetemplate_add", "Interfaces", "mdi-ethernet", ["dcim.add_interfacetemplate"]),
903
- (600, "dcim:frontporttemplate_add", "Front Ports", "mdi-square-rounded-outline", ["dcim.add_frontporttemplate"]),
904
- (700, "dcim:rearporttemplate_add", "Rear Ports", "mdi-square-rounded-outline", ["dcim.add_rearporttemplate"]),
905
- (800, "dcim:devicebaytemplate_add", "Device Bays", "mdi-circle-outline", ["dcim.add_devicebaytemplate"]),
906
- (900, "dcim:modulebaytemplate_add", "Module Bays", "mdi-tray", ["dcim.add_modulebaytemplate"]),
907
- ]
908
-
909
-
910
- def make_bulk_tab(weight, tab_name, label, url_name, related_attr, table_class, model):
911
- """Build a bulk-enabled tab."""
912
- form_id = f"{tab_name}template_form"
913
- return object_detail.DistinctViewTab(
914
- weight=weight,
915
- tab_id=tab_name,
916
- label=label,
917
- url_name=url_name,
918
- related_object_attribute=related_attr,
919
- hide_if_empty=True,
920
- panels=(
921
- object_detail.ObjectsTablePanel(
922
- section=SectionChoices.FULL_WIDTH,
923
- weight=100,
924
- table_title=label,
925
- table_class=table_class,
926
- table_filter="device_type",
927
- tab_id=tab_name,
928
- enable_bulk_actions=True,
929
- form_id=form_id,
930
- footer_buttons=bulk_footer_buttons(form_id=form_id, model=model),
931
- include_paginator=True,
932
- ),
933
- ),
934
- )
935
-
936
-
937
996
  # --- DeviceType UI ViewSet ---
938
997
  class DeviceTypeUIViewSet(NautobotUIViewSet):
939
998
  bulk_update_form_class = forms.DeviceTypeBulkEditForm
@@ -980,7 +1039,242 @@ class DeviceTypeUIViewSet(NautobotUIViewSet):
980
1039
  exclude_columns=["actions", "tags"],
981
1040
  ),
982
1041
  ),
983
- extra_tabs=tuple(make_bulk_tab(*cfg) for cfg in TAB_CONFIGS),
1042
+ extra_tabs=(
1043
+ object_detail.DistinctViewTab(
1044
+ weight=100,
1045
+ tab_id="interfaces",
1046
+ label="Interfaces",
1047
+ url_name="dcim:devicetype_interfaces",
1048
+ related_object_attribute="interface_templates",
1049
+ hide_if_empty=True,
1050
+ panels=(
1051
+ object_detail.ObjectsTablePanel(
1052
+ section=SectionChoices.FULL_WIDTH,
1053
+ weight=100,
1054
+ table_title="Interfaces",
1055
+ table_class=tables.InterfaceTemplateTable,
1056
+ table_filter="device_type",
1057
+ tab_id="interfaces",
1058
+ enable_bulk_actions=True,
1059
+ form_id="interfacestemplate_form",
1060
+ footer_buttons=bulk_footer_buttons(
1061
+ form_id="interfacestemplate_form",
1062
+ model=InterfaceTemplate,
1063
+ ),
1064
+ include_paginator=True,
1065
+ enable_related_link=False,
1066
+ ),
1067
+ ),
1068
+ ),
1069
+ object_detail.DistinctViewTab(
1070
+ weight=200,
1071
+ tab_id="frontports",
1072
+ label="Front Ports",
1073
+ url_name="dcim:devicetype_frontports",
1074
+ related_object_attribute="front_port_templates",
1075
+ hide_if_empty=True,
1076
+ panels=(
1077
+ object_detail.ObjectsTablePanel(
1078
+ section=SectionChoices.FULL_WIDTH,
1079
+ weight=100,
1080
+ table_title="Front Ports",
1081
+ table_class=tables.FrontPortTemplateTable,
1082
+ table_filter="device_type",
1083
+ tab_id="frontports",
1084
+ enable_bulk_actions=True,
1085
+ form_id="frontportstemplate_form",
1086
+ footer_buttons=bulk_footer_buttons(
1087
+ form_id="frontportstemplate_form",
1088
+ model=FrontPortTemplate,
1089
+ ),
1090
+ include_paginator=True,
1091
+ enable_related_link=False,
1092
+ ),
1093
+ ),
1094
+ ),
1095
+ object_detail.DistinctViewTab(
1096
+ weight=300,
1097
+ tab_id="rearports",
1098
+ label="Rear Ports",
1099
+ url_name="dcim:devicetype_rearports",
1100
+ related_object_attribute="rear_port_templates",
1101
+ hide_if_empty=True,
1102
+ panels=(
1103
+ object_detail.ObjectsTablePanel(
1104
+ section=SectionChoices.FULL_WIDTH,
1105
+ weight=100,
1106
+ table_title="Rear Ports",
1107
+ table_class=tables.RearPortTemplateTable,
1108
+ table_filter="device_type",
1109
+ tab_id="rearports",
1110
+ enable_bulk_actions=True,
1111
+ form_id="rearportstemplate_form",
1112
+ footer_buttons=bulk_footer_buttons(
1113
+ form_id="rearportstemplate_form",
1114
+ model=RearPortTemplate,
1115
+ ),
1116
+ include_paginator=True,
1117
+ enable_related_link=False,
1118
+ ),
1119
+ ),
1120
+ ),
1121
+ object_detail.DistinctViewTab(
1122
+ weight=400,
1123
+ tab_id="consoleports",
1124
+ label="Console Ports",
1125
+ url_name="dcim:devicetype_consoleports",
1126
+ related_object_attribute="console_port_templates",
1127
+ hide_if_empty=True,
1128
+ panels=(
1129
+ object_detail.ObjectsTablePanel(
1130
+ section=SectionChoices.FULL_WIDTH,
1131
+ weight=100,
1132
+ table_title="Console Ports",
1133
+ table_class=tables.ConsolePortTemplateTable,
1134
+ table_filter="device_type",
1135
+ tab_id="consoleports",
1136
+ enable_bulk_actions=True,
1137
+ form_id="consoleportstemplate_form",
1138
+ footer_buttons=bulk_footer_buttons(
1139
+ form_id="consoleportstemplate_form",
1140
+ model=ConsolePortTemplate,
1141
+ ),
1142
+ include_paginator=True,
1143
+ enable_related_link=False,
1144
+ ),
1145
+ ),
1146
+ ),
1147
+ object_detail.DistinctViewTab(
1148
+ weight=500,
1149
+ tab_id="consoleserverports",
1150
+ label="Console Server Ports",
1151
+ url_name="dcim:devicetype_consoleserverports",
1152
+ related_object_attribute="console_server_port_templates",
1153
+ hide_if_empty=True,
1154
+ panels=(
1155
+ object_detail.ObjectsTablePanel(
1156
+ section=SectionChoices.FULL_WIDTH,
1157
+ weight=100,
1158
+ table_title="Console Server Ports",
1159
+ table_class=tables.ConsoleServerPortTemplateTable,
1160
+ table_filter="device_type",
1161
+ tab_id="consoleserverports",
1162
+ enable_bulk_actions=True,
1163
+ form_id="consoleserverportstemplate_form",
1164
+ footer_buttons=bulk_footer_buttons(
1165
+ form_id="consoleserverportstemplate_form",
1166
+ model=ConsoleServerPortTemplate,
1167
+ ),
1168
+ include_paginator=True,
1169
+ enable_related_link=False,
1170
+ ),
1171
+ ),
1172
+ ),
1173
+ object_detail.DistinctViewTab(
1174
+ weight=600,
1175
+ tab_id="powerports",
1176
+ label="Power Ports",
1177
+ url_name="dcim:devicetype_powerports",
1178
+ related_object_attribute="power_port_templates",
1179
+ hide_if_empty=True,
1180
+ panels=(
1181
+ object_detail.ObjectsTablePanel(
1182
+ section=SectionChoices.FULL_WIDTH,
1183
+ weight=100,
1184
+ table_title="Power Ports",
1185
+ table_class=tables.PowerPortTemplateTable,
1186
+ table_filter="device_type",
1187
+ tab_id="powerports",
1188
+ enable_bulk_actions=True,
1189
+ form_id="powerportstemplate_form",
1190
+ footer_buttons=bulk_footer_buttons(
1191
+ form_id="powerportstemplate_form",
1192
+ model=PowerPortTemplate,
1193
+ ),
1194
+ include_paginator=True,
1195
+ enable_related_link=False,
1196
+ ),
1197
+ ),
1198
+ ),
1199
+ object_detail.DistinctViewTab(
1200
+ weight=700,
1201
+ tab_id="poweroutlets",
1202
+ label="Power Outlets",
1203
+ url_name="dcim:devicetype_poweroutlets",
1204
+ related_object_attribute="power_outlet_templates",
1205
+ hide_if_empty=True,
1206
+ panels=(
1207
+ object_detail.ObjectsTablePanel(
1208
+ section=SectionChoices.FULL_WIDTH,
1209
+ weight=100,
1210
+ table_title="Power Outlets",
1211
+ table_class=tables.PowerOutletTemplateTable,
1212
+ table_filter="device_type",
1213
+ tab_id="poweroutlets",
1214
+ enable_bulk_actions=True,
1215
+ form_id="poweroutletstemplate_form",
1216
+ footer_buttons=bulk_footer_buttons(
1217
+ form_id="poweroutletstemplate_form",
1218
+ model=PowerOutletTemplate,
1219
+ ),
1220
+ include_paginator=True,
1221
+ enable_related_link=False,
1222
+ ),
1223
+ ),
1224
+ ),
1225
+ object_detail.DistinctViewTab(
1226
+ weight=800,
1227
+ tab_id="devicebays",
1228
+ label="Device Bays",
1229
+ url_name="dcim:devicetype_devicebays",
1230
+ related_object_attribute="device_bay_templates",
1231
+ hide_if_empty=True,
1232
+ panels=(
1233
+ object_detail.ObjectsTablePanel(
1234
+ section=SectionChoices.FULL_WIDTH,
1235
+ weight=100,
1236
+ table_title="Device Bays",
1237
+ table_class=tables.DeviceBayTemplateTable,
1238
+ table_filter="device_type",
1239
+ tab_id="devicebays",
1240
+ enable_bulk_actions=True,
1241
+ form_id="devicebaystemplate_form",
1242
+ footer_buttons=bulk_footer_buttons(
1243
+ form_id="devicebaystemplate_form",
1244
+ model=DeviceBayTemplate,
1245
+ ),
1246
+ include_paginator=True,
1247
+ enable_related_link=False,
1248
+ ),
1249
+ ),
1250
+ ),
1251
+ object_detail.DistinctViewTab(
1252
+ weight=900,
1253
+ tab_id="modulebays",
1254
+ label="Module Bays",
1255
+ url_name="dcim:devicetype_modulebays",
1256
+ related_object_attribute="module_bay_templates",
1257
+ hide_if_empty=True,
1258
+ panels=(
1259
+ object_detail.ObjectsTablePanel(
1260
+ section=SectionChoices.FULL_WIDTH,
1261
+ weight=100,
1262
+ table_title="Module Bays",
1263
+ table_class=tables.ModuleBayTemplateTable,
1264
+ table_filter="device_type",
1265
+ tab_id="modulebays",
1266
+ enable_bulk_actions=True,
1267
+ form_id="modulebaystemplate_form",
1268
+ footer_buttons=bulk_footer_buttons(
1269
+ form_id="modulebaystemplate_form",
1270
+ model=ModuleBayTemplate,
1271
+ ),
1272
+ include_paginator=True,
1273
+ enable_related_link=False,
1274
+ ),
1275
+ ),
1276
+ ),
1277
+ ),
984
1278
  extra_buttons=(
985
1279
  object_detail.DropdownButton(
986
1280
  weight=100,
@@ -989,16 +1283,70 @@ class DeviceTypeUIViewSet(NautobotUIViewSet):
989
1283
  attributes={"id": "device-type-add-components-button"},
990
1284
  icon="mdi-plus-thick",
991
1285
  required_permissions=["dcim.change_devicetype"],
992
- children=tuple(
1286
+ children=(
993
1287
  object_detail.Button(
994
- weight=weight,
995
- link_name=link_name,
996
- label=label,
997
- icon=icon,
998
- required_permissions=perms,
999
- link_includes_pk=False,
1000
- )
1001
- for weight, link_name, label, icon, perms in ADD_COMPONENTS_CONFIG
1288
+ weight=100,
1289
+ link_name="dcim:devicetype_consoleporttemplate_add",
1290
+ label="Console Ports",
1291
+ icon="mdi-console",
1292
+ required_permissions=["dcim.add_consoleporttemplate"],
1293
+ ),
1294
+ object_detail.Button(
1295
+ weight=200,
1296
+ link_name="dcim:devicetype_consoleserverporttemplate_add",
1297
+ label="Console Server Ports",
1298
+ icon="mdi-console-network-outline",
1299
+ required_permissions=["dcim.add_consoleserverporttemplate"],
1300
+ ),
1301
+ object_detail.Button(
1302
+ weight=300,
1303
+ link_name="dcim:devicetype_powerporttemplate_add",
1304
+ label="Power Ports",
1305
+ icon="mdi-power-plug-outline",
1306
+ required_permissions=["dcim.add_powerporttemplate"],
1307
+ ),
1308
+ object_detail.Button(
1309
+ weight=400,
1310
+ link_name="dcim:devicetype_poweroutlettemplate_add",
1311
+ label="Power Outlets",
1312
+ icon="mdi-power-socket",
1313
+ required_permissions=["dcim.add_poweroutlettemplate"],
1314
+ ),
1315
+ object_detail.Button(
1316
+ weight=500,
1317
+ link_name="dcim:devicetype_interfacetemplate_add",
1318
+ label="Interfaces",
1319
+ icon="mdi-ethernet",
1320
+ required_permissions=["dcim.add_interfacetemplate"],
1321
+ ),
1322
+ object_detail.Button(
1323
+ weight=600,
1324
+ link_name="dcim:devicetype_frontporttemplate_add",
1325
+ label="Front Ports",
1326
+ icon="mdi-square-rounded-outline",
1327
+ required_permissions=["dcim.add_frontporttemplate"],
1328
+ ),
1329
+ object_detail.Button(
1330
+ weight=700,
1331
+ link_name="dcim:devicetype_rearporttemplate_add",
1332
+ label="Rear Ports",
1333
+ icon="mdi-square-rounded-outline",
1334
+ required_permissions=["dcim.add_rearporttemplate"],
1335
+ ),
1336
+ object_detail.Button(
1337
+ weight=800,
1338
+ link_name="dcim:devicetype_devicebaytemplate_add",
1339
+ label="Device Bays",
1340
+ icon="mdi-circle-outline",
1341
+ required_permissions=["dcim.add_devicebaytemplate"],
1342
+ ),
1343
+ object_detail.Button(
1344
+ weight=900,
1345
+ link_name="dcim:devicetype_modulebaytemplate_add",
1346
+ label="Module Bays",
1347
+ icon="mdi-tray",
1348
+ required_permissions=["dcim.add_modulebaytemplate"],
1349
+ ),
1002
1350
  ),
1003
1351
  ),
1004
1352
  ),
@@ -1138,6 +1486,7 @@ class ModuleTypeUIViewSet(
1138
1486
  ObjectDestroyViewMixin,
1139
1487
  ObjectBulkDestroyViewMixin,
1140
1488
  ObjectBulkUpdateViewMixin,
1489
+ # ObjectDataComplianceViewMixin, # TODO: enable once converted to UI framework
1141
1490
  ObjectChangeLogViewMixin,
1142
1491
  ObjectNotesViewMixin,
1143
1492
  ):
@@ -1186,65 +1535,68 @@ class ModuleTypeUIViewSet(
1186
1535
  return super().get_required_permission()
1187
1536
 
1188
1537
  def get_extra_context(self, request, instance):
1189
- if not instance:
1190
- 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()
1191
1541
 
1192
- 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")
1193
1584
 
1194
- # Component tables
1195
- consoleport_table = tables.ConsolePortTemplateTable(
1196
- ConsolePortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1197
- orderable=False,
1198
- )
1199
- consoleserverport_table = tables.ConsoleServerPortTemplateTable(
1200
- ConsoleServerPortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1201
- orderable=False,
1202
- )
1203
- powerport_table = tables.PowerPortTemplateTable(
1204
- PowerPortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1205
- orderable=False,
1206
- )
1207
- poweroutlet_table = tables.PowerOutletTemplateTable(
1208
- PowerOutletTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1209
- orderable=False,
1210
- )
1211
- interface_table = tables.InterfaceTemplateTable(
1212
- list(InterfaceTemplate.objects.restrict(request.user, "view").filter(module_type=instance)),
1213
- orderable=False,
1214
- )
1215
- front_port_table = tables.FrontPortTemplateTable(
1216
- FrontPortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1217
- orderable=False,
1218
- )
1219
- rear_port_table = tables.RearPortTemplateTable(
1220
- RearPortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1221
- orderable=False,
1222
- )
1223
- modulebay_table = tables.ModuleBayTemplateTable(
1224
- ModuleBayTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1225
- orderable=False,
1226
- )
1227
- if request.user.has_perm("dcim.change_moduletype"):
1228
- consoleport_table.columns.show("pk")
1229
- consoleserverport_table.columns.show("pk")
1230
- powerport_table.columns.show("pk")
1231
- poweroutlet_table.columns.show("pk")
1232
- interface_table.columns.show("pk")
1233
- front_port_table.columns.show("pk")
1234
- rear_port_table.columns.show("pk")
1235
- 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
+ )
1236
1598
 
1237
- return {
1238
- "instance_count": instance_count,
1239
- "consoleport_table": consoleport_table,
1240
- "consoleserverport_table": consoleserverport_table,
1241
- "powerport_table": powerport_table,
1242
- "poweroutlet_table": poweroutlet_table,
1243
- "interface_table": interface_table,
1244
- "front_port_table": front_port_table,
1245
- "rear_port_table": rear_port_table,
1246
- "modulebay_table": modulebay_table,
1247
- }
1599
+ return context
1248
1600
 
1249
1601
  @action(
1250
1602
  detail=False,
@@ -1947,32 +2299,43 @@ class DeviceComponentPageMixin:
1947
2299
  - Console Port assigned to the device: Devices / <Device name and link to details> / Console Ports (dcim/devices/<id>/console-ports/) / <Console Port name>
1948
2300
  """
1949
2301
 
1950
- breadcrumbs = Breadcrumbs(
1951
- items={
1952
- "detail": [
1953
- ModelBreadcrumbItem(model=Device, should_render=lambda c: c["object"].device is not None),
1954
- InstanceBreadcrumbItem(
1955
- instance=lambda c: c["object"].device, should_render=lambda c: c["object"].device is not None
1956
- ),
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(
1957
2319
  ViewNameBreadcrumbItem(
1958
- view_name_key="device_breadcrumb_url",
1959
- 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,
1960
2322
  reverse_kwargs=lambda c: {"pk": c["object"].device.pk},
1961
- label=lambda c: c["object"]._meta.verbose_name_plural,
1962
- ),
1963
- ModelBreadcrumbItem(model=Module, should_render=lambda c: c["object"].device is None),
1964
- InstanceBreadcrumbItem(
1965
- 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),
1966
2324
  ),
2325
+ )
2326
+
2327
+ if module_breadcrumb_url := getattr(cls, "module_breadcrumb_url", None):
2328
+ module_breadcrumbs.append(
1967
2329
  ViewNameBreadcrumbItem(
1968
- view_name_key="module_breadcrumb_url",
1969
- 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,
1970
2332
  reverse_kwargs=lambda c: {"pk": c["object"].module.pk},
1971
- label=lambda c: c["object"]._meta.verbose_name_plural,
2333
+ label=lambda c: bettertitle(c["object"]._meta.verbose_name_plural),
1972
2334
  ),
1973
- ]
1974
- }
1975
- )
2335
+ )
2336
+
2337
+ cls.breadcrumbs = Breadcrumbs(items={"detail": [*device_breadcrumbs, *module_breadcrumbs]})
2338
+
1976
2339
  view_titles = Titles(
1977
2340
  titles={
1978
2341
  "detail": "{% if object.device %}{{ object.device }}{% else %}{{ object.module.display }}{% endif %} / {{ object }}"
@@ -1995,7 +2358,11 @@ class DeviceUIViewSet(NautobotUIViewSet):
1995
2358
  items={
1996
2359
  "detail": [
1997
2360
  ModelBreadcrumbItem(model=Device),
1998
- 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
+ ),
1999
2366
  InstanceBreadcrumbItem(
2000
2367
  instance=lambda c: c["object"].parent_bay.device,
2001
2368
  should_render=lambda c: hasattr(c["object"], "parent_bay"),
@@ -2009,7 +2376,7 @@ class DeviceUIViewSet(NautobotUIViewSet):
2009
2376
 
2010
2377
  def get_queryset(self):
2011
2378
  queryset = super().get_queryset()
2012
- if self.detail: # TODO: change to self.action == "retrieve" as a part of addressing NAUTOBOT-1051
2379
+ if self.action == "retrieve":
2013
2380
  queryset = queryset.select_related(
2014
2381
  "controller_managed_device_group__controller",
2015
2382
  "device_redundancy_group",
@@ -2035,34 +2402,6 @@ class DeviceUIViewSet(NautobotUIViewSet):
2035
2402
  Override base ObjectDetailContent to render dynamic-groups table as a separate view/tab instead of inline.
2036
2403
  """
2037
2404
 
2038
- class DeviceDynamicGroupsTextPanel(object_detail.BaseTextPanel):
2039
- """Panel displaying a note about caching of dynamic groups."""
2040
-
2041
- def __init__(
2042
- self,
2043
- *,
2044
- weight,
2045
- render_as=object_detail.BaseTextPanel.RenderOptions.MARKDOWN,
2046
- label="Dynamic Group caching",
2047
- **kwargs,
2048
- ):
2049
- super().__init__(weight=weight, render_as=render_as, label=label, **kwargs)
2050
-
2051
- def get_value(self, context):
2052
- dg_list_url = reverse("extras:dynamicgroup_list")
2053
- job_run_url = reverse(
2054
- "extras:job_run_by_class_path",
2055
- kwargs={"class_path": "nautobot.core.jobs.groups.RefreshDynamicGroupCaches"},
2056
- )
2057
- return (
2058
- "Dynamic group membership is cached for performance reasons, "
2059
- "therefore this page may not always be up-to-date.\n\n"
2060
- "You can refresh the membership of any specific group by viewing it from the list below or from the "
2061
- f"[Dynamic Groups list view]({dg_list_url}).\n\n"
2062
- "You can also refresh the membership of **all** groups by running the "
2063
- f"[Refresh Dynamic Group Caches job]({job_run_url})."
2064
- )
2065
-
2066
2405
  def __init__(self, **kwargs):
2067
2406
  super().__init__(**kwargs)
2068
2407
  # Remove inline tab definition
@@ -2078,7 +2417,7 @@ class DeviceUIViewSet(NautobotUIViewSet):
2078
2417
  url_name="dcim:device_dynamicgroups",
2079
2418
  related_object_attribute="dynamic_groups",
2080
2419
  panels=(
2081
- self.DeviceDynamicGroupsTextPanel(weight=100),
2420
+ object_detail.DynamicGroupsTextPanel(weight=100),
2082
2421
  object_detail.ObjectsTablePanel(
2083
2422
  weight=200,
2084
2423
  table_class=DynamicGroupTable,
@@ -2165,11 +2504,11 @@ class DeviceUIViewSet(NautobotUIViewSet):
2165
2504
  for powerport in instance.all_power_ports.all():
2166
2505
  utilization = powerport.get_power_draw()
2167
2506
  # Table row for each power-port
2168
- powerfeed = powerport.connected_endpoint
2169
- if powerfeed is not None and powerfeed.available_power:
2170
- available_power = powerfeed.available_power
2507
+ connected_endpoint = powerport.connected_endpoint
2508
+ if isinstance(connected_endpoint, PowerFeed) and connected_endpoint.available_power:
2509
+ available_power = connected_endpoint.available_power
2171
2510
  utilization_data = Context(
2172
- helpers.utilization_graph_raw_data(utilization["allocated"], powerfeed.available_power)
2511
+ helpers.utilization_graph_raw_data(utilization["allocated"], connected_endpoint.available_power)
2173
2512
  )
2174
2513
  utilization_graph = object_detail.render_component_template(
2175
2514
  "utilities/templatetags/utilization_graph.html", utilization_data
@@ -2188,13 +2527,10 @@ class DeviceUIViewSet(NautobotUIViewSet):
2188
2527
 
2189
2528
  # Indented table row for each leg of a three-phase power-port.
2190
2529
  for leg in utilization["legs"]:
2191
- if powerfeed is not None and powerfeed.available_power:
2192
- available_power = powerfeed.available_power / 3
2530
+ if isinstance(connected_endpoint, PowerFeed) and connected_endpoint.available_power:
2531
+ available_power = connected_endpoint.available_power / 3
2193
2532
  utilization_data = Context(
2194
- helpers.utilization_graph_raw_data(leg["allocated"], powerfeed.available_power / 3)
2195
- )
2196
- utilization_graph = object_detail.render_component_template(
2197
- "utilities/templatetags/utilization_graph.html", utilization_data
2533
+ helpers.utilization_graph_raw_data(leg["allocated"], connected_endpoint.available_power / 3)
2198
2534
  )
2199
2535
  else:
2200
2536
  available_power = helpers.HTML_NONE
@@ -2434,6 +2770,7 @@ class DeviceUIViewSet(NautobotUIViewSet):
2434
2770
  table_class=VRFDeviceAssignmentTable,
2435
2771
  table_filter="device",
2436
2772
  exclude_columns=["related_object_type", "related_object_name"],
2773
+ related_list_url_name="ipam:vrf_list",
2437
2774
  show_table_config_button=False,
2438
2775
  ),
2439
2776
  object_detail.ObjectsTablePanel(
@@ -2463,6 +2800,7 @@ class DeviceUIViewSet(NautobotUIViewSet):
2463
2800
  table_attribute="images",
2464
2801
  related_field_name="device",
2465
2802
  show_table_config_button=False,
2803
+ enable_related_link=False,
2466
2804
  ),
2467
2805
  object_detail.ObjectsTablePanel(
2468
2806
  weight=100,
@@ -2773,29 +3111,52 @@ class DeviceUIViewSet(NautobotUIViewSet):
2773
3111
  ),
2774
3112
  ),
2775
3113
  ),
2776
- DeviceNAPALMTab(
3114
+ object_detail.DistinctViewTab(
2777
3115
  weight=object_detail.Tab.WEIGHT_CHANGELOG_TAB + 1200,
3116
+ tab_id="vpn_endpoints",
3117
+ label="VPN Endpoints",
3118
+ url_name="dcim:device_vpnendpoints",
3119
+ related_object_attribute="vpn_tunnel_endpoints",
3120
+ hide_if_empty=True,
3121
+ panels=(
3122
+ object_detail.ObjectsTablePanel(
3123
+ weight=100,
3124
+ section=SectionChoices.FULL_WIDTH,
3125
+ table_title="VPN Endpoints",
3126
+ table_class=VPNTunnelEndpointTable,
3127
+ table_attribute="vpn_tunnel_endpoints",
3128
+ related_field_name="device",
3129
+ select_related_fields=["source_interface", "role"],
3130
+ exclude_columns=["device"],
3131
+ tab_id="vpn_endpoints",
3132
+ enable_bulk_actions=True,
3133
+ include_paginator=True,
3134
+ ),
3135
+ ),
3136
+ ),
3137
+ DeviceNAPALMTab(
3138
+ weight=object_detail.Tab.WEIGHT_CHANGELOG_TAB + 1300,
2778
3139
  tab_id="status",
2779
3140
  label="Status",
2780
3141
  url_name="dcim:device_status",
2781
3142
  required_permissions=["dcim.napalm_read_device"],
2782
3143
  ),
2783
3144
  DeviceNAPALMTab(
2784
- weight=object_detail.Tab.WEIGHT_CHANGELOG_TAB + 1300,
3145
+ weight=object_detail.Tab.WEIGHT_CHANGELOG_TAB + 1400,
2785
3146
  tab_id="lldp_neighbors",
2786
3147
  label="LLDP Neighbors",
2787
3148
  url_name="dcim:device_lldp_neighbors",
2788
3149
  required_permissions=["dcim.napalm_read_device"],
2789
3150
  ),
2790
3151
  DeviceNAPALMTab(
2791
- weight=object_detail.Tab.WEIGHT_CHANGELOG_TAB + 1400,
3152
+ weight=object_detail.Tab.WEIGHT_CHANGELOG_TAB + 1500,
2792
3153
  tab_id="config",
2793
3154
  label="Configuration",
2794
3155
  url_name="dcim:device_config",
2795
3156
  required_permissions=["dcim.napalm_read_device"],
2796
3157
  ),
2797
3158
  object_detail.DistinctViewTab(
2798
- weight=object_detail.Tab.WEIGHT_CHANGELOG_TAB + 1500,
3159
+ weight=object_detail.Tab.WEIGHT_CHANGELOG_TAB + 1600,
2799
3160
  tab_id="config_context",
2800
3161
  label="Config Context",
2801
3162
  url_name="dcim:device_configcontext",
@@ -2807,7 +3168,7 @@ class DeviceUIViewSet(NautobotUIViewSet):
2807
3168
  def get_extra_context(self, request, instance):
2808
3169
  extra_context = super().get_extra_context(request, instance)
2809
3170
 
2810
- if self.detail: # TODO: change to `if self.action == "retrieve"` as a part of addressing NAUTOBOT-1051
3171
+ if self.action == "retrieve":
2811
3172
  # VirtualChassis members
2812
3173
  if instance.virtual_chassis is not None:
2813
3174
  vc_members = (
@@ -2822,6 +3183,16 @@ class DeviceUIViewSet(NautobotUIViewSet):
2822
3183
 
2823
3184
  return extra_context
2824
3185
 
3186
+ @action(
3187
+ detail=True,
3188
+ url_path="vpn-endpoints",
3189
+ url_name="vpnendpoints",
3190
+ custom_view_base_action="view",
3191
+ custom_view_additional_permissions=["vpn.view_vpntunnelendpoint"],
3192
+ )
3193
+ def vpn_endpoints(self, request, *args, **kwargs):
3194
+ return Response({})
3195
+
2825
3196
  @action(
2826
3197
  detail=True,
2827
3198
  url_path="dynamic-groups",
@@ -3505,11 +3876,13 @@ class ConsolePortListView(generic.ObjectListView):
3505
3876
 
3506
3877
  class ConsolePortView(DeviceComponentPageMixin, generic.ObjectView):
3507
3878
  queryset = ConsolePort.objects.all()
3879
+ device_breadcrumb_url = "dcim:device_consoleports"
3880
+ module_breadcrumb_url = "dcim:module_consoleports"
3508
3881
 
3509
3882
  def get_extra_context(self, request, instance):
3510
3883
  return {
3511
- "device_breadcrumb_url": "dcim:device_consoleports",
3512
- "module_breadcrumb_url": "dcim:module_consoleports",
3884
+ "device_breadcrumb_url": self.device_breadcrumb_url,
3885
+ "module_breadcrumb_url": self.module_breadcrumb_url,
3513
3886
  **super().get_extra_context(request, instance),
3514
3887
  }
3515
3888
 
@@ -3571,11 +3944,13 @@ class ConsoleServerPortListView(generic.ObjectListView):
3571
3944
 
3572
3945
  class ConsoleServerPortView(DeviceComponentPageMixin, generic.ObjectView):
3573
3946
  queryset = ConsoleServerPort.objects.all()
3947
+ device_breadcrumb_url = "dcim:device_consoleserverports"
3948
+ module_breadcrumb_url = "dcim:module_consoleserverports"
3574
3949
 
3575
3950
  def get_extra_context(self, request, instance):
3576
3951
  return {
3577
- "device_breadcrumb_url": "dcim:device_consoleserverports",
3578
- "module_breadcrumb_url": "dcim:module_consoleserverports",
3952
+ "device_breadcrumb_url": self.device_breadcrumb_url,
3953
+ "module_breadcrumb_url": self.module_breadcrumb_url,
3579
3954
  **super().get_extra_context(request, instance),
3580
3955
  }
3581
3956
 
@@ -3637,11 +4012,13 @@ class PowerPortListView(generic.ObjectListView):
3637
4012
 
3638
4013
  class PowerPortView(DeviceComponentPageMixin, generic.ObjectView):
3639
4014
  queryset = PowerPort.objects.all()
4015
+ device_breadcrumb_url = "dcim:device_powerports"
4016
+ module_breadcrumb_url = "dcim:module_powerports"
3640
4017
 
3641
4018
  def get_extra_context(self, request, instance):
3642
4019
  return {
3643
- "device_breadcrumb_url": "dcim:device_powerports",
3644
- "module_breadcrumb_url": "dcim:module_powerports",
4020
+ "device_breadcrumb_url": self.device_breadcrumb_url,
4021
+ "module_breadcrumb_url": self.module_breadcrumb_url,
3645
4022
  **super().get_extra_context(request, instance),
3646
4023
  }
3647
4024
 
@@ -3703,11 +4080,13 @@ class PowerOutletListView(generic.ObjectListView):
3703
4080
 
3704
4081
  class PowerOutletView(DeviceComponentPageMixin, generic.ObjectView):
3705
4082
  queryset = PowerOutlet.objects.all()
4083
+ device_breadcrumb_url = "dcim:device_poweroutlets"
4084
+ module_breadcrumb_url = "dcim:module_poweroutlets"
3706
4085
 
3707
4086
  def get_extra_context(self, request, instance):
3708
4087
  return {
3709
- "device_breadcrumb_url": "dcim:device_poweroutlets",
3710
- "module_breadcrumb_url": "dcim:module_poweroutlets",
4088
+ "device_breadcrumb_url": self.device_breadcrumb_url,
4089
+ "module_breadcrumb_url": self.module_breadcrumb_url,
3711
4090
  **super().get_extra_context(request, instance),
3712
4091
  }
3713
4092
 
@@ -3772,6 +4151,8 @@ class InterfaceView(
3772
4151
  generic.ObjectView,
3773
4152
  ):
3774
4153
  queryset = Interface.objects.all()
4154
+ device_breadcrumb_url = "dcim:device_interfaces"
4155
+ module_breadcrumb_url = "dcim:module_interfaces"
3775
4156
 
3776
4157
  def get_extra_context(self, request, instance):
3777
4158
  # Get assigned IP addresses
@@ -3812,8 +4193,8 @@ class InterfaceView(
3812
4193
  return {
3813
4194
  "ipaddress_table": ipaddress_table,
3814
4195
  "vlan_table": vlan_table,
3815
- "device_breadcrumb_url": "dcim:device_interfaces",
3816
- "module_breadcrumb_url": "dcim:module_interfaces",
4196
+ "device_breadcrumb_url": self.device_breadcrumb_url,
4197
+ "module_breadcrumb_url": self.module_breadcrumb_url,
3817
4198
  "child_interfaces_table": child_interfaces_tables,
3818
4199
  "redundancy_table": redundancy_table,
3819
4200
  "virtual_device_contexts_table": virtual_device_contexts_table,
@@ -3902,11 +4283,13 @@ class FrontPortListView(generic.ObjectListView):
3902
4283
 
3903
4284
  class FrontPortView(DeviceComponentPageMixin, generic.ObjectView):
3904
4285
  queryset = FrontPort.objects.all()
4286
+ device_breadcrumb_url = "dcim:device_frontports"
4287
+ module_breadcrumb_url = "dcim:module_frontports"
3905
4288
 
3906
4289
  def get_extra_context(self, request, instance):
3907
4290
  return {
3908
- "device_breadcrumb_url": "dcim:device_frontports",
3909
- "module_breadcrumb_url": "dcim:module_frontports",
4291
+ "device_breadcrumb_url": self.device_breadcrumb_url,
4292
+ "module_breadcrumb_url": self.module_breadcrumb_url,
3910
4293
  **super().get_extra_context(request, instance),
3911
4294
  }
3912
4295
 
@@ -3968,11 +4351,13 @@ class RearPortListView(generic.ObjectListView):
3968
4351
 
3969
4352
  class RearPortView(DeviceComponentPageMixin, generic.ObjectView):
3970
4353
  queryset = RearPort.objects.all()
4354
+ device_breadcrumb_url = "dcim:device_rearports"
4355
+ module_breadcrumb_url = "dcim:module_rearports"
3971
4356
 
3972
4357
  def get_extra_context(self, request, instance):
3973
4358
  return {
3974
- "device_breadcrumb_url": "dcim:device_rearports",
3975
- "module_breadcrumb_url": "dcim:module_rearports",
4359
+ "device_breadcrumb_url": self.device_breadcrumb_url,
4360
+ "module_breadcrumb_url": self.module_breadcrumb_url,
3976
4361
  **super().get_extra_context(request, instance),
3977
4362
  }
3978
4363
 
@@ -4034,9 +4419,13 @@ class DeviceBayListView(generic.ObjectListView):
4034
4419
 
4035
4420
  class DeviceBayView(DeviceComponentPageMixin, generic.ObjectView):
4036
4421
  queryset = DeviceBay.objects.all()
4422
+ device_breadcrumb_url = "dcim:device_devicebays"
4037
4423
 
4038
4424
  def get_extra_context(self, request, instance):
4039
- 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
+ }
4040
4429
 
4041
4430
 
4042
4431
  class DeviceBayCreateView(generic.ComponentCreateView):
@@ -4271,6 +4660,7 @@ class InventoryItemListView(generic.ObjectListView):
4271
4660
 
4272
4661
  class InventoryItemView(DeviceComponentPageMixin, generic.ObjectView):
4273
4662
  queryset = InventoryItem.objects.all().select_related("device", "manufacturer", "software_version")
4663
+ device_breadcrumb_url = "dcim:device_inventory"
4274
4664
 
4275
4665
  def get_extra_context(self, request, instance):
4276
4666
  # Software images
@@ -4280,7 +4670,7 @@ class InventoryItemView(DeviceComponentPageMixin, generic.ObjectView):
4280
4670
  software_version_images = []
4281
4671
 
4282
4672
  return {
4283
- "device_breadcrumb_url": "dcim:device_inventory",
4673
+ "device_breadcrumb_url": self.device_breadcrumb_url,
4284
4674
  "software_version_images": software_version_images,
4285
4675
  **super().get_extra_context(request, instance),
4286
4676
  }
@@ -4444,18 +4834,23 @@ class DeviceBulkAddInventoryItemView(generic.BulkComponentCreateView):
4444
4834
  #
4445
4835
  # Cables
4446
4836
  #
4447
-
4448
-
4449
- class CableListView(generic.ObjectListView):
4450
- queryset = Cable.objects.all()
4451
- filterset = filters.CableFilterSet
4452
- filterset_form = forms.CableFilterForm
4453
- table = tables.CableTable
4837
+ class CableUIViewSet(NautobotUIViewSet):
4838
+ bulk_update_form_class = forms.CableBulkEditForm
4839
+ filterset_class = filters.CableFilterSet
4840
+ filterset_form_class = forms.CableFilterForm
4841
+ form_class = forms.CableForm
4842
+ serializer_class = serializers.CableSerializer
4843
+ table_class = tables.CableTable
4844
+ queryset = Cable.objects.prefetch_related("termination_a", "termination_b")
4454
4845
  action_buttons = ("import", "export")
4455
4846
 
4456
-
4457
- class CableView(generic.ObjectView):
4458
- queryset = Cable.objects.all()
4847
+ def get_queryset(self):
4848
+ # 6933 fix: with prefetch related in queryset
4849
+ # DeviceInterface is not properly cleared of _path_id
4850
+ queryset = super().get_queryset()
4851
+ if self.action == "destroy":
4852
+ queryset = queryset.prefetch_related(None)
4853
+ return queryset
4459
4854
 
4460
4855
 
4461
4856
  class PathTraceView(generic.ObjectView):
@@ -4465,6 +4860,7 @@ class PathTraceView(generic.ObjectView):
4465
4860
 
4466
4861
  additional_permissions = ["dcim.view_cable"]
4467
4862
  template_name = "dcim/cable_trace.html"
4863
+ view_titles = Titles(titles={"detail": "Cable Trace for {{ object }}"})
4468
4864
 
4469
4865
  def dispatch(self, request, *args, **kwargs):
4470
4866
  model = kwargs.pop("model")
@@ -4501,6 +4897,7 @@ class PathTraceView(generic.ObjectView):
4501
4897
  "path": path,
4502
4898
  "related_paths": related_paths,
4503
4899
  "total_length": path.get_total_length() if path else None,
4900
+ "view_titles": self.get_view_titles(),
4504
4901
  **super().get_extra_context(request, instance),
4505
4902
  }
4506
4903
 
@@ -4585,34 +4982,6 @@ class CableCreateView(generic.ObjectEditView):
4585
4982
  )
4586
4983
 
4587
4984
 
4588
- class CableEditView(generic.ObjectEditView):
4589
- queryset = Cable.objects.all()
4590
- model_form = forms.CableForm
4591
- template_name = "dcim/cable_edit.html"
4592
-
4593
-
4594
- class CableDeleteView(generic.ObjectDeleteView):
4595
- queryset = Cable.objects.all()
4596
-
4597
-
4598
- class CableBulkImportView(generic.BulkImportView): # 3.0 TODO: remove, unused
4599
- queryset = Cable.objects.all()
4600
- table = tables.CableTable
4601
-
4602
-
4603
- class CableBulkEditView(generic.BulkEditView):
4604
- queryset = Cable.objects.prefetch_related("termination_a", "termination_b")
4605
- filterset = filters.CableFilterSet
4606
- table = tables.CableTable
4607
- form = forms.CableBulkEditForm
4608
-
4609
-
4610
- class CableBulkDeleteView(generic.BulkDeleteView):
4611
- queryset = Cable.objects.prefetch_related("termination_a", "termination_b")
4612
- filterset = filters.CableFilterSet
4613
- table = tables.CableTable
4614
-
4615
-
4616
4985
  #
4617
4986
  # Connections
4618
4987
  #
@@ -4815,7 +5184,6 @@ class VirtualChassisUIViewSet(NautobotUIViewSet):
4815
5184
  url_path="add-member",
4816
5185
  url_name="add_member",
4817
5186
  custom_view_base_action="change",
4818
- custom_view_additional_permissions=["dcim.change_virtualchassis"],
4819
5187
  )
4820
5188
  def add_member(self, request, pk=None):
4821
5189
  virtual_chassis = self.get_object()
@@ -5012,7 +5380,9 @@ class PowerFeedUIViewSet(NautobotUIViewSet):
5012
5380
  items={
5013
5381
  "detail": [
5014
5382
  ModelBreadcrumbItem(),
5015
- InstanceBreadcrumbItem(instance=context_object_attr("power_panel.location")),
5383
+ AncestorsInstanceBreadcrumbItem(
5384
+ instance=context_object_attr("power_panel.location"), include_self=True
5385
+ ),
5016
5386
  InstanceBreadcrumbItem(instance=context_object_attr("power_panel")),
5017
5387
  InstanceBreadcrumbItem(
5018
5388
  instance=context_object_attr("rack"),
@@ -5164,7 +5534,7 @@ class PowerFeedUIViewSet(NautobotUIViewSet):
5164
5534
  + f"?return_url={instance.get_absolute_url()}"
5165
5535
  )
5166
5536
  connect_link = format_html(
5167
- '<a href="{}" class="btn btn-primary btn-sm pull-right">'
5537
+ '<a href="{}" class="btn btn-primary btn-sm float-end">'
5168
5538
  '<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> Connect</a>',
5169
5539
  connect_url,
5170
5540
  )
@@ -5248,7 +5618,8 @@ class InterfaceRedundancyGroupUIViewSet(NautobotUIViewSet):
5248
5618
  prefetch_related_fields=["interface"],
5249
5619
  order_by_fields=["priority"],
5250
5620
  table_title="Interfaces",
5251
- related_field_name="interface_redundancy_group",
5621
+ related_field_name="interface_redundancy_groups",
5622
+ related_list_url_name="dcim:interface_list",
5252
5623
  include_columns=[
5253
5624
  "interface__device",
5254
5625
  "interface",
@@ -5501,6 +5872,14 @@ class SoftwareVersionUIViewSet(NautobotUIViewSet):
5501
5872
  queryset = SoftwareVersion.objects.all()
5502
5873
  serializer_class = serializers.SoftwareVersionSerializer
5503
5874
  table_class = tables.SoftwareVersionTable
5875
+ breadcrumbs = Breadcrumbs(
5876
+ items={
5877
+ "detail": [
5878
+ ModelBreadcrumbItem(),
5879
+ InstanceBreadcrumbItem(instance=context_object_attr("platform")),
5880
+ ]
5881
+ }
5882
+ )
5504
5883
  object_detail_content = object_detail.ObjectDetailContent(
5505
5884
  panels=(
5506
5885
  object_detail.ObjectFieldsPanel(
@@ -5578,7 +5957,7 @@ class ControllerUIViewSet(NautobotUIViewSet):
5578
5957
  object_detail.DistinctViewTab(
5579
5958
  weight=700,
5580
5959
  tab_id="wireless_networks",
5581
- url_name="dcim:controller_wirelessnetworks",
5960
+ url_name="dcim:controller_wireless_networks",
5582
5961
  label="Wireless Networks",
5583
5962
  related_object_attribute="wireless_network_assignments",
5584
5963
  panels=(
@@ -5593,6 +5972,7 @@ class ControllerUIViewSet(NautobotUIViewSet):
5593
5972
  select_related_fields=["wireless_network"],
5594
5973
  exclude_columns=["controller"],
5595
5974
  include_paginator=True,
5975
+ enable_related_link=False,
5596
5976
  ),
5597
5977
  ),
5598
5978
  ),
@@ -5602,12 +5982,12 @@ class ControllerUIViewSet(NautobotUIViewSet):
5602
5982
  @action(
5603
5983
  detail=True,
5604
5984
  url_path="wireless-networks",
5605
- url_name="wirelessnetworks",
5985
+ url_name="wireless_networks",
5606
5986
  methods=["get"],
5607
5987
  custom_view_base_action="view",
5608
5988
  custom_view_additional_permissions=["wireless.view_controllermanageddevicegroupwirelessnetworkassignment"],
5609
5989
  )
5610
- def wirelessnetworks(self, request, *args, **kwargs):
5990
+ def wireless_networks(self, request, *args, **kwargs):
5611
5991
  return Response({})
5612
5992
 
5613
5993