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
@@ -6,7 +6,7 @@ from django.contrib.contenttypes.models import ContentType
6
6
  from django.core.cache import cache
7
7
  from django.core.exceptions import ObjectDoesNotExist, ValidationError
8
8
  from django.core.validators import MaxValueValidator, MinValueValidator
9
- from django.db import models, transaction
9
+ from django.db import models
10
10
  from django.db.models import Sum
11
11
  from django.utils.functional import classproperty
12
12
 
@@ -20,6 +20,7 @@ from nautobot.core.utils.cache import construct_cache_key
20
20
  from nautobot.core.utils.data import UtilizationData
21
21
  from nautobot.dcim.choices import (
22
22
  ConsolePortTypeChoices,
23
+ InterfaceDuplexChoices,
23
24
  InterfaceModeChoices,
24
25
  InterfaceRedundancyGroupProtocolChoices,
25
26
  InterfaceStatusChoices,
@@ -32,6 +33,7 @@ from nautobot.dcim.choices import (
32
33
  SubdeviceRoleChoices,
33
34
  )
34
35
  from nautobot.dcim.constants import (
36
+ COPPER_TWISTED_PAIR_IFACE_TYPES,
35
37
  NONCONNECTABLE_IFACE_TYPES,
36
38
  REARPORT_POSITIONS_MAX,
37
39
  REARPORT_POSITIONS_MIN,
@@ -596,6 +598,86 @@ class BaseInterface(RelationshipModel):
596
598
 
597
599
  return super().save(*args, **kwargs)
598
600
 
601
+ def add_ip_addresses(
602
+ self,
603
+ ip_addresses,
604
+ is_source=False,
605
+ is_destination=False,
606
+ is_default=False,
607
+ is_preferred=False,
608
+ is_primary=False,
609
+ is_secondary=False,
610
+ is_standby=False,
611
+ ):
612
+ """Add one or more IPAddress instances to this interface's `ip_addresses` many-to-many relationship.
613
+
614
+ Args:
615
+ ip_addresses (:obj:`list` or `IPAddress`): Instance of `nautobot.ipam.models.IPAddress` or list of `IPAddress` instances.
616
+ is_source (bool, optional): Is source address. Defaults to False.
617
+ is_destination (bool, optional): Is destination address. Defaults to False.
618
+ is_default (bool, optional): Is default address. Defaults to False.
619
+ is_preferred (bool, optional): Is preferred address. Defaults to False.
620
+ is_primary (bool, optional): Is primary address. Defaults to False.
621
+ is_secondary (bool, optional): Is secondary address. Defaults to False.
622
+ is_standby (bool, optional): Is standby address. Defaults to False.
623
+
624
+ Returns:
625
+ Number of instances added.
626
+ """
627
+ through_defaults = {
628
+ "is_source": is_source,
629
+ "is_destination": is_destination,
630
+ "is_default": is_default,
631
+ "is_preferred": is_preferred,
632
+ "is_primary": is_primary,
633
+ "is_secondary": is_secondary,
634
+ "is_standby": is_standby,
635
+ }
636
+
637
+ if not isinstance(ip_addresses, (tuple, list)):
638
+ ip_addresses = [ip_addresses]
639
+
640
+ # This ensures that ips_to_add only contains IPs which need to be added to the interface. This ensures
641
+ # that len(ips_to_add) accurately represents the results of the action.
642
+ ips_to_add = set(ip_addresses) - set(self.ip_addresses.all())
643
+
644
+ if ips_to_add:
645
+ self.ip_addresses.add(*ips_to_add, through_defaults=through_defaults) # pylint: disable=no-member # Intf/VMIntf both have ip_addresses
646
+
647
+ return len(ips_to_add)
648
+
649
+ add_ip_addresses.alters_data = True
650
+
651
+ def remove_ip_addresses(self, ip_addresses):
652
+ """Remove one or more IPAddress instances from this interface's `ip_addresses` many-to-many relationship.
653
+
654
+ Args:
655
+ ip_addresses (:obj:`list` or `IPAddress`): Instance of `nautobot.ipam.models.IPAddress` or list of `IPAddress` instances.
656
+
657
+ Returns:
658
+ Number of instances removed.
659
+ """
660
+ if not isinstance(ip_addresses, (tuple, list)):
661
+ ip_addresses = [ip_addresses]
662
+
663
+ # The delete() call used previously (ref: https://github.com/nautobot/nautobot/issues/3236)
664
+ # meant that if None was passed in, it was silently ignored. Rather that raise an exception,
665
+ # this comprehension maintains backwards compatibility.
666
+ ip_addresses = {ip for ip in ip_addresses if ip is not None}
667
+
668
+ # This checks that the IPs passed in are actually on the interface. By populating
669
+ # ips_to_remove correctly, we ensure that the only IPs passed to remove() are IPs known
670
+ # to be on the interface. This ensures that len(ips_to_remove) accurately represents
671
+ # the results of the action.
672
+ ips_to_remove = ip_addresses & set(self.ip_addresses.all())
673
+
674
+ if ips_to_remove:
675
+ self.ip_addresses.remove(*ips_to_remove) # pylint: disable=no-member # Intf/VMIntf both have ip_addresses
676
+
677
+ return len(ips_to_remove)
678
+
679
+ remove_ip_addresses.alters_data = True
680
+
599
681
 
600
682
  @extras_features(
601
683
  "cable_terminations",
@@ -664,6 +746,9 @@ class Interface(ModularComponentModel, CableTermination, PathEndpoint, BaseInter
664
746
  blank=True,
665
747
  verbose_name="IP Addresses",
666
748
  )
749
+ # Operational attributes (distinct from interface type capabilities)
750
+ speed = models.PositiveIntegerField(null=True, blank=True)
751
+ duplex = models.CharField(max_length=10, choices=InterfaceDuplexChoices, blank=True, default="")
667
752
 
668
753
  class Meta(ModularComponentModel.Meta):
669
754
  ordering = ("device", "module__id", CollateAsChar("_name")) # Module.ordering is complex; don't order by module
@@ -798,72 +883,21 @@ class Interface(ModularComponentModel, CableTermination, PathEndpoint, BaseInter
798
883
  }
799
884
  )
800
885
 
801
- def add_ip_addresses(
802
- self,
803
- ip_addresses,
804
- is_source=False,
805
- is_destination=False,
806
- is_default=False,
807
- is_preferred=False,
808
- is_primary=False,
809
- is_secondary=False,
810
- is_standby=False,
811
- ):
812
- """Add one or more IPAddress instances to this interface's `ip_addresses` many-to-many relationship.
886
+ # Speed/Duplex validation
887
+ self._validate_speed_and_duplex()
813
888
 
814
- Args:
815
- ip_addresses (:obj:`list` or `IPAddress`): Instance of `nautobot.ipam.models.IPAddress` or list of `IPAddress` instances.
816
- is_source (bool, optional): Is source address. Defaults to False.
817
- is_destination (bool, optional): Is destination address. Defaults to False.
818
- is_default (bool, optional): Is default address. Defaults to False.
819
- is_preferred (bool, optional): Is preferred address. Defaults to False.
820
- is_primary (bool, optional): Is primary address. Defaults to False.
821
- is_secondary (bool, optional): Is secondary address. Defaults to False.
822
- is_standby (bool, optional): Is standby address. Defaults to False.
889
+ def _validate_speed_and_duplex(self):
890
+ """Validate speed (Kbps) and duplex based on interface type."""
823
891
 
824
- Returns:
825
- Number of instances added.
826
- """
827
- if not isinstance(ip_addresses, (tuple, list)):
828
- ip_addresses = [ip_addresses]
829
- with transaction.atomic():
830
- for ip in ip_addresses:
831
- instance = self.ip_addresses.through(
832
- ip_address=ip,
833
- interface=self,
834
- is_source=is_source,
835
- is_destination=is_destination,
836
- is_default=is_default,
837
- is_preferred=is_preferred,
838
- is_primary=is_primary,
839
- is_secondary=is_secondary,
840
- is_standby=is_standby,
841
- )
842
- instance.validated_save()
843
- return len(ip_addresses)
892
+ # Check settings by interface type
893
+ if self.speed and any([self.is_lag, self.is_virtual, self.is_wireless]):
894
+ raise ValidationError({"speed": "Speed is not applicable to this interface type."})
844
895
 
845
- add_ip_addresses.alters_data = True
896
+ if self.duplex and any([self.is_lag, self.is_virtual, self.is_wireless]):
897
+ raise ValidationError({"duplex": "Duplex is not applicable to this interface type."})
846
898
 
847
- def remove_ip_addresses(self, ip_addresses):
848
- """Remove one or more IPAddress instances from this interface's `ip_addresses` many-to-many relationship.
849
-
850
- Args:
851
- ip_addresses (:obj:`list` or `IPAddress`): Instance of `nautobot.ipam.models.IPAddress` or list of `IPAddress` instances.
852
-
853
- Returns:
854
- Number of instances removed.
855
- """
856
- count = 0
857
- if not isinstance(ip_addresses, (tuple, list)):
858
- ip_addresses = [ip_addresses]
859
- with transaction.atomic():
860
- for ip in ip_addresses:
861
- qs = self.ip_addresses.through.objects.filter(ip_address=ip, interface=self)
862
- deleted_count, _ = qs.delete()
863
- count += deleted_count
864
- return count
865
-
866
- remove_ip_addresses.alters_data = True
899
+ if self.duplex and self.type not in COPPER_TWISTED_PAIR_IFACE_TYPES:
900
+ raise ValidationError({"duplex": "Duplex is only applicable to copper twisted-pair interfaces."})
867
901
 
868
902
  @property
869
903
  def is_connectable(self):
@@ -997,7 +1031,7 @@ class InterfaceRedundancyGroupAssociation(BaseModel, ChangeLoggedModel):
997
1031
  on_delete=models.CASCADE,
998
1032
  related_name="interface_redundancy_group_associations",
999
1033
  )
1000
- priority = models.PositiveSmallIntegerField()
1034
+ priority = models.PositiveIntegerField()
1001
1035
  is_metadata_associable_model = False
1002
1036
 
1003
1037
  class Meta:
@@ -25,6 +25,7 @@ from nautobot.dcim.choices import (
25
25
  ControllerCapabilitiesChoices,
26
26
  DeviceFaceChoices,
27
27
  DeviceRedundancyGroupFailoverStrategyChoices,
28
+ DeviceUniquenessChoices,
28
29
  SoftwareImageFileHashingAlgorithmChoices,
29
30
  SubdeviceRoleChoices,
30
31
  )
@@ -391,6 +392,10 @@ class DeviceType(PrimaryModel):
391
392
  def display(self):
392
393
  return f"{self.manufacturer.name} {self.model}"
393
394
 
395
+ @property
396
+ def page_title(self):
397
+ return str(self)
398
+
394
399
  @property
395
400
  def is_parent_device(self):
396
401
  return self.subdevice_role == SubdeviceRoleChoices.ROLE_PARENT
@@ -620,7 +625,7 @@ class Device(PrimaryModel, ConfigContextModel):
620
625
  null=True,
621
626
  verbose_name="Device Redundancy Group",
622
627
  )
623
- device_redundancy_group_priority = models.PositiveSmallIntegerField(
628
+ device_redundancy_group_priority = models.PositiveIntegerField(
624
629
  blank=True,
625
630
  null=True,
626
631
  validators=[MinValueValidator(1)],
@@ -682,19 +687,20 @@ class Device(PrimaryModel, ConfigContextModel):
682
687
  @classproperty # https://github.com/PyCQA/pylint-django/issues/240
683
688
  def natural_key_field_names(cls): # pylint: disable=no-self-argument
684
689
  """
685
- When DEVICE_NAME_AS_NATURAL_KEY is set in settings or Constance, we use just the `name` for simplicity.
690
+ Check DEVICE_UNIQUENESS from settings or Constance and return proper field.
686
691
  """
687
- if get_settings_or_config("DEVICE_NAME_AS_NATURAL_KEY"):
688
- # opt-in simplified "pseudo-natural-key"
692
+ if get_settings_or_config("DEVICE_UNIQUENESS") == DeviceUniquenessChoices.NAME:
693
+ # Simplified pseudo-natural key (opt-in for name-only uniqueness)
689
694
  return ["name"]
695
+ elif get_settings_or_config("DEVICE_UNIQUENESS") == DeviceUniquenessChoices.LOCATION_TENANT_NAME:
696
+ # Full natural key based on tenant, location, and name
697
+ return ["name", "tenant", "location"]
690
698
  else:
691
- # true natural-key given current uniqueness constraints
692
- return ["name", "tenant", "location"] # location should be last since it's potentially variadic
699
+ return ["pk"]
693
700
 
694
701
  class Meta:
695
702
  ordering = ("_name",) # Name may be null
696
703
  unique_together = (
697
- ("location", "tenant", "name"), # See validate_unique below
698
704
  ("rack", "position", "face"),
699
705
  ("virtual_chassis", "vc_position"),
700
706
  )
@@ -716,16 +722,6 @@ class Device(PrimaryModel, ConfigContextModel):
716
722
  else:
717
723
  self.clusters.set([cluster])
718
724
 
719
- def validate_unique(self, exclude=None):
720
- # Check for a duplicate name on a device assigned to the same Location and no Tenant. This is necessary
721
- # because Django does not consider two NULL fields to be equal, and thus will not trigger a violation
722
- # of the uniqueness constraint without manual intervention.
723
- if self.name and hasattr(self, "location") and self.tenant is None:
724
- if Device.objects.exclude(pk=self.pk).filter(name=self.name, location=self.location, tenant__isnull=True):
725
- raise ValidationError({"name": "A device with this name already exists."})
726
-
727
- super().validate_unique(exclude)
728
-
729
725
  def clean(self):
730
726
  from nautobot.ipam import models as ipam_models # circular import workaround
731
727
 
@@ -909,6 +905,11 @@ class Device(PrimaryModel, ConfigContextModel):
909
905
  def save(self, *args, **kwargs):
910
906
  is_new = not self.present_in_database
911
907
 
908
+ # to avoid circular import
909
+ from nautobot.dcim.custom_validators import DeviceUniquenessValidator
910
+
911
+ DeviceUniquenessValidator(self).clean()
912
+
912
913
  super().save(*args, **kwargs)
913
914
 
914
915
  # Apply any pending cluster assignment that was deferred during creation
@@ -1953,10 +1954,19 @@ class Module(PrimaryModel):
1953
1954
  def display(self):
1954
1955
  if self.location:
1955
1956
  return f"{self!s} at location {self.location}"
1956
- elif self.parent_module_bay.parent_device is not None:
1957
+ if self.parent_module_bay.parent_device is not None:
1957
1958
  return f"{self.module_type!s} installed in {self.parent_module_bay.parent_device.display}"
1958
- else:
1959
- return f"{self.module_type!s} installed in {self.parent_module_bay.parent_module.display}"
1959
+
1960
+ return f"{self.module_type!s} installed in {self.parent_module_bay.parent_module.display}"
1961
+
1962
+ @property
1963
+ def page_title(self):
1964
+ if self.location:
1965
+ return f"{self.module_type!s} at location {self.location}"
1966
+ if self.parent_module_bay.parent_device is not None:
1967
+ return f"{self.module_type!s} installed in {self.parent_module_bay.parent_device.display}"
1968
+
1969
+ return f"{self.module_type!s} installed in {self.parent_module_bay.parent_module.module_type!s}"
1960
1970
 
1961
1971
  @property
1962
1972
  def device(self):
@@ -4,12 +4,13 @@ from nautobot.core.apps import (
4
4
  NavMenuItem,
5
5
  NavMenuTab,
6
6
  )
7
+ from nautobot.core.ui.choices import NavigationIconChoices, NavigationWeightChoices
7
8
 
8
9
  menu_items = (
9
10
  NavMenuTab(
10
11
  name="Organization",
11
- icon="briefcase-2",
12
- weight=100,
12
+ icon=NavigationIconChoices.ORGANIZATION,
13
+ weight=NavigationWeightChoices.ORGANIZATION,
13
14
  groups=(
14
15
  NavMenuGroup(
15
16
  name="Locations",
@@ -116,8 +117,8 @@ menu_items = (
116
117
  ),
117
118
  NavMenuTab(
118
119
  name="Devices",
119
- icon="laptop",
120
- weight=200,
120
+ icon=NavigationIconChoices.DEVICES,
121
+ weight=NavigationWeightChoices.DEVICES,
121
122
  groups=(
122
123
  NavMenuGroup(
123
124
  name="Devices",
@@ -549,8 +550,8 @@ menu_items = (
549
550
  ),
550
551
  NavMenuTab(
551
552
  name="Power",
552
- icon="lightning",
553
- weight=600,
553
+ icon=NavigationIconChoices.POWER,
554
+ weight=NavigationWeightChoices.POWER,
554
555
  groups=(
555
556
  NavMenuGroup(
556
557
  name="Power",
@@ -11,6 +11,7 @@ from nautobot.core.tables import (
11
11
  TagColumn,
12
12
  ToggleColumn,
13
13
  )
14
+ from nautobot.core.templatetags.helpers import humanize_speed
14
15
  from nautobot.dcim.models import (
15
16
  ConsolePort,
16
17
  ConsoleServerPort,
@@ -295,6 +296,7 @@ class DeviceImportTable(StatusTableMixin, RoleTableMixin, BaseTable):
295
296
 
296
297
  class ModuleTable(StatusTableMixin, RoleTableMixin, BaseTable):
297
298
  pk = ToggleColumn()
299
+ id = tables.Column(linkify=True, verbose_name="ID")
298
300
  module_type = tables.Column(
299
301
  linkify=lambda record: record.module_type.get_absolute_url(),
300
302
  verbose_name="Type",
@@ -315,6 +317,7 @@ class ModuleTable(StatusTableMixin, RoleTableMixin, BaseTable):
315
317
  model = Module
316
318
  fields = (
317
319
  "pk",
320
+ "id",
318
321
  "module_type",
319
322
  "module_family",
320
323
  "parent_module_bay",
@@ -329,6 +332,7 @@ class ModuleTable(StatusTableMixin, RoleTableMixin, BaseTable):
329
332
  )
330
333
  default_columns = (
331
334
  "pk",
335
+ "id",
332
336
  "module_type",
333
337
  "module_family",
334
338
  "parent_module_bay",
@@ -725,6 +729,8 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
725
729
  url_params={"interfaces": "pk"},
726
730
  verbose_name="Virtual Device Contexts",
727
731
  )
732
+ speed = tables.Column(verbose_name="Speed", accessor="speed", orderable=True)
733
+ duplex = tables.Column(verbose_name="Duplex", accessor="duplex", orderable=True)
728
734
 
729
735
  class Meta(ModularDeviceComponentTable.Meta):
730
736
  model = Interface
@@ -738,6 +744,8 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
738
744
  "label",
739
745
  "enabled",
740
746
  "type",
747
+ "speed",
748
+ "duplex",
741
749
  "mgmt_only",
742
750
  "mtu",
743
751
  "vrf",
@@ -763,9 +771,13 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
763
771
  "label",
764
772
  "enabled",
765
773
  "type",
774
+ "speed",
766
775
  "description",
767
776
  )
768
777
 
778
+ def render_speed(self, record):
779
+ return humanize_speed(record.speed)
780
+
769
781
 
770
782
  class DeviceModuleInterfaceTable(InterfaceTable):
771
783
  name = tables.TemplateColumn(
@@ -791,6 +803,8 @@ class DeviceModuleInterfaceTable(InterfaceTable):
791
803
  "module",
792
804
  "enabled",
793
805
  "type",
806
+ "speed",
807
+ "duplex",
794
808
  "parent_interface",
795
809
  "bridge",
796
810
  "lag",
@@ -836,6 +850,9 @@ class DeviceModuleInterfaceTable(InterfaceTable):
836
850
  "data-name": lambda record: record.name,
837
851
  }
838
852
 
853
+ def render_speed(self, record):
854
+ return humanize_speed(record.speed)
855
+
839
856
 
840
857
  class FrontPortTable(ModularDeviceComponentTable, CableTerminationTable):
841
858
  rear_port_position = tables.Column(verbose_name="Position")
@@ -1288,6 +1305,7 @@ class InterfaceRedundancyGroupAssociationTable(BaseTable):
1288
1305
  """Table for list view."""
1289
1306
 
1290
1307
  pk = ToggleColumn()
1308
+ interface__enabled = BooleanColumn()
1291
1309
  interface_redundancy_group = tables.Column(linkify=True, verbose_name="Group Name")
1292
1310
  interface_redundancy_group__virtual_ip = tables.Column(linkify=True, verbose_name="Virtual IP")
1293
1311
  interface_redundancy_group__protocol_group_id = tables.Column(verbose_name="Group ID")
@@ -8,6 +8,7 @@ from nautobot.core.tables import (
8
8
  TagColumn,
9
9
  ToggleColumn,
10
10
  )
11
+ from nautobot.core.templatetags.helpers import humanize_speed
11
12
  from nautobot.dcim.models import (
12
13
  ConsolePortTemplate,
13
14
  ConsoleServerPortTemplate,
@@ -270,6 +271,8 @@ class PowerOutletTemplateTable(ComponentTemplateTable):
270
271
 
271
272
  class InterfaceTemplateTable(ComponentTemplateTable):
272
273
  mgmt_only = BooleanColumn(verbose_name="Management Only")
274
+ speed = tables.Column(verbose_name="Speed", accessor="speed", orderable=True)
275
+ duplex = tables.Column(verbose_name="Duplex", accessor="duplex", orderable=True)
273
276
  actions = ButtonsColumn(
274
277
  model=InterfaceTemplate,
275
278
  buttons=("edit", "delete"),
@@ -278,9 +281,13 @@ class InterfaceTemplateTable(ComponentTemplateTable):
278
281
 
279
282
  class Meta(BaseTable.Meta):
280
283
  model = InterfaceTemplate
281
- fields = ("pk", "name", "label", "mgmt_only", "type", "description", "actions")
284
+ fields = ("pk", "name", "label", "mgmt_only", "type", "speed", "duplex", "description", "actions")
285
+ default_columns = ("pk", "name", "label", "mgmt_only", "type", "speed", "description", "actions")
282
286
  empty_text = "None"
283
287
 
288
+ def render_speed(self, record):
289
+ return humanize_speed(record.speed)
290
+
284
291
 
285
292
  class FrontPortTemplateTable(ComponentTemplateTable):
286
293
  rear_port_position = tables.Column(verbose_name="Position")
@@ -130,8 +130,6 @@ class RackDetailTable(RackTable):
130
130
  "role",
131
131
  "u_height",
132
132
  "device_count",
133
- "get_utilization",
134
- "get_power_utilization",
135
133
  )
136
134
 
137
135
 
@@ -102,7 +102,7 @@ TREE_LINK = """
102
102
 
103
103
  POWERFEED_CABLE = """
104
104
  <a href="{{ value.get_absolute_url }}">{{ value }}</a>
105
- <a href="{% url 'dcim:powerfeed_trace' pk=record.pk %}" class="btn btn-primary btn-xs" title="Trace">
105
+ <a href="{% url 'dcim:powerfeed_trace' pk=record.pk %}" class="btn btn-primary btn-sm" title="Trace">
106
106
  <i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
107
107
  </a>
108
108
  """
@@ -137,8 +137,8 @@ CONSOLEPORT_BUTTONS = """
137
137
  <li><a href="{% url 'dcim:consoleport_trace' pk=record.pk %}" class="dropdown-item text-primary"><span class="mdi mdi-transit-connection-variant" aria-hidden="true"></span>Trace</a></li>
138
138
  {% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
139
139
  {% elif perms.dcim.add_cable %}
140
- <li><a href="#" class="dropdown-item disabled"><span class="mdi mdi-transit-connection-variant" aria-hidden="true"></span>Trace</a></li>
141
- <li><a href="#" class="dropdown-item disabled"><span class="mdi mdi-lan-connect" aria-hidden="true"></span>Mark installed</a></li>
140
+ <li><a class="dropdown-item disabled" aria-disabled="true"><span class="mdi mdi-transit-connection-variant" aria-hidden="true"></span>Trace</a></li>
141
+ <li><a class="dropdown-item disabled" aria-disabled="true"><span class="mdi mdi-lan-connect" aria-hidden="true"></span>Mark installed</a></li>
142
142
  <li>
143
143
  <a href="{% url 'dcim:consoleport_connect' termination_a_id=record.pk termination_b_type='console-server-port' %}?return_url={{ request.path }}" class="dropdown-item text-success">
144
144
  <span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
@@ -165,8 +165,8 @@ CONSOLESERVERPORT_BUTTONS = """
165
165
  <li><a href="{% url 'dcim:consoleserverport_trace' pk=record.pk %}" class="dropdown-item text-primary"><span class="mdi mdi-transit-connection-variant" aria-hidden="true"></span>Trace</a></li>
166
166
  {% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
167
167
  {% elif perms.dcim.add_cable %}
168
- <li><a href="#" class="dropdown-item disabled"><span class="mdi mdi-transit-connection-variant" aria-hidden="true"></span>Trace</a></li>
169
- <li><a href="#" class="dropdown-item disabled"><span class="mdi mdi-lan-connect" aria-hidden="true"></span>Mark installed</a></li>
168
+ <li><a class="dropdown-item disabled" aria-disabled="true"><span class="mdi mdi-transit-connection-variant" aria-hidden="true"></span>Trace</a></li>
169
+ <li><a class="dropdown-item disabled" aria-disabled="true"><span class="mdi mdi-lan-connect" aria-hidden="true"></span>Mark installed</a></li>
170
170
  <li>
171
171
  <a href="{% url 'dcim:consoleserverport_connect' termination_a_id=record.pk termination_b_type='console-port' %}?return_url={{ request.path }}" class="dropdown-item text-success">
172
172
  <span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
@@ -193,8 +193,8 @@ POWERPORT_BUTTONS = """
193
193
  <li><a href="{% url 'dcim:powerport_trace' pk=record.pk %}" class="dropdown-item text-primary"><span class="mdi mdi-transit-connection-variant" aria-hidden="true"></span>Trace</a></li>
194
194
  {% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
195
195
  {% elif perms.dcim.add_cable %}
196
- <li><a href="#" class="dropdown-item disabled"><span class="mdi mdi-transit-connection-variant" aria-hidden="true"></span>Trace</a></li>
197
- <li><a href="#" class="dropdown-item disabled"><span class="mdi mdi-lan-connect" aria-hidden="true"></span>Mark installed</a></li>
196
+ <li><a class="dropdown-item disabled" aria-disabled="true"><span class="mdi mdi-transit-connection-variant" aria-hidden="true"></span>Trace</a></li>
197
+ <li><a class="dropdown-item disabled" aria-disabled="true"><span class="mdi mdi-lan-connect" aria-hidden="true"></span>Mark installed</a></li>
198
198
  <li>
199
199
  <a href="{% url 'dcim:powerport_connect' termination_a_id=record.pk termination_b_type='power-outlet' %}?return_url={{ request.path }}" class="dropdown-item text-success">
200
200
  <span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
@@ -215,8 +215,8 @@ POWEROUTLET_BUTTONS = """
215
215
  <li><a href="{% url 'dcim:poweroutlet_trace' pk=record.pk %}" class="dropdown-item text-primary"><span class="mdi mdi-transit-connection-variant" aria-hidden="true"></span>Trace</a></li>
216
216
  {% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
217
217
  {% elif perms.dcim.add_cable %}
218
- <li><a href="#" class="dropdown-item disabled"><span class="mdi mdi-transit-connection-variant" aria-hidden="true"></span>Trace</a></li>
219
- <li><a href="#" class="dropdown-item disabled"><span class="mdi mdi-lan-connect" aria-hidden="true"></span>Mark installed</a></li>
218
+ <li><a class="dropdown-item disabled" aria-disabled="true"><span class="mdi mdi-transit-connection-variant" aria-hidden="true"></span>Trace</a></li>
219
+ <li><a class="dropdown-item disabled" aria-disabled="true"><span class="mdi mdi-lan-connect" aria-hidden="true"></span>Mark installed</a></li>
220
220
  <li>
221
221
  <a href="{% url 'dcim:poweroutlet_connect' termination_a_id=record.pk termination_b_type='power-port' %}?return_url={{ request.path }}" class="dropdown-item text-success">
222
222
  <span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
@@ -239,8 +239,8 @@ INTERFACE_BUTTONS = """
239
239
  <li><a href="{% url 'dcim:interface_trace' pk=record.pk %}" class="dropdown-item text-primary"><span class="mdi mdi-transit-connection-variant" aria-hidden="true"></span><Trace/a></li>
240
240
  {% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
241
241
  {% elif record.is_connectable and perms.dcim.add_cable %}
242
- <li><a href="#" class="dropdown-item disabled"><span class="mdi mdi-transit-connection-variant" aria-hidden="true"></span>Trace</a></li>
243
- <li><a href="#" class="dropdown-item disabled"><span class="mdi mdi-lan-connect" aria-hidden="true"></span>Mark installed</a></li>
242
+ <li><a class="dropdown-item disabled" aria-disabled="true"><span class="mdi mdi-transit-connection-variant" aria-hidden="true"></span>Trace</a></li>
243
+ <li><a class="dropdown-item disabled" aria-disabled="true"><span class="mdi mdi-lan-connect" aria-hidden="true"></span>Mark installed</a></li>
244
244
  <li>
245
245
  <a href="{% url 'dcim:interface_connect' termination_a_id=record.pk termination_b_type='interface' %}?return_url={{ request.path }}" class="dropdown-item text-success">
246
246
  <span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
@@ -273,8 +273,8 @@ FRONTPORT_BUTTONS = """
273
273
  <li><a href="{% url 'dcim:frontport_trace' pk=record.pk %}" class="dropdown-item text-primary"><span class="mdi mdi-transit-connection-variant" aria-hidden="true"></span>Trace</a></li>
274
274
  {% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
275
275
  {% elif perms.dcim.add_cable %}
276
- <li><a href="#" class="dropdown-item disabled"><span class="mdi mdi-transit-connection-variant" aria-hidden="true"></span>Trace</a></li>
277
- <li><a href="#" class="dropdown-item disabled"><span class="mdi mdi-lan-connect" aria-hidden="true"></span>Mark installed</a></li>
276
+ <li><a class="dropdown-item disabled" aria-disabled="true"><span class="mdi mdi-transit-connection-variant" aria-hidden="true"></span>Trace</a></li>
277
+ <li><a class="dropdown-item disabled" aria-disabled="true"><span class="mdi mdi-lan-connect" aria-hidden="true"></span>Mark installed</a></li>
278
278
  <li>
279
279
  <a href="{% url 'dcim:frontport_connect' termination_a_id=record.pk termination_b_type='interface' %}?return_url={{ request.path }}" class="dropdown-item text-success">
280
280
  <span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
@@ -319,8 +319,8 @@ REARPORT_BUTTONS = """
319
319
  <li><a href="{% url 'dcim:rearport_trace' pk=record.pk %}" class="dropdown-item text-primary"><span class="mdi mdi-transit-connection-variant" aria-hidden="true"></span>Trace</a></li>
320
320
  {% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
321
321
  {% elif perms.dcim.add_cable %}
322
- <li><a href="#" class="dropdown-item disabled"><span class="mdi mdi-transit-connection-variant" aria-hidden="true"></span>Trace</a></li>
323
- <li><a href="#" class="dropdown-item disabled"><span class="mdi mdi-lan-connect" aria-hidden="true"></span>Mark installed</a></li>
322
+ <li><a class="dropdown-item disabled" aria-disabled="true"><span class="mdi mdi-transit-connection-variant" aria-hidden="true"></span>Trace</a></li>
323
+ <li><a class="dropdown-item disabled" aria-disabled="true"><span class="mdi mdi-lan-connect" aria-hidden="true"></span>Mark installed</a></li>
324
324
  <li>
325
325
  <a href="{% url 'dcim:rearport_connect' termination_a_id=record.pk termination_b_type='interface' %}?return_url={{ request.path }}" class="dropdown-item text-success">
326
326
  <span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>