nautobot 2.4.21__py3-none-any.whl → 3.0.0a3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of nautobot might be problematic. Click here for more details.

Files changed (919) hide show
  1. nautobot/apps/choices.py +2 -2
  2. nautobot/apps/filters.py +9 -9
  3. nautobot/apps/forms.py +2 -0
  4. nautobot/apps/models.py +7 -2
  5. nautobot/apps/ui.py +20 -1
  6. nautobot/apps/utils.py +2 -3
  7. nautobot/apps/views.py +7 -1
  8. nautobot/circuits/filters.py +8 -23
  9. nautobot/circuits/navigation.py +3 -1
  10. nautobot/circuits/templates/circuits/circuit_create.html +9 -9
  11. nautobot/circuits/templates/circuits/circuit_terminations_swap.html +2 -2
  12. nautobot/circuits/templates/circuits/circuittermination_create.html +24 -33
  13. nautobot/circuits/templates/circuits/inc/circuit_termination.html +10 -10
  14. nautobot/circuits/templates/circuits/inc/circuit_termination_cable_fragment.html +13 -13
  15. nautobot/circuits/templates/circuits/inc/circuit_termination_header_extra_content.html +6 -6
  16. nautobot/circuits/templates/circuits/inc/circuit_termination_speed_fragment.html +3 -3
  17. nautobot/circuits/templates/circuits/inc/speed_widget.html +13 -13
  18. nautobot/circuits/templates/circuits/provider_create.html +9 -9
  19. nautobot/circuits/tests/integration/test_circuit.py +19 -19
  20. nautobot/circuits/tests/integration/test_circuits_bulk_operations.py +3 -0
  21. nautobot/circuits/tests/integration/test_relationships.py +4 -12
  22. nautobot/circuits/views.py +0 -2
  23. nautobot/cloud/filters.py +1 -13
  24. nautobot/cloud/navigation.py +3 -1
  25. nautobot/cloud/templates/cloud/cloudnetwork_update.html +9 -9
  26. nautobot/cloud/templates/cloud/cloudservice_update.html +6 -6
  27. nautobot/core/api/fields.py +30 -2
  28. nautobot/core/api/schema.py +1 -1
  29. nautobot/core/api/serializers.py +9 -2
  30. nautobot/core/api/urls.py +2 -0
  31. nautobot/core/api/views.py +58 -37
  32. nautobot/core/apps/__init__.py +6 -12
  33. nautobot/core/branching.py +83 -0
  34. nautobot/core/celery/__init__.py +11 -6
  35. nautobot/core/celery/backends.py +2 -0
  36. nautobot/core/celery/encoders.py +7 -0
  37. nautobot/core/celery/task.py +44 -0
  38. nautobot/core/checks.py +60 -0
  39. nautobot/core/cli/bootstrap_v3_to_v5.py +776 -0
  40. nautobot/core/constants.py +9 -0
  41. nautobot/core/context_processors.py +84 -0
  42. nautobot/core/filters.py +131 -2
  43. nautobot/core/forms/__init__.py +4 -2
  44. nautobot/core/forms/fields.py +10 -8
  45. nautobot/core/forms/forms.py +21 -9
  46. nautobot/core/forms/search.py +0 -15
  47. nautobot/core/forms/widgets.py +3 -2
  48. nautobot/core/graphql/__init__.py +8 -26
  49. nautobot/core/graphql/generators.py +16 -6
  50. nautobot/core/graphql/schema.py +1 -1
  51. nautobot/core/graphql/schema_init.py +1 -2
  52. nautobot/core/graphql/utils.py +7 -9
  53. nautobot/core/jobs/__init__.py +158 -0
  54. nautobot/core/management/commands/generate_test_data.py +28 -9
  55. nautobot/core/models/__init__.py +17 -2
  56. nautobot/core/models/fields.py +3 -2
  57. nautobot/core/models/generics.py +9 -1
  58. nautobot/core/models/name_color_content_types.py +1 -1
  59. nautobot/core/models/ordering.py +7 -5
  60. nautobot/core/models/querysets.py +77 -2
  61. nautobot/core/models/tree_queries.py +6 -4
  62. nautobot/core/settings.py +30 -16
  63. nautobot/core/settings.yaml +13 -7
  64. nautobot/core/tables.py +114 -44
  65. nautobot/core/templates/403.html +1 -1
  66. nautobot/core/templates/403_csrf_failure.html +1 -1
  67. nautobot/core/templates/404.html +1 -1
  68. nautobot/core/templates/40x.html +8 -8
  69. nautobot/core/templates/500.html +10 -10
  70. nautobot/core/templates/about.html +13 -12
  71. nautobot/core/templates/admin/actions.html +1 -1
  72. nautobot/core/templates/admin/app_index.html +3 -3
  73. nautobot/core/templates/admin/base.html +45 -52
  74. nautobot/core/templates/admin/base_site.html +0 -9
  75. nautobot/core/templates/admin/change_form.html +5 -5
  76. nautobot/core/templates/admin/change_list.html +8 -12
  77. nautobot/core/templates/admin/change_list_results.html +3 -3
  78. nautobot/core/templates/admin/config/config.html +24 -24
  79. nautobot/core/templates/admin/delete_confirmation.html +5 -5
  80. nautobot/core/templates/admin/edit_inline/stacked.html +5 -5
  81. nautobot/core/templates/admin/edit_inline/tabular.html +3 -3
  82. nautobot/core/templates/admin/includes/fieldset.html +15 -15
  83. nautobot/core/templates/admin/index.html +8 -8
  84. nautobot/core/templates/admin/submit_line.html +5 -5
  85. nautobot/core/templates/base_django.html +36 -32
  86. nautobot/core/templates/buttons/add.html +1 -1
  87. nautobot/core/templates/buttons/consolidated_detail_view_action_buttons.html +2 -2
  88. nautobot/core/templates/buttons/export.html +17 -18
  89. nautobot/core/templates/buttons/job_import.html +2 -2
  90. nautobot/core/templates/components/breadcrumbs.html +19 -17
  91. nautobot/core/templates/components/button/dropdown.html +7 -5
  92. nautobot/core/templates/components/echarts.html +2 -0
  93. nautobot/core/templates/components/layout/one_over_two.html +3 -3
  94. nautobot/core/templates/components/layout/two_over_one.html +3 -3
  95. nautobot/core/templates/components/panel/body_content_data_table.html +2 -2
  96. nautobot/core/templates/components/panel/body_content_tags.html +1 -1
  97. nautobot/core/templates/components/panel/body_wrapper_generic.html +4 -2
  98. nautobot/core/templates/components/panel/body_wrapper_generic_table.html +1 -1
  99. nautobot/core/templates/components/panel/body_wrapper_key_value_table.html +5 -3
  100. nautobot/core/templates/components/panel/body_wrapper_table.html +4 -2
  101. nautobot/core/templates/components/panel/footer_contacts_table.html +4 -4
  102. nautobot/core/templates/components/panel/footer_content_table.html +3 -3
  103. nautobot/core/templates/components/panel/grouping_toggle.html +12 -11
  104. nautobot/core/templates/components/panel/header_extra_content_table.html +2 -11
  105. nautobot/core/templates/components/panel/panel.html +6 -3
  106. nautobot/core/templates/components/panel/stats_panel_body.html +9 -7
  107. nautobot/core/templates/components/tab/content_wrapper.html +29 -1
  108. nautobot/core/templates/components/tab/label_wrapper.html +10 -2
  109. nautobot/core/templates/components/tab/label_wrapper_distinct_view.html +11 -4
  110. nautobot/core/templates/echarts/echarts.html +20 -0
  111. nautobot/core/templates/exceptions/import_error.html +2 -2
  112. nautobot/core/templates/exceptions/permission_error.html +1 -1
  113. nautobot/core/templates/exceptions/programming_error.html +2 -2
  114. nautobot/core/templates/generic/object_bulk_add_component.html +29 -20
  115. nautobot/core/templates/generic/object_bulk_create.html +87 -75
  116. nautobot/core/templates/generic/object_bulk_destroy.html +35 -37
  117. nautobot/core/templates/generic/object_bulk_remove.html +30 -26
  118. nautobot/core/templates/generic/object_bulk_rename.html +53 -40
  119. nautobot/core/templates/generic/object_bulk_update.html +36 -29
  120. nautobot/core/templates/generic/object_create.html +40 -27
  121. nautobot/core/templates/generic/object_import.html +36 -24
  122. nautobot/core/templates/generic/object_list.html +279 -215
  123. nautobot/core/templates/generic/object_notes.html +21 -11
  124. nautobot/core/templates/generic/object_retrieve.html +161 -213
  125. nautobot/core/templates/graphene/graphiql.html +113 -60
  126. nautobot/core/templates/home.html +164 -87
  127. nautobot/core/templates/import_success.html +3 -2
  128. nautobot/core/templates/inc/ajax_loader.html +1 -1
  129. nautobot/core/templates/inc/computed_fields/panel_data.html +25 -13
  130. nautobot/core/templates/inc/created_updated.html +12 -7
  131. nautobot/core/templates/inc/custom_fields/panel_data.html +28 -16
  132. nautobot/core/templates/inc/custom_fields_panel.html +3 -3
  133. nautobot/core/templates/inc/dynamic_groups_panel.html +3 -3
  134. nautobot/core/templates/inc/extras_features_edit_form_fields.html +15 -15
  135. nautobot/core/templates/inc/footer.html +90 -40
  136. nautobot/core/templates/inc/form_static_field.html +6 -0
  137. nautobot/core/templates/inc/header.html +75 -0
  138. nautobot/core/templates/inc/header_banners.html +17 -0
  139. nautobot/core/templates/inc/header_messages.html +6 -0
  140. nautobot/core/templates/inc/image_attachments.html +9 -9
  141. nautobot/core/templates/inc/javascript.html +7 -24
  142. nautobot/core/templates/inc/media.html +4 -29
  143. nautobot/core/templates/inc/modal.html +2 -2
  144. nautobot/core/templates/inc/nav_favorites.html +27 -0
  145. nautobot/core/templates/inc/nav_menu.html +150 -108
  146. nautobot/core/templates/inc/object_details_advanced_panel.html +84 -71
  147. nautobot/core/templates/inc/page_title.html +23 -0
  148. nautobot/core/templates/inc/paginator.html +39 -28
  149. nautobot/core/templates/inc/relationships/panel_override.html +3 -3
  150. nautobot/core/templates/inc/relationships_panel.html +3 -3
  151. nautobot/core/templates/inc/relationships_table_rows.html +1 -1
  152. nautobot/core/templates/inc/search_panel.html +22 -16
  153. nautobot/core/templates/inc/table.html +61 -36
  154. nautobot/core/templates/inc/tenancy_form_panel.html +3 -3
  155. nautobot/core/templates/login.html +17 -59
  156. nautobot/core/templates/modals/modal_theme.html +12 -23
  157. nautobot/core/templates/nautobot_config.py.j2 +6 -5
  158. nautobot/core/templates/panel_table.html +8 -12
  159. nautobot/core/templates/redoc_ui.html +80 -0
  160. nautobot/core/templates/rest_framework/api.html +43 -21
  161. nautobot/core/templates/search.html +12 -13
  162. nautobot/core/templates/swagger_ui.html +19 -4
  163. nautobot/core/templates/system_jobs/import_objects.html +70 -58
  164. nautobot/core/templates/template.css +0 -6
  165. nautobot/core/templates/utilities/comment_form.html +34 -0
  166. nautobot/core/templates/utilities/confirmation_form.html +17 -9
  167. nautobot/core/templates/utilities/obj_table.html +19 -11
  168. nautobot/core/templates/utilities/render_field.html +27 -21
  169. nautobot/core/templates/utilities/render_jinja2.html +22 -25
  170. nautobot/core/templates/utilities/templatetags/advanced_filter_indicator.html +8 -0
  171. nautobot/core/templates/utilities/templatetags/badge.html +1 -1
  172. nautobot/core/templates/utilities/templatetags/dynamic_group_assignment_modal.html +2 -3
  173. nautobot/core/templates/utilities/templatetags/filter_form_drawer.html +482 -0
  174. nautobot/core/templates/utilities/templatetags/modal_form_as_dialog.html +14 -18
  175. nautobot/core/templates/utilities/templatetags/saved_view_modal.html +11 -11
  176. nautobot/core/templates/utilities/templatetags/table_config_form.html +51 -24
  177. nautobot/core/templates/utilities/templatetags/tag.html +1 -1
  178. nautobot/core/templates/utilities/templatetags/utilization_graph.html +3 -3
  179. nautobot/core/templates/utilities/theme_preview.html +829 -566
  180. nautobot/core/templates/utilities/worker_status.html +42 -41
  181. nautobot/core/templates/widgets/selectwithdisabled_option.html +3 -1
  182. nautobot/core/templates/widgets/sluginput.html +2 -2
  183. nautobot/core/templatetags/buttons.py +38 -40
  184. nautobot/core/templatetags/helpers.py +105 -28
  185. nautobot/core/templatetags/ui_framework.py +17 -0
  186. nautobot/core/testing/api.py +76 -12
  187. nautobot/core/testing/filters.py +11 -27
  188. nautobot/core/testing/integration.py +128 -10
  189. nautobot/core/testing/mixins.py +7 -4
  190. nautobot/core/testing/utils.py +28 -5
  191. nautobot/core/testing/views.py +125 -27
  192. nautobot/core/tests/integration/test_app_home.py +39 -35
  193. nautobot/core/tests/integration/test_app_navbar.py +60 -67
  194. nautobot/core/tests/integration/test_filters.py +123 -55
  195. nautobot/core/tests/integration/test_general_functionality.py +1 -1
  196. nautobot/core/tests/integration/test_home.py +10 -18
  197. nautobot/core/tests/integration/test_import_objects_ui.py +2 -9
  198. nautobot/core/tests/integration/test_navbar.py +41 -16
  199. nautobot/core/tests/integration/test_swagger.py +1 -7
  200. nautobot/core/tests/integration/test_theme.py +3 -0
  201. nautobot/core/tests/nautobot_config_without_example_apps.py +4 -0
  202. nautobot/core/tests/runner.py +6 -1
  203. nautobot/core/tests/test_api.py +5 -3
  204. nautobot/core/tests/test_branching.py +154 -0
  205. nautobot/core/tests/test_breadcrumbs.py +7 -8
  206. nautobot/core/tests/test_checks.py +28 -0
  207. nautobot/core/tests/test_commands.py +0 -41
  208. nautobot/core/tests/test_config.py +2 -1
  209. nautobot/core/tests/test_csv.py +4 -7
  210. nautobot/core/tests/test_filters.py +326 -318
  211. nautobot/core/tests/test_forms.py +19 -30
  212. nautobot/core/tests/test_graphql.py +67 -57
  213. nautobot/core/tests/test_models.py +1 -1
  214. nautobot/core/tests/test_nautobot_server.py +2 -0
  215. nautobot/core/tests/test_navigations.py +78 -10
  216. nautobot/core/tests/test_tables.py +3 -1
  217. nautobot/core/tests/test_templatetags_helpers.py +61 -21
  218. nautobot/core/tests/test_templatetags_ui_framework.py +36 -18
  219. nautobot/core/tests/test_ui.py +207 -2
  220. nautobot/core/tests/test_utils.py +147 -2
  221. nautobot/core/tests/test_views.py +201 -64
  222. nautobot/core/tests/test_views_utils.py +1 -1
  223. nautobot/core/ui/breadcrumbs.py +2 -12
  224. nautobot/core/ui/choices.py +190 -0
  225. nautobot/core/ui/constants.py +86 -0
  226. nautobot/core/ui/echarts.py +474 -0
  227. nautobot/core/ui/nav.py +5 -1
  228. nautobot/core/ui/object_detail.py +180 -16
  229. nautobot/core/urls.py +13 -1
  230. nautobot/core/utils/cache.py +71 -0
  231. nautobot/core/utils/data.py +8 -5
  232. nautobot/core/utils/filtering.py +8 -2
  233. nautobot/core/utils/git.py +3 -3
  234. nautobot/core/utils/lookup.py +87 -13
  235. nautobot/core/utils/migrations.py +22 -0
  236. nautobot/core/utils/module_loading.py +26 -0
  237. nautobot/core/utils/permissions.py +9 -5
  238. nautobot/core/views/__init__.py +114 -63
  239. nautobot/core/views/generic.py +34 -27
  240. nautobot/core/views/mixins.py +49 -27
  241. nautobot/core/views/renderers.py +3 -5
  242. nautobot/core/views/utils.py +10 -5
  243. nautobot/core/views/viewsets.py +2 -1
  244. nautobot/data_validation/__init__.py +0 -0
  245. nautobot/data_validation/api/__init__.py +1 -0
  246. nautobot/data_validation/api/serializers.py +80 -0
  247. nautobot/data_validation/api/urls.py +20 -0
  248. nautobot/data_validation/api/views.py +44 -0
  249. nautobot/data_validation/apps.py +18 -0
  250. nautobot/data_validation/custom_validators.py +330 -0
  251. nautobot/data_validation/filters.py +133 -0
  252. nautobot/data_validation/form_mixin.py +25 -0
  253. nautobot/data_validation/forms.py +342 -0
  254. nautobot/data_validation/migrations/0001_initial.py +224 -0
  255. nautobot/data_validation/migrations/0002_data_migration_from_app.py +324 -0
  256. nautobot/data_validation/migrations/__init__.py +0 -0
  257. nautobot/data_validation/models.py +361 -0
  258. nautobot/data_validation/navigation.py +74 -0
  259. nautobot/data_validation/signals.py +30 -0
  260. nautobot/data_validation/tables.py +259 -0
  261. nautobot/data_validation/templates/data_validation/datacompliance_retrieve.html +1 -0
  262. nautobot/data_validation/templates/data_validation/datacompliance_tab.html +11 -0
  263. nautobot/data_validation/templates/data_validation/device_constraints.html +61 -0
  264. nautobot/data_validation/tests/__init__.py +20 -0
  265. nautobot/data_validation/tests/migrations/__init__.py +0 -0
  266. nautobot/data_validation/tests/migrations/test_migrations.py +489 -0
  267. nautobot/data_validation/tests/test_api.py +238 -0
  268. nautobot/data_validation/tests/test_custom_validators.py +423 -0
  269. nautobot/data_validation/tests/test_data_compliance_rules.py +85 -0
  270. nautobot/data_validation/tests/test_filters.py +240 -0
  271. nautobot/data_validation/tests/test_form_mixin.py +115 -0
  272. nautobot/data_validation/tests/test_models.py +393 -0
  273. nautobot/data_validation/tests/test_views.py +435 -0
  274. nautobot/data_validation/urls.py +21 -0
  275. nautobot/data_validation/views.py +227 -0
  276. nautobot/dcim/api/serializers.py +10 -13
  277. nautobot/dcim/api/urls.py +2 -0
  278. nautobot/dcim/api/views.py +7 -0
  279. nautobot/dcim/apps.py +4 -0
  280. nautobot/dcim/choices.py +16 -0
  281. nautobot/dcim/custom_validators.py +84 -0
  282. nautobot/dcim/filter_mixins.py +353 -4
  283. nautobot/dcim/{filters/__init__.py → filters.py} +70 -157
  284. nautobot/dcim/forms.py +12 -6
  285. nautobot/dcim/graphql/types.py +1 -0
  286. nautobot/dcim/migrations/0075_add_deviceclusterassignment.py +52 -0
  287. nautobot/dcim/migrations/0076_device_cluster_to_clusters_data_migration.py +40 -0
  288. nautobot/dcim/migrations/0077_remove_device_cluster.py +14 -0
  289. nautobot/dcim/migrations/0078_remove_device_location_tenant_name_uniqueness.py +16 -0
  290. nautobot/dcim/migrations/0079_device_name_data_migration.py +59 -0
  291. nautobot/dcim/models/__init__.py +2 -0
  292. nautobot/dcim/models/device_components.py +3 -1
  293. nautobot/dcim/models/devices.py +115 -51
  294. nautobot/dcim/navigation.py +7 -3
  295. nautobot/dcim/querysets.py +6 -0
  296. nautobot/dcim/signals.py +19 -0
  297. nautobot/dcim/tables/devices.py +9 -5
  298. nautobot/dcim/tables/template_code.py +191 -102
  299. nautobot/dcim/templates/dcim/cable.html +1 -1
  300. nautobot/dcim/templates/dcim/cable_connect.html +62 -146
  301. nautobot/dcim/templates/dcim/cable_retrieve.html +10 -10
  302. nautobot/dcim/templates/dcim/cable_trace.html +15 -17
  303. nautobot/dcim/templates/dcim/console_port_connection_list.html +2 -2
  304. nautobot/dcim/templates/dcim/consoleport.html +18 -17
  305. nautobot/dcim/templates/dcim/consoleserverport.html +18 -17
  306. nautobot/dcim/templates/dcim/controller_create.html +12 -8
  307. nautobot/dcim/templates/dcim/controller_wirelessnetworks.html +1 -1
  308. nautobot/dcim/templates/dcim/controllermanageddevicegroup_create.html +6 -6
  309. nautobot/dcim/templates/dcim/controllermanageddevicegroup_retrieve.html +1 -1
  310. nautobot/dcim/templates/dcim/device/config.html +17 -19
  311. nautobot/dcim/templates/dcim/device/lldp_neighbors.html +4 -4
  312. nautobot/dcim/templates/dcim/device/status.html +20 -20
  313. nautobot/dcim/templates/dcim/device_component_add.html +24 -15
  314. nautobot/dcim/templates/dcim/device_create.html +120 -120
  315. nautobot/dcim/templates/dcim/device_list.html +75 -12
  316. nautobot/dcim/templates/dcim/devicebay.html +7 -7
  317. nautobot/dcim/templates/dcim/devicebay_populate.html +29 -23
  318. nautobot/dcim/templates/dcim/deviceredundancygroup_create.html +6 -6
  319. nautobot/dcim/templates/dcim/devicetype.html +1 -1
  320. nautobot/dcim/templates/dcim/devicetype_component_add.html +25 -19
  321. nautobot/dcim/templates/dcim/devicetype_list.html +4 -4
  322. nautobot/dcim/templates/dcim/devicetype_update.html +9 -9
  323. nautobot/dcim/templates/dcim/footer_convert_to_contact_or_team_record.html +3 -3
  324. nautobot/dcim/templates/dcim/frontport.html +21 -20
  325. nautobot/dcim/templates/dcim/inc/cable_form.html +7 -7
  326. nautobot/dcim/templates/dcim/inc/cable_termination.html +1 -1
  327. nautobot/dcim/templates/dcim/inc/cable_toggle_buttons.html +18 -9
  328. nautobot/dcim/templates/dcim/inc/detail_softwareversion_softwareimagefile_rows.html +1 -1
  329. nautobot/dcim/templates/dcim/inc/device_interface_filter.html +1 -1
  330. nautobot/dcim/templates/dcim/inc/devicetype_component_table.html +10 -10
  331. nautobot/dcim/templates/dcim/inc/edit_form_softwareversion_js.html +2 -2
  332. nautobot/dcim/templates/dcim/inc/homepage_connections.html +2 -2
  333. nautobot/dcim/templates/dcim/inc/moduletype_component_table.html +10 -10
  334. nautobot/dcim/templates/dcim/inc/rack_elevation.html +2 -2
  335. nautobot/dcim/templates/dcim/interface.html +42 -22
  336. nautobot/dcim/templates/dcim/interface_connection_list.html +2 -2
  337. nautobot/dcim/templates/dcim/interface_edit.html +26 -11
  338. nautobot/dcim/templates/dcim/interfaceredundancygroupassociation_create.html +3 -3
  339. nautobot/dcim/templates/dcim/inventoryitem.html +3 -3
  340. nautobot/dcim/templates/dcim/inventoryitem_add.html +21 -10
  341. nautobot/dcim/templates/dcim/inventoryitem_bulk_delete.html +1 -1
  342. nautobot/dcim/templates/dcim/inventoryitem_edit.html +6 -4
  343. nautobot/dcim/templates/dcim/location.html +1 -1
  344. nautobot/dcim/templates/dcim/location_migrate_data_to_contact.html +24 -18
  345. nautobot/dcim/templates/dcim/location_retrieve.html +1 -1
  346. nautobot/dcim/templates/dcim/location_update.html +9 -9
  347. nautobot/dcim/templates/dcim/locationtype.html +0 -1
  348. nautobot/dcim/templates/dcim/module/base.html +67 -27
  349. nautobot/dcim/templates/dcim/module_consoleports.html +13 -15
  350. nautobot/dcim/templates/dcim/module_consoleserverports.html +13 -15
  351. nautobot/dcim/templates/dcim/module_frontports.html +13 -15
  352. nautobot/dcim/templates/dcim/module_interfaces.html +14 -16
  353. nautobot/dcim/templates/dcim/module_list.html +59 -10
  354. nautobot/dcim/templates/dcim/module_modulebays.html +12 -14
  355. nautobot/dcim/templates/dcim/module_poweroutlets.html +13 -15
  356. nautobot/dcim/templates/dcim/module_powerports.html +13 -15
  357. nautobot/dcim/templates/dcim/module_rearports.html +13 -15
  358. nautobot/dcim/templates/dcim/module_retrieve.html +3 -3
  359. nautobot/dcim/templates/dcim/module_update.html +15 -9
  360. nautobot/dcim/templates/dcim/modulebay_retrieve.html +0 -93
  361. nautobot/dcim/templates/dcim/modulefamily_retrieve.html +7 -7
  362. nautobot/dcim/templates/dcim/moduletype_list.html +2 -2
  363. nautobot/dcim/templates/dcim/moduletype_retrieve.html +74 -35
  364. nautobot/dcim/templates/dcim/platform_create.html +9 -9
  365. nautobot/dcim/templates/dcim/power_port_connection_list.html +3 -3
  366. nautobot/dcim/templates/dcim/powerfeed.html +1 -1
  367. nautobot/dcim/templates/dcim/powerfeed_edit.html +15 -15
  368. nautobot/dcim/templates/dcim/poweroutlet.html +13 -13
  369. nautobot/dcim/templates/dcim/powerpanel.html +1 -1
  370. nautobot/dcim/templates/dcim/powerport.html +17 -16
  371. nautobot/dcim/templates/dcim/rack.html +1 -1
  372. nautobot/dcim/templates/dcim/rack_elevation.html +2 -2
  373. nautobot/dcim/templates/dcim/rack_elevation_list.html +21 -9
  374. nautobot/dcim/templates/dcim/rack_retrieve.html +75 -57
  375. nautobot/dcim/templates/dcim/rack_update.html +14 -14
  376. nautobot/dcim/templates/dcim/rackreservation.html +1 -1
  377. nautobot/dcim/templates/dcim/rackreservation_edit.html +6 -6
  378. nautobot/dcim/templates/dcim/rearport.html +19 -18
  379. nautobot/dcim/templates/dcim/trace/cable.html +1 -1
  380. nautobot/dcim/templates/dcim/trace/circuit.html +1 -1
  381. nautobot/dcim/templates/dcim/trace/device.html +1 -1
  382. nautobot/dcim/templates/dcim/trace/powerpanel.html +1 -1
  383. nautobot/dcim/templates/dcim/trace/termination.html +1 -1
  384. nautobot/dcim/templates/dcim/virtualchassis.html +1 -1
  385. nautobot/dcim/templates/dcim/virtualchassis_add_member.html +25 -16
  386. nautobot/dcim/templates/dcim/virtualchassis_create.html +6 -6
  387. nautobot/dcim/templates/dcim/virtualchassis_edit.html +1 -1
  388. nautobot/dcim/templates/dcim/virtualchassis_retrieve.html +1 -1
  389. nautobot/dcim/templates/dcim/virtualchassis_update.html +36 -22
  390. nautobot/dcim/templates/dcim/virtualdevicecontext_update.html +9 -9
  391. nautobot/dcim/tests/integration/test_controller.py +6 -6
  392. nautobot/dcim/tests/integration/test_controller_managed_device_group.py +7 -7
  393. nautobot/dcim/tests/integration/test_create_device.py +9 -9
  394. nautobot/dcim/tests/integration/test_device_bulk_operations.py +7 -2
  395. nautobot/dcim/tests/integration/test_fileinputpicker.py +5 -7
  396. nautobot/dcim/tests/integration/test_location_bulk_operations.py +2 -0
  397. nautobot/dcim/tests/integration/test_module_bay_position.py +4 -1
  398. nautobot/dcim/tests/test_api.py +86 -6
  399. nautobot/dcim/tests/test_custom_validators.py +229 -0
  400. nautobot/dcim/tests/test_filters.py +159 -110
  401. nautobot/dcim/tests/test_graphql.py +32 -36
  402. nautobot/dcim/tests/test_jobs.py +1 -1
  403. nautobot/dcim/tests/test_models.py +229 -1
  404. nautobot/dcim/tests/test_views.py +31 -20
  405. nautobot/dcim/utils.py +3 -3
  406. nautobot/dcim/views.py +77 -41
  407. nautobot/extras/api/serializers.py +83 -19
  408. nautobot/extras/api/urls.py +7 -0
  409. nautobot/extras/api/views.py +243 -140
  410. nautobot/extras/choices.py +34 -13
  411. nautobot/extras/constants.py +1 -1
  412. nautobot/extras/context_managers.py +26 -26
  413. nautobot/extras/datasources/git.py +22 -0
  414. nautobot/extras/datasources/registry.py +3 -0
  415. nautobot/extras/exceptions.py +5 -0
  416. nautobot/extras/factory.py +11 -1
  417. nautobot/extras/{filters/mixins.py → filter_mixins.py} +4 -3
  418. nautobot/extras/{filters/__init__.py → filters.py} +203 -58
  419. nautobot/extras/forms/base.py +2 -1
  420. nautobot/extras/forms/forms.py +225 -20
  421. nautobot/extras/forms/mixins.py +0 -41
  422. nautobot/extras/homepage.py +21 -2
  423. nautobot/extras/jobs.py +2 -8
  424. nautobot/extras/jobs_ui.py +2 -2
  425. nautobot/extras/management/__init__.py +9 -0
  426. nautobot/extras/managers.py +31 -22
  427. nautobot/extras/migrations/0126_approval_workflow_pre_check.py +58 -0
  428. nautobot/extras/migrations/0127_approval_workflow_models.py +266 -0
  429. nautobot/extras/migrations/0128_remove_job_approval_required_and_more.py +29 -0
  430. nautobot/extras/migrations/0129_jobresult_debug_log_count_jobresult_error_log_count_and_more.py +37 -0
  431. nautobot/extras/migrations/0130_jobresult_generate_log_entry_counts.py +42 -0
  432. nautobot/extras/models/__init__.py +14 -3
  433. nautobot/extras/models/approvals.py +556 -0
  434. nautobot/extras/models/change_logging.py +1 -0
  435. nautobot/extras/models/contacts.py +2 -0
  436. nautobot/extras/models/customfields.py +57 -22
  437. nautobot/extras/models/datasources.py +21 -0
  438. nautobot/extras/models/groups.py +2 -0
  439. nautobot/extras/models/jobs.py +122 -39
  440. nautobot/extras/models/metadata.py +2 -3
  441. nautobot/extras/models/mixins.py +129 -1
  442. nautobot/extras/models/models.py +22 -14
  443. nautobot/extras/models/relationships.py +47 -10
  444. nautobot/extras/models/secrets.py +1 -0
  445. nautobot/extras/models/statuses.py +0 -15
  446. nautobot/extras/models/tags.py +1 -1
  447. nautobot/extras/navigation.py +42 -15
  448. nautobot/extras/plugins/__init__.py +33 -55
  449. nautobot/extras/plugins/marketplace_manifest.yml +1 -23
  450. nautobot/extras/plugins/tables.py +8 -6
  451. nautobot/extras/plugins/urls.py +2 -21
  452. nautobot/extras/plugins/utils.py +1 -33
  453. nautobot/extras/plugins/validators.py +10 -10
  454. nautobot/extras/plugins/views.py +1 -5
  455. nautobot/extras/querysets.py +17 -21
  456. nautobot/extras/signals.py +23 -8
  457. nautobot/extras/tables.py +420 -99
  458. nautobot/extras/templates/extras/approval_dashboard.html +15 -0
  459. nautobot/extras/templates/extras/approval_workflow/approve.html +11 -0
  460. nautobot/extras/templates/extras/approval_workflow/comment.html +9 -0
  461. nautobot/extras/templates/extras/approval_workflow/deny.html +10 -0
  462. nautobot/extras/templates/extras/approvalworkflowdefinition_update.html +77 -0
  463. nautobot/extras/templates/extras/approvalworkflowstage_retrieve.html +29 -0
  464. nautobot/extras/templates/extras/configcontext_update.html +12 -12
  465. nautobot/extras/templates/extras/configcontextschema.html +1 -1
  466. nautobot/extras/templates/extras/configcontextschema_retrieve.html +9 -9
  467. nautobot/extras/templates/extras/configcontextschema_update.html +6 -6
  468. nautobot/extras/templates/extras/configcontextschema_validation.html +2 -2
  469. nautobot/extras/templates/extras/customfield_update.html +12 -12
  470. nautobot/extras/templates/extras/dynamicgroup.html +1 -1
  471. nautobot/extras/templates/extras/dynamicgroup_edit.html +1 -1
  472. nautobot/extras/templates/extras/dynamicgroup_retrieve.html +17 -17
  473. nautobot/extras/templates/extras/dynamicgroup_update.html +24 -24
  474. nautobot/extras/templates/extras/externalintegration_update.html +6 -6
  475. nautobot/extras/templates/extras/gitrepository.html +1 -1
  476. nautobot/extras/templates/extras/gitrepository_object_edit.html +1 -1
  477. nautobot/extras/templates/extras/gitrepository_result.html +1 -1
  478. nautobot/extras/templates/extras/gitrepository_retrieve.html +12 -12
  479. nautobot/extras/templates/extras/gitrepository_update.html +25 -7
  480. nautobot/extras/templates/extras/graphqlquery_retrieve.html +1 -1
  481. nautobot/extras/templates/extras/inc/approval_buttons_column.html +38 -0
  482. nautobot/extras/templates/extras/inc/bulk_edit_overridable_field.html +14 -13
  483. nautobot/extras/templates/extras/inc/configcontext_format.html +11 -4
  484. nautobot/extras/templates/extras/inc/graphqlquery_execute.html +7 -7
  485. nautobot/extras/templates/extras/inc/job_label.html +5 -5
  486. nautobot/extras/templates/extras/inc/job_table.html +23 -10
  487. nautobot/extras/templates/extras/inc/job_tiles.html +33 -21
  488. nautobot/extras/templates/extras/inc/jobresult.html +6 -6
  489. nautobot/extras/templates/extras/inc/json_format.html +11 -4
  490. nautobot/extras/templates/extras/inc/object_contact_header.html +6 -6
  491. nautobot/extras/templates/extras/inc/overridable_field.html +16 -15
  492. nautobot/extras/templates/extras/inc/panel_approvalworkflowstage.html +34 -0
  493. nautobot/extras/templates/extras/inc/panel_changelog.html +9 -9
  494. nautobot/extras/templates/extras/inc/panel_jobhistory.html +8 -6
  495. nautobot/extras/templates/extras/inc/tags_panel.html +3 -3
  496. nautobot/extras/templates/extras/job.html +154 -155
  497. nautobot/extras/templates/extras/job_approval_confirmation.html +4 -27
  498. nautobot/extras/templates/extras/job_bulk_edit.html +18 -1
  499. nautobot/extras/templates/extras/job_detail.html +1 -1
  500. nautobot/extras/templates/extras/job_edit.html +69 -64
  501. nautobot/extras/templates/extras/job_list.html +37 -60
  502. nautobot/extras/templates/extras/jobresult.html +1 -1
  503. nautobot/extras/templates/extras/jobresult_retrieve.html +17 -17
  504. nautobot/extras/templates/extras/marketplace.html +62 -71
  505. nautobot/extras/templates/extras/metadatatype_create.html +9 -9
  506. nautobot/extras/templates/extras/note.html +1 -1
  507. nautobot/extras/templates/extras/object_approvalworkflow.html +36 -0
  508. nautobot/extras/templates/extras/object_assign_contact_or_team.html +16 -7
  509. nautobot/extras/templates/extras/object_configcontext.html +20 -20
  510. nautobot/extras/templates/extras/object_new_contact.html +6 -6
  511. nautobot/extras/templates/extras/object_new_team.html +6 -6
  512. nautobot/extras/templates/extras/objectchange.html +1 -1
  513. nautobot/extras/templates/extras/objectchange_retrieve.html +37 -56
  514. nautobot/extras/templates/extras/plugin_detail.html +40 -41
  515. nautobot/extras/templates/extras/plugins_list.html +23 -38
  516. nautobot/extras/templates/extras/plugins_tiles.html +28 -28
  517. nautobot/extras/templates/extras/role_retrieve.html +112 -48
  518. nautobot/extras/templates/extras/scheduledjob.html +25 -28
  519. nautobot/extras/templates/extras/secret_create.html +11 -11
  520. nautobot/extras/templates/extras/secretsgroup_update.html +6 -6
  521. nautobot/extras/templates/extras/staticgroupassociation_retrieve.html +3 -3
  522. nautobot/extras/templates/extras/status.html +1 -1
  523. nautobot/extras/templates/extras/tag.html +1 -1
  524. nautobot/extras/templates/extras/tag_update.html +3 -3
  525. nautobot/extras/templates/extras/templatetags/log_level.html +1 -1
  526. nautobot/extras/templates/extras/templatetags/plugin_object_detail_tabs.html +2 -2
  527. nautobot/extras/templates/extras/webhook.html +12 -12
  528. nautobot/extras/templatetags/approvals.py +19 -0
  529. nautobot/extras/templatetags/custom_links.py +12 -12
  530. nautobot/extras/templatetags/job_buttons.py +14 -12
  531. nautobot/extras/test_jobs/invalid_import.py +9 -0
  532. nautobot/extras/test_jobs/log_counts_by_level.py +23 -0
  533. nautobot/extras/test_jobs/missing_import.py +11 -0
  534. nautobot/extras/tests/integration/test_computedfields.py +5 -8
  535. nautobot/extras/tests/integration/test_configcontextschema.py +43 -48
  536. nautobot/extras/tests/integration/test_customfields.py +33 -33
  537. nautobot/extras/tests/integration/test_dynamicgroups.py +5 -10
  538. nautobot/extras/tests/integration/test_jobs.py +2 -4
  539. nautobot/extras/tests/integration/test_notes.py +3 -9
  540. nautobot/extras/tests/integration/test_plugin_banner.py +3 -0
  541. nautobot/extras/tests/integration/test_plugins.py +35 -27
  542. nautobot/extras/tests/integration/test_relationships.py +7 -11
  543. nautobot/extras/tests/integration/test_tagfilter.py +3 -11
  544. nautobot/extras/tests/test_api.py +786 -242
  545. nautobot/extras/tests/test_approvals.py +715 -0
  546. nautobot/extras/tests/test_changelog.py +18 -14
  547. nautobot/extras/tests/test_customfields.py +14 -13
  548. nautobot/extras/tests/test_datasources.py +1 -1
  549. nautobot/extras/tests/test_dynamicgroups.py +9 -4
  550. nautobot/extras/tests/test_filters.py +443 -13
  551. nautobot/extras/tests/test_forms.py +18 -57
  552. nautobot/extras/tests/test_jobs.py +25 -4
  553. nautobot/extras/tests/test_migrations.py +81 -1
  554. nautobot/extras/tests/test_models.py +378 -47
  555. nautobot/extras/tests/test_plugins.py +47 -13
  556. nautobot/extras/tests/test_relationships.py +7 -2
  557. nautobot/extras/tests/test_utils.py +2 -0
  558. nautobot/extras/tests/test_views.py +780 -493
  559. nautobot/extras/urls.py +36 -12
  560. nautobot/extras/utils.py +58 -12
  561. nautobot/extras/views.py +668 -209
  562. nautobot/ipam/factory.py +7 -0
  563. nautobot/ipam/filter_mixins.py +38 -0
  564. nautobot/ipam/filters.py +35 -71
  565. nautobot/ipam/formfields.py +1 -1
  566. nautobot/ipam/forms.py +6 -3
  567. nautobot/ipam/migrations/0030_ipam__namespaces.py +13 -0
  568. nautobot/ipam/migrations/0031_ipam___data_migrations.py +4 -1
  569. nautobot/ipam/migrations/0054_namespace_tenant.py +25 -0
  570. nautobot/ipam/models.py +29 -2
  571. nautobot/ipam/navigation.py +3 -1
  572. nautobot/ipam/querysets.py +1 -2
  573. nautobot/ipam/tables.py +26 -17
  574. nautobot/ipam/templates/ipam/inc/ipadress_edit_header.html +6 -6
  575. nautobot/ipam/templates/ipam/inc/service.html +8 -8
  576. nautobot/ipam/templates/ipam/inc/toggle_available.html +10 -10
  577. nautobot/ipam/templates/ipam/inc/vlangroup_header.html +3 -2
  578. nautobot/ipam/templates/ipam/ipaddress.html +27 -13
  579. nautobot/ipam/templates/ipam/ipaddress_assign.html +31 -24
  580. nautobot/ipam/templates/ipam/ipaddress_bulk_add.html +3 -3
  581. nautobot/ipam/templates/ipam/ipaddress_edit.html +9 -9
  582. nautobot/ipam/templates/ipam/ipaddress_interfaces.html +7 -9
  583. nautobot/ipam/templates/ipam/ipaddress_merge.html +195 -186
  584. nautobot/ipam/templates/ipam/ipaddress_vm_interfaces.html +7 -9
  585. nautobot/ipam/templates/ipam/ipaddresstointerface_retrieve.html +7 -5
  586. nautobot/ipam/templates/ipam/namespace_ip_addresses.html +1 -1
  587. nautobot/ipam/templates/ipam/namespace_prefixes.html +1 -1
  588. nautobot/ipam/templates/ipam/namespace_update.html +15 -0
  589. nautobot/ipam/templates/ipam/namespace_vrfs.html +1 -1
  590. nautobot/ipam/templates/ipam/prefix_create.html +9 -9
  591. nautobot/ipam/templates/ipam/prefix_list.html +15 -14
  592. nautobot/ipam/templates/ipam/prefix_retrieve.html +0 -1
  593. nautobot/ipam/templates/ipam/vlan.html +1 -1
  594. nautobot/ipam/templates/ipam/vlan_interfaces.html +1 -1
  595. nautobot/ipam/templates/ipam/vlan_update.html +6 -6
  596. nautobot/ipam/templates/ipam/vlan_vminterfaces.html +1 -1
  597. nautobot/ipam/templates/ipam/vrf_edit.html +15 -15
  598. nautobot/ipam/tests/integration/test_prefixes.py +5 -13
  599. nautobot/ipam/tests/migration/test_migrations.py +89 -0
  600. nautobot/ipam/tests/test_api.py +20 -7
  601. nautobot/ipam/tests/test_filters.py +10 -0
  602. nautobot/ipam/tests/test_forms.py +1 -1
  603. nautobot/ipam/tests/test_models.py +1 -1
  604. nautobot/ipam/tests/test_tables.py +1 -2
  605. nautobot/ipam/tests/test_utils.py +1 -1
  606. nautobot/ipam/tests/test_views.py +24 -21
  607. nautobot/ipam/ui.py +0 -17
  608. nautobot/ipam/utils/migrations.py +16 -2
  609. nautobot/ipam/utils/testing.py +9 -3
  610. nautobot/ipam/views.py +49 -7
  611. nautobot/project-static/dist/css/graphql-libraries.css +655 -0
  612. nautobot/project-static/dist/css/graphql-libraries.css.map +1 -0
  613. nautobot/project-static/dist/css/materialdesignicons.css +3 -0
  614. nautobot/project-static/dist/css/materialdesignicons.css.map +1 -0
  615. nautobot/project-static/dist/css/nautobot.css +13 -0
  616. nautobot/project-static/dist/css/nautobot.css.map +1 -0
  617. nautobot/project-static/dist/js/graphql-libraries.js +3 -0
  618. nautobot/project-static/dist/js/graphql-libraries.js.LICENSE.txt +62 -0
  619. nautobot/project-static/dist/js/graphql-libraries.js.map +1 -0
  620. nautobot/project-static/dist/js/libraries.js +3 -0
  621. nautobot/project-static/dist/js/libraries.js.LICENSE.txt +65 -0
  622. nautobot/project-static/dist/js/libraries.js.map +1 -0
  623. nautobot/project-static/dist/js/materialdesignicons.js +0 -0
  624. nautobot/project-static/dist/js/nautobot-graphiql.js +2 -0
  625. nautobot/project-static/dist/js/nautobot-graphiql.js.map +1 -0
  626. nautobot/project-static/dist/js/nautobot.js +2 -0
  627. nautobot/project-static/dist/js/nautobot.js.map +1 -0
  628. nautobot/project-static/fonts/Montserrat-v30-Bold.woff2 +0 -0
  629. nautobot/project-static/fonts/Montserrat-v30-Light.woff2 +0 -0
  630. nautobot/project-static/fonts/Montserrat-v30-Regular.woff2 +0 -0
  631. nautobot/project-static/fonts/Roboto-v48-Bold.woff2 +0 -0
  632. nautobot/project-static/fonts/Roboto-v48-Light.woff2 +0 -0
  633. nautobot/project-static/fonts/Roboto-v48-Regular.woff2 +0 -0
  634. nautobot/project-static/img/jinja_logo.svg +21 -92
  635. nautobot/project-static/js/cabletrace.js +1 -1
  636. nautobot/project-static/js/editor.js +4 -4
  637. nautobot/project-static/js/forms.js +67 -717
  638. nautobot/project-static/js/job_result.js +2 -2
  639. nautobot/project-static/nautobot-icons/360-degrees.svg +3 -0
  640. nautobot/project-static/nautobot-icons/arrow-decision.svg +3 -0
  641. nautobot/project-static/nautobot-icons/arrows-expand-rec.svg +3 -0
  642. nautobot/project-static/nautobot-icons/arrows-move-2-rec.svg +3 -0
  643. nautobot/project-static/nautobot-icons/arrows-move-rec.svg +3 -0
  644. nautobot/project-static/nautobot-icons/atom.svg +3 -0
  645. nautobot/project-static/nautobot-icons/battery-3.svg +3 -0
  646. nautobot/project-static/nautobot-icons/branch.svg +3 -0
  647. nautobot/project-static/nautobot-icons/briefcase-2.svg +3 -0
  648. nautobot/project-static/nautobot-icons/cable-data-2.svg +3 -0
  649. nautobot/project-static/nautobot-icons/cable-data.svg +3 -0
  650. nautobot/project-static/nautobot-icons/cast.svg +3 -0
  651. nautobot/project-static/nautobot-icons/check-circle.svg +3 -0
  652. nautobot/project-static/nautobot-icons/checkbox-circle.svg +3 -0
  653. nautobot/project-static/nautobot-icons/checkbox-rec.svg +3 -0
  654. nautobot/project-static/nautobot-icons/cloud-check.svg +3 -0
  655. nautobot/project-static/nautobot-icons/cloud-lightning.svg +3 -0
  656. nautobot/project-static/nautobot-icons/cloud-upload.svg +3 -0
  657. nautobot/project-static/nautobot-icons/cloud.svg +3 -0
  658. nautobot/project-static/nautobot-icons/compass.svg +3 -0
  659. nautobot/project-static/nautobot-icons/control-panel.svg +3 -0
  660. nautobot/project-static/nautobot-icons/credit-card.svg +3 -0
  661. nautobot/project-static/nautobot-icons/device-lifecycle.svg +3 -0
  662. nautobot/project-static/nautobot-icons/direction.svg +3 -0
  663. nautobot/project-static/nautobot-icons/elements.svg +3 -0
  664. nautobot/project-static/nautobot-icons/extensibility.svg +3 -0
  665. nautobot/project-static/nautobot-icons/globe-2.svg +3 -0
  666. nautobot/project-static/nautobot-icons/globe.svg +3 -0
  667. nautobot/project-static/nautobot-icons/hammer.svg +3 -0
  668. nautobot/project-static/nautobot-icons/history.svg +3 -0
  669. nautobot/project-static/nautobot-icons/ip.svg +3 -0
  670. nautobot/project-static/nautobot-icons/laptop.svg +3 -0
  671. nautobot/project-static/nautobot-icons/lightning.svg +3 -0
  672. nautobot/project-static/nautobot-icons/list-unordered.svg +3 -0
  673. nautobot/project-static/nautobot-icons/map-view.svg +3 -0
  674. nautobot/project-static/nautobot-icons/organization.svg +3 -0
  675. nautobot/project-static/nautobot-icons/pin-2.svg +3 -0
  676. nautobot/project-static/nautobot-icons/pin-3.svg +3 -0
  677. nautobot/project-static/nautobot-icons/plug.svg +3 -0
  678. nautobot/project-static/nautobot-icons/refresh-cw.svg +3 -0
  679. nautobot/project-static/nautobot-icons/rocket-2.svg +3 -0
  680. nautobot/project-static/nautobot-icons/rotate-cw.svg +3 -0
  681. nautobot/project-static/nautobot-icons/route.svg +3 -0
  682. nautobot/project-static/nautobot-icons/secrets.svg +3 -0
  683. nautobot/project-static/nautobot-icons/security.svg +3 -0
  684. nautobot/project-static/nautobot-icons/server-2.svg +3 -0
  685. nautobot/project-static/nautobot-icons/server.svg +3 -0
  686. nautobot/project-static/nautobot-icons/share.svg +3 -0
  687. nautobot/project-static/nautobot-icons/shield-check.svg +3 -0
  688. nautobot/project-static/nautobot-icons/sitemap-outline.svg +3 -0
  689. nautobot/project-static/nautobot-icons/sliders-vert-2.svg +3 -0
  690. nautobot/project-static/nautobot-icons/sliders-vert.svg +3 -0
  691. nautobot/project-static/nautobot-icons/star-filled.svg +3 -0
  692. nautobot/project-static/nautobot-icons/star.svg +3 -0
  693. nautobot/project-static/nautobot-icons/transform.svg +3 -0
  694. nautobot/project-static/nautobot-icons/wifi.svg +3 -0
  695. nautobot/tenancy/api/serializers.py +1 -0
  696. nautobot/tenancy/api/views.py +2 -1
  697. nautobot/tenancy/{filters/__init__.py → filters.py} +2 -10
  698. nautobot/tenancy/navigation.py +3 -1
  699. nautobot/tenancy/templates/tenancy/tenant_create.html +6 -6
  700. nautobot/tenancy/tests/test_filters.py +0 -2
  701. nautobot/tenancy/views.py +2 -1
  702. nautobot/ui/.gitignore +137 -0
  703. nautobot/ui/.node-version +1 -0
  704. nautobot/ui/.prettierignore +3 -0
  705. nautobot/ui/eslint.config.js +33 -0
  706. nautobot/ui/package-lock.json +6594 -0
  707. nautobot/ui/package.json +67 -0
  708. nautobot/ui/prettier.config.js +9 -0
  709. nautobot/ui/src/js/collapse.js +69 -0
  710. nautobot/ui/src/js/cookie.js +31 -0
  711. nautobot/ui/src/js/draggable.js +101 -0
  712. nautobot/ui/src/js/drawer.js +106 -0
  713. nautobot/ui/src/js/form.js +23 -0
  714. nautobot/ui/src/js/history.js +51 -0
  715. nautobot/ui/src/js/nautobot-graphiql.js +19 -0
  716. nautobot/ui/src/js/nautobot.js +128 -0
  717. nautobot/ui/src/js/search.js +274 -0
  718. nautobot/ui/src/js/select2.js +318 -0
  719. nautobot/ui/src/js/sidenav.js +87 -0
  720. nautobot/ui/src/js/tabs.js +139 -0
  721. nautobot/ui/src/js/theme.js +104 -0
  722. nautobot/ui/src/js/utils.js +54 -0
  723. nautobot/ui/src/scss/colors.scss +58 -0
  724. nautobot/ui/src/scss/nautobot.scss +2471 -0
  725. nautobot/ui/webpack.config.js +148 -0
  726. nautobot/users/apps.py +3 -0
  727. nautobot/users/filters.py +7 -11
  728. nautobot/users/forms.py +10 -0
  729. nautobot/users/models.py +8 -0
  730. nautobot/users/templates/users/advanced_settings_edit.html +31 -21
  731. nautobot/users/templates/users/api_tokens.html +61 -51
  732. nautobot/users/templates/users/base.html +23 -31
  733. nautobot/users/templates/users/change_password.html +29 -19
  734. nautobot/users/templates/users/preferences.html +55 -45
  735. nautobot/users/templates/users/profile.html +45 -14
  736. nautobot/users/tests/test_api.py +4 -0
  737. nautobot/users/urls.py +2 -0
  738. nautobot/users/views.py +70 -2
  739. nautobot/virtualization/api/views.py +1 -1
  740. nautobot/virtualization/filters.py +18 -32
  741. nautobot/virtualization/forms.py +22 -59
  742. nautobot/virtualization/models.py +1 -19
  743. nautobot/virtualization/navigation.py +3 -1
  744. nautobot/virtualization/tables.py +10 -6
  745. nautobot/virtualization/templates/virtualization/cluster.html +13 -13
  746. nautobot/virtualization/templates/virtualization/cluster_edit.html +6 -6
  747. nautobot/virtualization/templates/virtualization/inc/virtualmachine_vminterface_filter.html +1 -1
  748. nautobot/virtualization/templates/virtualization/virtualmachine.html +1 -1
  749. nautobot/virtualization/templates/virtualization/virtualmachine_component_add.html +24 -16
  750. nautobot/virtualization/templates/virtualization/virtualmachine_edit.html +1 -1
  751. nautobot/virtualization/templates/virtualization/virtualmachine_list.html +4 -4
  752. nautobot/virtualization/templates/virtualization/virtualmachine_update.html +27 -25
  753. nautobot/virtualization/templates/virtualization/vminterface.html +5 -5
  754. nautobot/virtualization/templates/virtualization/vminterface_edit.html +27 -11
  755. nautobot/virtualization/tests/test_api.py +3 -0
  756. nautobot/virtualization/tests/test_models.py +20 -5
  757. nautobot/virtualization/tests/test_views.py +3 -5
  758. nautobot/virtualization/urls.py +0 -11
  759. nautobot/virtualization/views.py +5 -122
  760. nautobot/vpn/__init__.py +0 -0
  761. nautobot/vpn/api/serializers.py +113 -0
  762. nautobot/vpn/api/urls.py +19 -0
  763. nautobot/vpn/api/views.py +70 -0
  764. nautobot/vpn/apps.py +8 -0
  765. nautobot/vpn/choices.py +171 -0
  766. nautobot/vpn/factory.py +209 -0
  767. nautobot/vpn/filters.py +233 -0
  768. nautobot/vpn/forms.py +486 -0
  769. nautobot/vpn/homepage.py +19 -0
  770. nautobot/vpn/migrations/0001_initial.py +541 -0
  771. nautobot/vpn/migrations/0002_populate_defaults.py +199 -0
  772. nautobot/vpn/migrations/__init__.py +0 -0
  773. nautobot/vpn/models.py +527 -0
  774. nautobot/vpn/navigation.py +98 -0
  775. nautobot/vpn/tables.py +380 -0
  776. nautobot/vpn/templates/vpn/vpnprofile.html +2 -0
  777. nautobot/vpn/templates/vpn/vpnprofile_create.html +150 -0
  778. nautobot/vpn/tests/__init__.py +0 -0
  779. nautobot/vpn/tests/test_api.py +341 -0
  780. nautobot/vpn/tests/test_filters.py +139 -0
  781. nautobot/vpn/tests/test_forms.py +294 -0
  782. nautobot/vpn/tests/test_models.py +97 -0
  783. nautobot/vpn/tests/test_views.py +281 -0
  784. nautobot/vpn/urls.py +16 -0
  785. nautobot/vpn/views.py +437 -0
  786. nautobot/wireless/filters.py +0 -8
  787. nautobot/wireless/navigation.py +3 -1
  788. nautobot/wireless/templates/wireless/wirelessnetwork_create.html +6 -6
  789. nautobot/wireless/tests/integration/test_radio_profile.py +3 -7
  790. nautobot/wireless/tests/test_api.py +1 -1
  791. {nautobot-2.4.21.dist-info → nautobot-3.0.0a3.dist-info}/METADATA +5 -4
  792. {nautobot-2.4.21.dist-info → nautobot-3.0.0a3.dist-info}/RECORD +802 -707
  793. {nautobot-2.4.21.dist-info → nautobot-3.0.0a3.dist-info}/entry_points.txt +1 -0
  794. nautobot/core/management/commands/check_job_approval_status.py +0 -47
  795. nautobot/core/templates/search_form.html +0 -9
  796. nautobot/core/templates/utilities/templatetags/filter_form_modal.html +0 -87
  797. nautobot/dcim/filters/mixins.py +0 -354
  798. nautobot/extras/templates/extras/job_approval_request.html +0 -134
  799. nautobot/extras/templates/extras/scheduled_jobs_approval_queue_list.html +0 -28
  800. nautobot/ipam/mixins.py +0 -32
  801. nautobot/ipam/templates/ipam/inc/prefix_header_extra_content_table.html +0 -4
  802. nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap-theme.css +0 -587
  803. nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap-theme.css.map +0 -1
  804. nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap-theme.min.css +0 -6
  805. nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap-theme.min.css.map +0 -1
  806. nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.css +0 -6865
  807. nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.css.map +0 -1
  808. nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.min.css +0 -6
  809. nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.min.css.map +0 -1
  810. nautobot/project-static/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.eot +0 -0
  811. nautobot/project-static/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.svg +0 -288
  812. nautobot/project-static/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.ttf +0 -0
  813. nautobot/project-static/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.woff +0 -0
  814. nautobot/project-static/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.woff2 +0 -0
  815. nautobot/project-static/bootstrap-3.4.1-dist/js/bootstrap.js +0 -2580
  816. nautobot/project-static/bootstrap-3.4.1-dist/js/bootstrap.min.js +0 -6
  817. nautobot/project-static/bootstrap-3.4.1-dist/js/npm.js +0 -13
  818. nautobot/project-static/clipboard.js-2.0.9/clipboard.min.js +0 -7
  819. nautobot/project-static/css/base.css +0 -1040
  820. nautobot/project-static/css/dark.css +0 -282
  821. nautobot/project-static/flatpickr-4.6.9/flatpickr.min.js +0 -2
  822. nautobot/project-static/flatpickr-4.6.9/themes/light.min.css +0 -1
  823. nautobot/project-static/graphiql-1.5.16/graphiql.min.css +0 -12
  824. nautobot/project-static/graphiql-1.5.16/graphiql.min.js +0 -11
  825. nautobot/project-static/highlight.js-11.9.0/github-dark.min.css +0 -10
  826. nautobot/project-static/highlight.js-11.9.0/github.min.css +0 -10
  827. nautobot/project-static/highlight.js-11.9.0/highlight.min.js +0 -378
  828. nautobot/project-static/jquery/jquery-3.7.1.min.js +0 -2
  829. nautobot/project-static/jquery-ui-1.13.2/images/ui-icons_444444_256x240.png +0 -0
  830. nautobot/project-static/jquery-ui-1.13.2/images/ui-icons_555555_256x240.png +0 -0
  831. nautobot/project-static/jquery-ui-1.13.2/images/ui-icons_777620_256x240.png +0 -0
  832. nautobot/project-static/jquery-ui-1.13.2/images/ui-icons_777777_256x240.png +0 -0
  833. nautobot/project-static/jquery-ui-1.13.2/images/ui-icons_cc0000_256x240.png +0 -0
  834. nautobot/project-static/jquery-ui-1.13.2/images/ui-icons_ffffff_256x240.png +0 -0
  835. nautobot/project-static/jquery-ui-1.13.2/jquery-ui.min.css +0 -7
  836. nautobot/project-static/jquery-ui-1.13.2/jquery-ui.min.js +0 -6
  837. nautobot/project-static/jquery-ui-1.13.2/jquery-ui.structure.min.css +0 -5
  838. nautobot/project-static/jquery-ui-1.13.2/jquery-ui.theme.min.css +0 -5
  839. nautobot/project-static/js/homepage_layout.js +0 -182
  840. nautobot/project-static/js/nav_menu.js +0 -250
  841. nautobot/project-static/js/theme.js +0 -133
  842. nautobot/project-static/materialdesignicons-7.4.47/LICENSE +0 -20
  843. nautobot/project-static/materialdesignicons-7.4.47/css/materialdesignicons.min.css +0 -3
  844. nautobot/project-static/react-16.14.0/react.production.min.js +0 -32
  845. nautobot/project-static/react-dom-16.14.0/react-dom.production.min.js +0 -239
  846. nautobot/project-static/select2-4.0.13/i18n/af.js +0 -3
  847. nautobot/project-static/select2-4.0.13/i18n/ar.js +0 -3
  848. nautobot/project-static/select2-4.0.13/i18n/az.js +0 -3
  849. nautobot/project-static/select2-4.0.13/i18n/bg.js +0 -3
  850. nautobot/project-static/select2-4.0.13/i18n/bn.js +0 -3
  851. nautobot/project-static/select2-4.0.13/i18n/bs.js +0 -3
  852. nautobot/project-static/select2-4.0.13/i18n/ca.js +0 -3
  853. nautobot/project-static/select2-4.0.13/i18n/cs.js +0 -3
  854. nautobot/project-static/select2-4.0.13/i18n/da.js +0 -3
  855. nautobot/project-static/select2-4.0.13/i18n/de.js +0 -3
  856. nautobot/project-static/select2-4.0.13/i18n/dsb.js +0 -3
  857. nautobot/project-static/select2-4.0.13/i18n/el.js +0 -3
  858. nautobot/project-static/select2-4.0.13/i18n/en.js +0 -3
  859. nautobot/project-static/select2-4.0.13/i18n/es.js +0 -3
  860. nautobot/project-static/select2-4.0.13/i18n/et.js +0 -3
  861. nautobot/project-static/select2-4.0.13/i18n/eu.js +0 -3
  862. nautobot/project-static/select2-4.0.13/i18n/fa.js +0 -3
  863. nautobot/project-static/select2-4.0.13/i18n/fi.js +0 -3
  864. nautobot/project-static/select2-4.0.13/i18n/fr.js +0 -3
  865. nautobot/project-static/select2-4.0.13/i18n/gl.js +0 -3
  866. nautobot/project-static/select2-4.0.13/i18n/he.js +0 -3
  867. nautobot/project-static/select2-4.0.13/i18n/hi.js +0 -3
  868. nautobot/project-static/select2-4.0.13/i18n/hr.js +0 -3
  869. nautobot/project-static/select2-4.0.13/i18n/hsb.js +0 -3
  870. nautobot/project-static/select2-4.0.13/i18n/hu.js +0 -3
  871. nautobot/project-static/select2-4.0.13/i18n/hy.js +0 -3
  872. nautobot/project-static/select2-4.0.13/i18n/id.js +0 -3
  873. nautobot/project-static/select2-4.0.13/i18n/is.js +0 -3
  874. nautobot/project-static/select2-4.0.13/i18n/it.js +0 -3
  875. nautobot/project-static/select2-4.0.13/i18n/ja.js +0 -3
  876. nautobot/project-static/select2-4.0.13/i18n/ka.js +0 -3
  877. nautobot/project-static/select2-4.0.13/i18n/km.js +0 -3
  878. nautobot/project-static/select2-4.0.13/i18n/ko.js +0 -3
  879. nautobot/project-static/select2-4.0.13/i18n/lt.js +0 -3
  880. nautobot/project-static/select2-4.0.13/i18n/lv.js +0 -3
  881. nautobot/project-static/select2-4.0.13/i18n/mk.js +0 -3
  882. nautobot/project-static/select2-4.0.13/i18n/ms.js +0 -3
  883. nautobot/project-static/select2-4.0.13/i18n/nb.js +0 -3
  884. nautobot/project-static/select2-4.0.13/i18n/ne.js +0 -3
  885. nautobot/project-static/select2-4.0.13/i18n/nl.js +0 -3
  886. nautobot/project-static/select2-4.0.13/i18n/pl.js +0 -3
  887. nautobot/project-static/select2-4.0.13/i18n/ps.js +0 -3
  888. nautobot/project-static/select2-4.0.13/i18n/pt-BR.js +0 -3
  889. nautobot/project-static/select2-4.0.13/i18n/pt.js +0 -3
  890. nautobot/project-static/select2-4.0.13/i18n/ro.js +0 -3
  891. nautobot/project-static/select2-4.0.13/i18n/ru.js +0 -3
  892. nautobot/project-static/select2-4.0.13/i18n/sk.js +0 -3
  893. nautobot/project-static/select2-4.0.13/i18n/sl.js +0 -3
  894. nautobot/project-static/select2-4.0.13/i18n/sq.js +0 -3
  895. nautobot/project-static/select2-4.0.13/i18n/sr-Cyrl.js +0 -3
  896. nautobot/project-static/select2-4.0.13/i18n/sr.js +0 -3
  897. nautobot/project-static/select2-4.0.13/i18n/sv.js +0 -3
  898. nautobot/project-static/select2-4.0.13/i18n/th.js +0 -3
  899. nautobot/project-static/select2-4.0.13/i18n/tk.js +0 -3
  900. nautobot/project-static/select2-4.0.13/i18n/tr.js +0 -3
  901. nautobot/project-static/select2-4.0.13/i18n/uk.js +0 -3
  902. nautobot/project-static/select2-4.0.13/i18n/vi.js +0 -3
  903. nautobot/project-static/select2-4.0.13/i18n/zh-CN.js +0 -3
  904. nautobot/project-static/select2-4.0.13/i18n/zh-TW.js +0 -3
  905. nautobot/project-static/select2-4.0.13/select2.min.css +0 -1
  906. nautobot/project-static/select2-4.0.13/select2.min.js +0 -2
  907. nautobot/project-static/select2-bootstrap-0.1.0-beta.10/select2-bootstrap.min.css +0 -7
  908. nautobot/project-static/subscriptions-transport-ws-0.9.18/client.min.js +0 -8
  909. nautobot/project-static/whatwg-fetch-3.6.2/fetch.umd.min.js +0 -8
  910. nautobot/virtualization/templates/virtualization/cluster_add_devices.html +0 -37
  911. /nautobot/extras/{filters/customfields.py → filter_mixins_customfields.py} +0 -0
  912. /nautobot/project-static/{materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.ttf → dist/1fcc36272ea3e53d0031.ttf} +0 -0
  913. /nautobot/project-static/{materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.eot → dist/2146c3c82b553977abc7.eot} +0 -0
  914. /nautobot/project-static/{materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.woff → dist/e55a20c80650829ec5fd.woff} +0 -0
  915. /nautobot/project-static/{materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.woff2 → dist/ec024da790d2972da002.woff2} +0 -0
  916. /nautobot/tenancy/{filters/mixins.py → filter_mixins.py} +0 -0
  917. {nautobot-2.4.21.dist-info → nautobot-3.0.0a3.dist-info}/LICENSE.txt +0 -0
  918. {nautobot-2.4.21.dist-info → nautobot-3.0.0a3.dist-info}/NOTICE +0 -0
  919. {nautobot-2.4.21.dist-info → nautobot-3.0.0a3.dist-info}/WHEEL +0 -0
@@ -63,26 +63,22 @@ class GraphQLTestCase(TestCase):
63
63
  def test_execute_query(self):
64
64
  with self.subTest("device dynamic groups and interfaces query"):
65
65
  query = "query {devices {name interfaces {name mac_address} dynamic_groups {name}}}"
66
- resp = execute_query(query, user=self.user).to_dict()
67
- self.assertFalse(resp["data"].get("error"))
66
+ resp = execute_query(query, user=self.user)
67
+ self.assertIsNone(resp.errors)
68
68
  self.assertEqual(
69
- resp["data"]["devices"][0]["interfaces"][0]["mac_address"], str(self.interfaces[0].mac_address)
69
+ resp.data["devices"][0]["interfaces"][0]["mac_address"], str(self.interfaces[0].mac_address)
70
70
  )
71
- self.assertIsNone(resp["data"]["devices"][0]["interfaces"][1]["mac_address"])
72
- self.assertEqual(resp["data"]["devices"][0]["dynamic_groups"][0]["name"], self.dynamic_group.name)
71
+ self.assertIsNone(resp.data["devices"][0]["interfaces"][1]["mac_address"])
72
+ self.assertEqual(resp.data["devices"][0]["dynamic_groups"][0]["name"], self.dynamic_group.name)
73
73
 
74
74
  with self.subTest("device platform drivers query"):
75
75
  query = "query {devices {platform {network_driver network_driver_mappings}}}"
76
- resp = execute_query(query, user=self.user).to_dict()
77
- self.assertFalse(resp["data"].get("error"))
78
- self.assertEqual(resp["data"]["devices"][0]["platform"]["network_driver"], "cisco_ios")
79
- self.assertEqual(
80
- resp["data"]["devices"][0]["platform"]["network_driver_mappings"]["ansible"], "cisco.ios.ios"
81
- )
82
- self.assertEqual(resp["data"]["devices"][0]["platform"]["network_driver_mappings"]["netmiko"], "cisco_ios")
83
- self.assertEqual(
84
- resp["data"]["devices"][0]["platform"]["network_driver_mappings"]["scrapli"], "cisco_iosxe"
85
- )
76
+ resp = execute_query(query, user=self.user)
77
+ self.assertIsNone(resp.errors)
78
+ self.assertEqual(resp.data["devices"][0]["platform"]["network_driver"], "cisco_ios")
79
+ self.assertEqual(resp.data["devices"][0]["platform"]["network_driver_mappings"]["ansible"], "cisco.ios.ios")
80
+ self.assertEqual(resp.data["devices"][0]["platform"]["network_driver_mappings"]["netmiko"], "cisco_ios")
81
+ self.assertEqual(resp.data["devices"][0]["platform"]["network_driver_mappings"]["scrapli"], "cisco_iosxe")
86
82
 
87
83
  with self.subTest("device serial number query"):
88
84
  non_empty_serial_device = Device.objects.first()
@@ -91,47 +87,47 @@ class GraphQLTestCase(TestCase):
91
87
 
92
88
  # Test device serial query default behavior: serial__ie
93
89
  query = 'query { devices (serial: " ") { name serial } }'
94
- resp = execute_query(query, user=self.user).to_dict()
95
- self.assertFalse(resp["data"].get("error"))
96
- for device in resp["data"]["devices"]:
90
+ resp = execute_query(query, user=self.user)
91
+ self.assertIsNone(resp.errors)
92
+ for device in resp.data["devices"]:
97
93
  self.assertEqual(device["serial"], "")
98
94
 
99
95
  # Test device serial default filter with non-empty serial number
100
96
  query = 'query { devices (serial:"' + non_empty_serial_device.serial.lower() + '") { name serial } }'
101
- resp = execute_query(query, user=self.user).to_dict()
102
- self.assertFalse(resp["data"].get("error"))
103
- self.assertEqual(resp["data"]["devices"][0]["serial"], non_empty_serial_device.serial)
97
+ resp = execute_query(query, user=self.user)
98
+ self.assertIsNone(resp.errors)
99
+ self.assertEqual(resp.data["devices"][0]["serial"], non_empty_serial_device.serial)
104
100
 
105
101
  # Test device serial iexact filter with non-empty serial number
106
102
  query = 'query { devices (serial__ie:"' + non_empty_serial_device.serial.upper() + '") { name serial } }'
107
- resp = execute_query(query, user=self.user).to_dict()
108
- self.assertFalse(resp["data"].get("error"))
109
- self.assertEqual(resp["data"]["devices"][0]["serial"], non_empty_serial_device.serial)
103
+ resp = execute_query(query, user=self.user)
104
+ self.assertIsNone(resp.errors)
105
+ self.assertEqual(resp.data["devices"][0]["serial"], non_empty_serial_device.serial)
110
106
 
111
107
  # Test device serial__nie filter with non-empty serial number
112
108
  query = 'query { devices (serial__nie:"' + non_empty_serial_device.serial.lower() + '") { name serial } }'
113
- resp = execute_query(query, user=self.user).to_dict()
114
- self.assertFalse(resp["data"].get("error"))
115
- for device in resp["data"]["devices"]:
109
+ resp = execute_query(query, user=self.user)
110
+ self.assertIsNone(resp.errors)
111
+ for device in resp.data["devices"]:
116
112
  self.assertNotEqual(device["serial"], non_empty_serial_device.serial)
117
113
 
118
114
  # Test device serial__ie filter with empty serial number
119
115
  query = 'query { devices (serial__ie:" ") { name serial } }'
120
- resp = execute_query(query, user=self.user).to_dict()
121
- self.assertFalse(resp["data"].get("error"))
122
- for device in resp["data"]["devices"]:
116
+ resp = execute_query(query, user=self.user)
117
+ self.assertIsNone(resp.errors)
118
+ for device in resp.data["devices"]:
123
119
  self.assertEqual(device["serial"], "")
124
120
 
125
121
  # Test device serial__nie filter with empty serial number
126
122
  query = 'query { devices (serial__nie:" ") { name serial } }'
127
- resp = execute_query(query, user=self.user).to_dict()
128
- self.assertFalse(resp["data"].get("error"))
129
- for device in resp["data"]["devices"]:
123
+ resp = execute_query(query, user=self.user)
124
+ self.assertIsNone(resp.errors)
125
+ for device in resp.data["devices"]:
130
126
  self.assertNotEqual(device["serial"], "")
131
127
 
132
128
  # Test device serial__n filter with empty serial number
133
129
  query = 'query { devices (serial__n:" ") { name serial } }'
134
- resp = execute_query(query, user=self.user).to_dict()
135
- self.assertFalse(resp["data"].get("error"))
136
- for device in resp["data"]["devices"]:
130
+ resp = execute_query(query, user=self.user)
131
+ self.assertIsNone(resp.errors)
132
+ for device in resp.data["devices"]:
137
133
  self.assertNotEqual(device["serial"], "")
@@ -95,7 +95,7 @@ class TestSoftwareImageFileTestCase(TransactionTestCase):
95
95
  class TestSoftwareVersionTestCase(TransactionTestCase):
96
96
  def test_correct_handling_for_model_protected_error(self):
97
97
  create_common_data_for_software_related_test_cases()
98
- software_version = SoftwareVersion.objects.get(version="Test version 1.0.0")
98
+ software_version = SoftwareVersion.objects.filter(version="Test version 1.0.0").first()
99
99
 
100
100
  initial_count = SoftwareVersion.objects.all().count()
101
101
  self.add_permissions("dcim.delete_softwareversion")
@@ -2,6 +2,7 @@ from decimal import Decimal
2
2
 
3
3
  from constance.test import override_config
4
4
  from django.contrib.contenttypes.models import ContentType
5
+ from django.core.cache import caches
5
6
  from django.core.exceptions import ValidationError
6
7
  from django.db import IntegrityError
7
8
  from django.db.models import Model
@@ -16,6 +17,7 @@ from nautobot.dcim.choices import (
16
17
  CableTypeChoices,
17
18
  ConsolePortTypeChoices,
18
19
  DeviceFaceChoices,
20
+ DeviceUniquenessChoices,
19
21
  InterfaceModeChoices,
20
22
  InterfaceTypeChoices,
21
23
  PortTypeChoices,
@@ -39,6 +41,7 @@ from nautobot.dcim.models import (
39
41
  Device,
40
42
  DeviceBay,
41
43
  DeviceBayTemplate,
44
+ DeviceClusterAssignment,
42
45
  DeviceRedundancyGroup,
43
46
  DeviceType,
44
47
  DeviceTypeToSoftwareImageFile,
@@ -1422,6 +1425,10 @@ class DeviceTestCase(ModelTestCases.BaseModelTestCase):
1422
1425
  model = Device
1423
1426
 
1424
1427
  def setUp(self):
1428
+ # clear Constance cache
1429
+ cache = caches[settings.CONSTANCE_DATABASE_CACHE_BACKEND]
1430
+ cache.clear()
1431
+
1425
1432
  manufacturer = Manufacturer.objects.first()
1426
1433
  self.device_type = DeviceType.objects.create(
1427
1434
  manufacturer=manufacturer,
@@ -1510,6 +1517,12 @@ class DeviceTestCase(ModelTestCases.BaseModelTestCase):
1510
1517
  )
1511
1518
  self.device.validated_save()
1512
1519
 
1520
+ self.cluster_type = ClusterType.objects.create(name="Test Cluster Type")
1521
+ self.cluster = Cluster.objects.create(
1522
+ name="Test Cluster", cluster_type=self.cluster_type, location=self.location_3
1523
+ )
1524
+ self.device.clusters.add(self.cluster)
1525
+
1513
1526
  def test_natural_key_default(self):
1514
1527
  """Ensure that default natural-key for Device is (name, tenant, location)."""
1515
1528
  self.assertEqual([self.device.name, None, *self.device.location.natural_key()], self.device.natural_key())
@@ -1525,12 +1538,25 @@ class DeviceTestCase(ModelTestCases.BaseModelTestCase):
1525
1538
 
1526
1539
  def test_natural_key_overrides(self):
1527
1540
  """Ensure that the natural-key for Device is affected by settings/Constance."""
1528
- with override_config(DEVICE_NAME_AS_NATURAL_KEY=True):
1541
+ with override_config(DEVICE_UNIQUENESS=DeviceUniquenessChoices.NAME):
1529
1542
  self.assertEqual([self.device.name], self.device.natural_key())
1530
1543
  # self.assertEqual(construct_composite_key([self.device.name]), self.device.composite_key) # TODO: Revist this if we reintroduce composite keys
1531
1544
  self.assertEqual(self.device, Device.objects.get_by_natural_key([self.device.name]))
1532
1545
  # self.assertEqual(self.device, Device.objects.get(composite_key=self.device.composite_key)) # TODO: Revist this if we reintroduce composite keys
1533
1546
 
1547
+ with override_config(DEVICE_UNIQUENESS=DeviceUniquenessChoices.LOCATION_TENANT_NAME):
1548
+ self.assertEqual(
1549
+ [self.device.name, self.device.tenant, self.device.location.name], self.device.natural_key()
1550
+ )
1551
+ self.assertEqual(
1552
+ self.device,
1553
+ Device.objects.get_by_natural_key([self.device.name, self.device.tenant, self.device.location]),
1554
+ )
1555
+
1556
+ with override_config(DEVICE_UNIQUENESS=DeviceUniquenessChoices.NONE):
1557
+ self.assertEqual([str(self.device.pk)], self.device.natural_key())
1558
+ self.assertEqual(self.device, Device.objects.get_by_natural_key([self.device.pk]))
1559
+
1534
1560
  with override_config(LOCATION_NAME_AS_NATURAL_KEY=True):
1535
1561
  self.assertEqual([self.device.name, None, self.device.location.name], self.device.natural_key())
1536
1562
  # self.assertEqual(
@@ -1644,6 +1670,28 @@ class DeviceTestCase(ModelTestCases.BaseModelTestCase):
1644
1670
  f'Devices may not associate to locations of type "{self.location_type_2.name}"', str(cm.exception)
1645
1671
  )
1646
1672
 
1673
+ def test_device_cluster_location_mismatch(self):
1674
+ with self.subTest("Invalid cluster assignment at creation time"):
1675
+ device = Device(
1676
+ name="Device in different location from cluster",
1677
+ device_type=self.device_type,
1678
+ role=self.device_role,
1679
+ status=self.device_status,
1680
+ location=self.location_2,
1681
+ cluster=self.cluster, # belongs to self.location_3
1682
+ )
1683
+ with self.assertRaises(ValidationError) as cm:
1684
+ device.validated_save()
1685
+ self.assertIn("does not include", str(cm.exception))
1686
+
1687
+ new_cluster = Cluster.objects.create(
1688
+ name="New Cluster", cluster_type=self.cluster_type, location=self.location_2
1689
+ )
1690
+ with self.subTest("Invalid cluster assignment via Device.clusters m2m manager"):
1691
+ with self.assertRaises(ValidationError) as cm:
1692
+ self.device.clusters.add(new_cluster) # self.device belongs to self.location_3
1693
+ self.assertIn("does not include", str(cm.exception))
1694
+
1647
1695
  def test_device_redundancy_group_validation(self):
1648
1696
  d2 = Device(
1649
1697
  name="Test Device 2",
@@ -1972,6 +2020,42 @@ class DeviceTestCase(ModelTestCases.BaseModelTestCase):
1972
2020
 
1973
2021
  self.assertNotEqual(child_mtime_after_parent_rack_update_save, child_mtime_after_parent_site_update_save)
1974
2022
 
2023
+ def test_cluster_queries(self):
2024
+ with self.subTest("Assert filtering and excluding `cluster`"):
2025
+ self.assertQuerysetEqualAndNotEmpty(
2026
+ Device.objects.filter(cluster=self.cluster),
2027
+ Device.objects.filter(clusters__in=[self.cluster]),
2028
+ )
2029
+ self.assertQuerysetEqualAndNotEmpty(
2030
+ Device.objects.exclude(cluster=self.cluster),
2031
+ Device.objects.exclude(clusters__in=[self.cluster]),
2032
+ )
2033
+ self.assertQuerysetEqualAndNotEmpty(
2034
+ Device.objects.filter(cluster__in=[self.cluster]),
2035
+ Device.objects.filter(clusters__in=[self.cluster]),
2036
+ )
2037
+ self.assertQuerysetEqualAndNotEmpty(
2038
+ Device.objects.exclude(cluster__in=[self.cluster]),
2039
+ Device.objects.exclude(clusters__in=[self.cluster]),
2040
+ )
2041
+
2042
+ # We use `assertQuerysetEqualAndNotEmpty` for test validation. Including a nullable field could lead
2043
+ # to flaky tests where querysets might return None, causing tests to fail. Therefore, we select
2044
+ # fields that consistently contain values to ensure reliable filtering.
2045
+ query_params = ["name", "cluster_type", "location"]
2046
+
2047
+ for field_name in query_params:
2048
+ with self.subTest(f"Assert cluster__{field_name} query."):
2049
+ value = getattr(self.cluster, field_name)
2050
+ self.assertQuerysetEqualAndNotEmpty(
2051
+ Device.objects.filter(**{f"cluster__{field_name}": value}),
2052
+ Device.objects.filter(**{f"clusters__{field_name}": value}),
2053
+ )
2054
+ self.assertQuerysetEqualAndNotEmpty(
2055
+ Device.objects.exclude(**{f"cluster__{field_name}": value}),
2056
+ Device.objects.exclude(**{f"clusters__{field_name}": value}),
2057
+ )
2058
+
1975
2059
 
1976
2060
  class DeviceBayTestCase(ModelTestCases.BaseModelTestCase):
1977
2061
  model = DeviceBay
@@ -3956,3 +4040,147 @@ class ModuleFamilyTestCase(ModelTestCases.BaseModelTestCase):
3956
4040
  def test_modulefamily_str(self):
3957
4041
  """Test string representation of ModuleFamily."""
3958
4042
  self.assertEqual(str(self.module_family), "Test Module Family")
4043
+
4044
+
4045
+ class DeviceClusterAssignmentTestCase(ModelTestCases.BaseModelTestCase):
4046
+ model = DeviceClusterAssignment
4047
+
4048
+ @classmethod
4049
+ def setUpTestData(cls):
4050
+ # Create necessary objects for testing
4051
+ cls.device_role = Role.objects.get_for_model(Device).first()
4052
+ cls.device_status = Status.objects.get_for_model(Device).first()
4053
+ cls.location = Location.objects.filter(location_type=LocationType.objects.get(name="Campus")).first()
4054
+ cls.manufacturer = Manufacturer.objects.first()
4055
+ cls.device_type = DeviceType.objects.create(
4056
+ manufacturer=cls.manufacturer,
4057
+ model="Test Device Type 1",
4058
+ )
4059
+ cls.devices = (
4060
+ Device.objects.create(
4061
+ device_type=cls.device_type,
4062
+ role=cls.device_role,
4063
+ name="Test Device 1",
4064
+ location=cls.location,
4065
+ status=cls.device_status,
4066
+ ),
4067
+ Device.objects.create(
4068
+ device_type=cls.device_type,
4069
+ role=cls.device_role,
4070
+ name="Test Device 2",
4071
+ location=cls.location,
4072
+ status=cls.device_status,
4073
+ ),
4074
+ )
4075
+ cls.cluster_type = ClusterType.objects.create(name="Test Cluster Type")
4076
+ cls.clusters = (
4077
+ Cluster.objects.create(name="Test Cluster 1", cluster_type=cls.cluster_type),
4078
+ Cluster.objects.create(name="Test Cluster 2", cluster_type=cls.cluster_type),
4079
+ )
4080
+ cls.assignment = DeviceClusterAssignment.objects.create(
4081
+ device=cls.devices[0],
4082
+ cluster=cls.clusters[0],
4083
+ )
4084
+
4085
+ def test_create_device_cluster_assignment(self):
4086
+ """Test creating a device-cluster assignment."""
4087
+ assignment = DeviceClusterAssignment(
4088
+ device=self.devices[1],
4089
+ cluster=self.clusters[0],
4090
+ )
4091
+ assignment.validated_save()
4092
+ self.assertTrue(
4093
+ DeviceClusterAssignment.objects.filter(
4094
+ device=self.devices[1],
4095
+ cluster=self.clusters[0],
4096
+ ).exists()
4097
+ )
4098
+ self.assertEqual(self.devices[1].clusters.count(), 1)
4099
+ self.assertEqual(self.devices[1].clusters.first(), self.clusters[0])
4100
+ self.assertEqual(self.clusters[0].devices.count(), 2)
4101
+ self.assertIn(self.devices[0], self.clusters[0].devices.all())
4102
+ self.assertIn(self.devices[1], self.clusters[0].devices.all())
4103
+
4104
+ def test_uniqueness_constraint(self):
4105
+ """Test that a device can't be assigned to the same cluster twice."""
4106
+ duplicate_assignment = DeviceClusterAssignment(
4107
+ device=self.devices[0],
4108
+ cluster=self.clusters[0],
4109
+ )
4110
+ with self.assertRaises(ValidationError):
4111
+ duplicate_assignment.full_clean()
4112
+ with self.assertRaises(IntegrityError):
4113
+ duplicate_assignment.save()
4114
+
4115
+ def test_cascade_on_delete(self):
4116
+ """Test that assignments are deleted when either the device or cluster is deleted."""
4117
+ new_assignment = DeviceClusterAssignment.objects.create(
4118
+ device=self.devices[1],
4119
+ cluster=self.clusters[1],
4120
+ )
4121
+ self.assertTrue(DeviceClusterAssignment.objects.filter(id=new_assignment.id).exists())
4122
+ self.devices[1].delete()
4123
+ self.assertFalse(DeviceClusterAssignment.objects.filter(id=new_assignment.id).exists())
4124
+ another_device = Device.objects.create(
4125
+ device_type=self.device_type,
4126
+ role=self.device_role,
4127
+ name="Test Device 3",
4128
+ location=self.location,
4129
+ status=self.device_status,
4130
+ )
4131
+ another_assignment = DeviceClusterAssignment.objects.create(
4132
+ device=another_device,
4133
+ cluster=self.clusters[1],
4134
+ )
4135
+
4136
+ self.assertTrue(DeviceClusterAssignment.objects.filter(id=another_assignment.id).exists())
4137
+
4138
+ self.clusters[1].delete()
4139
+ self.assertFalse(DeviceClusterAssignment.objects.filter(id=another_assignment.id).exists())
4140
+
4141
+ def test_cluster_property_backward_compatibility(self):
4142
+ """Test the backward-compatible cluster property on Device."""
4143
+ self.assertEqual(self.devices[0].cluster, self.clusters[0])
4144
+
4145
+ device_no_clusters = Device.objects.create(
4146
+ device_type=self.device_type,
4147
+ role=self.device_role,
4148
+ name="Test Device No Clusters",
4149
+ location=self.location,
4150
+ status=self.device_status,
4151
+ )
4152
+ self.assertIsNone(device_no_clusters.cluster)
4153
+
4154
+ device_multi_clusters = Device.objects.create(
4155
+ device_type=self.device_type,
4156
+ role=self.device_role,
4157
+ name="Test Device Multi Clusters",
4158
+ location=self.location,
4159
+ status=self.device_status,
4160
+ )
4161
+ DeviceClusterAssignment.objects.create(device=device_multi_clusters, cluster=self.clusters[0])
4162
+ DeviceClusterAssignment.objects.create(device=device_multi_clusters, cluster=self.clusters[1])
4163
+
4164
+ with self.assertRaises(Cluster.MultipleObjectsReturned):
4165
+ device_multi_clusters.cluster
4166
+ self.assertEqual(device_multi_clusters.clusters.count(), 2)
4167
+ self.assertIn(self.clusters[0], device_multi_clusters.clusters.all())
4168
+ self.assertIn(self.clusters[1], device_multi_clusters.clusters.all())
4169
+
4170
+ def test_cluster_setter_backward_compatibility(self):
4171
+ """Test the backward-compatible cluster setter on Device."""
4172
+ device = Device.objects.create(
4173
+ device_type=self.device_type,
4174
+ role=self.device_role,
4175
+ name="Test Device For Setter",
4176
+ location=self.location,
4177
+ status=self.device_status,
4178
+ )
4179
+ device.cluster = self.clusters[0]
4180
+ self.assertEqual(device.clusters.count(), 1)
4181
+ self.assertEqual(device.clusters.first(), self.clusters[0])
4182
+ device.cluster = self.clusters[1]
4183
+ self.assertEqual(device.clusters.count(), 1)
4184
+ self.assertEqual(device.clusters.first(), self.clusters[1])
4185
+ device.cluster = None
4186
+ self.assertEqual(device.clusters.count(), 0)
@@ -6,7 +6,7 @@ import zoneinfo
6
6
  from django.contrib.auth import get_user_model
7
7
  from django.contrib.contenttypes.models import ContentType
8
8
  from django.db.models import Q
9
- from django.test import override_settings
9
+ from django.test import override_settings, tag
10
10
  from django.urls import reverse
11
11
  from django.utils.html import strip_spaces_between_tags
12
12
  from netaddr import EUI
@@ -762,7 +762,7 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
762
762
  # Validate Power Utilization for PowerFeed 11 is displaying correctly on Rack View.
763
763
  power_feed_11_html = """
764
764
  <td><div title="Used: 1263&#13;Count: 3840" class="progress text-center">
765
- <div class="progress-bar progress-bar-success"
765
+ <div class="progress-bar bg-success"
766
766
  role="progressbar" aria-valuenow="32" aria-valuemin="0" aria-valuemax="100" style="width: 32%">
767
767
  32%
768
768
  </div>
@@ -772,7 +772,7 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
772
772
  # Validate Power Utilization for PowerFeed12 is displaying correctly on Rack View.
773
773
  power_feed_12_html = """
774
774
  <td><div title="Used: 2526&#13;Count: 3840" class="progress text-center">
775
- <div class="progress-bar progress-bar-success"
775
+ <div class="progress-bar bg-success"
776
776
  role="progressbar" aria-valuenow="65" aria-valuemin="0" aria-valuemax="100" style="width: 65%">
777
777
  65%
778
778
  </div>
@@ -782,7 +782,7 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
782
782
  # Validate Rack Power Utilization for Combined powerfeeds is displaying correctly on the Rack View
783
783
  total_utilization_html = """
784
784
  <td><div title="Used: 3789&#13;Count: 7680" class="progress text-center">
785
- <div class="progress-bar progress-bar-success"
785
+ <div class="progress-bar bg-success"
786
786
  role="progressbar" aria-valuenow="49" aria-valuemin="0" aria-valuemax="100" style="width: 49%">
787
787
  49%
788
788
  </div>
@@ -889,11 +889,11 @@ class DeviceTypeTestCase(
889
889
  csv_import_url = job_import_url(ContentType.objects.get_for_model(DeviceType))
890
890
  # Dropdown provides both YAML/JSON and CSV import as options
891
891
  self.assertInHTML(
892
- f'<a href="{yaml_import_url}"><span class="mdi mdi-database-import text-muted" aria-hidden="true"></span> Import from JSON/YAML (single record)</a>',
892
+ f'<a class="dropdown-item" href="{yaml_import_url}"><span class="mdi mdi-database-import text-secondary" aria-hidden="true"></span> Import from JSON/YAML (single record)</a>',
893
893
  content,
894
894
  )
895
895
  self.assertInHTML(
896
- f'<a href="{csv_import_url}"><span class="mdi mdi-database-import text-muted" aria-hidden="true"></span> Import from CSV (multiple records)</a>',
896
+ f'<a class="dropdown-item" href="{csv_import_url}"><span class="mdi mdi-database-import text-secondary" aria-hidden="true"></span> Import from CSV (multiple records)</a>',
897
897
  content,
898
898
  )
899
899
 
@@ -906,11 +906,11 @@ class DeviceTypeTestCase(
906
906
  )
907
907
  self.assertInHTML('<input type="hidden" name="export_format" value="yaml">', content)
908
908
  self.assertInHTML(
909
- '<button type="submit"><span class="mdi mdi-database-export text-muted" aria-hidden="true"></span> Export as YAML</button>',
909
+ '<button class="dropdown-item" type="submit"><span class="mdi mdi-database-export text-secondary" aria-hidden="true"></span> Export as YAML</button>',
910
910
  content,
911
911
  )
912
912
  self.assertInHTML(
913
- '<button type="submit"><span class="mdi mdi-database-export text-muted" aria-hidden="true"></span> Export as CSV</button>',
913
+ '<button class="dropdown-item" type="submit"><span class="mdi mdi-database-export text-secondary" aria-hidden="true"></span> Export as CSV</button>',
914
914
  content,
915
915
  )
916
916
 
@@ -1239,7 +1239,7 @@ module-bays:
1239
1239
  response_content = response.content.decode(response.charset)
1240
1240
  self.assertHttpStatus(response, 200)
1241
1241
  self.assertInHTML(
1242
- '<strong class="panel-title">U height</strong>: <ul class="errorlist"><li>Ensure this value is greater than or equal to 0.</li></ul>',
1242
+ '<strong>U height</strong>: <ul class="errorlist"><li>Ensure this value is greater than or equal to 0.</li></ul>',
1243
1243
  response_content,
1244
1244
  )
1245
1245
 
@@ -1293,6 +1293,7 @@ class ModuleTypeTestCase(
1293
1293
  "comments": "changed comment",
1294
1294
  }
1295
1295
 
1296
+ @tag("fix_in_v3")
1296
1297
  def test_list_has_correct_links(self):
1297
1298
  """Assert that the ModuleType list view has import/export buttons for both CSV and YAML/JSON formats."""
1298
1299
  self.add_permissions("dcim.add_moduletype", "dcim.view_moduletype")
@@ -1304,11 +1305,11 @@ class ModuleTypeTestCase(
1304
1305
  csv_import_url = job_import_url(ContentType.objects.get_for_model(ModuleType))
1305
1306
  # Dropdown provides both YAML/JSON and CSV import as options
1306
1307
  self.assertInHTML(
1307
- f'<a href="{yaml_import_url}"><span class="mdi mdi-database-import text-muted" aria-hidden="true"></span> Import from JSON/YAML (single record)</a>',
1308
+ f'<a href="{yaml_import_url}"><span class="mdi mdi-database-import text-secondary" aria-hidden="true"></span> Import from JSON/YAML (single record)</a>',
1308
1309
  content,
1309
1310
  )
1310
1311
  self.assertInHTML(
1311
- f'<a href="{csv_import_url}"><span class="mdi mdi-database-import text-muted" aria-hidden="true"></span> Import from CSV (multiple records)</a>',
1312
+ f'<a href="{csv_import_url}"><span class="mdi mdi-database-import text-secondary" aria-hidden="true"></span> Import from CSV (multiple records)</a>',
1312
1313
  content,
1313
1314
  )
1314
1315
 
@@ -1321,11 +1322,11 @@ class ModuleTypeTestCase(
1321
1322
  )
1322
1323
  self.assertInHTML('<input type="hidden" name="export_format" value="yaml">', content)
1323
1324
  self.assertInHTML(
1324
- '<button type="submit"><span class="mdi mdi-database-export text-muted" aria-hidden="true"></span> Export as YAML</button>',
1325
+ '<button type="submit"><span class="mdi mdi-database-export text-secondary" aria-hidden="true"></span> Export as YAML</button>',
1325
1326
  content,
1326
1327
  )
1327
1328
  self.assertInHTML(
1328
- '<button type="submit"><span class="mdi mdi-database-export text-muted" aria-hidden="true"></span> Export as CSV</button>',
1329
+ '<button type="submit"><span class="mdi mdi-database-export text-secondary" aria-hidden="true"></span> Export as CSV</button>',
1329
1330
  content,
1330
1331
  )
1331
1332
 
@@ -2310,7 +2311,7 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
2310
2311
  "status": statuses[1].pk,
2311
2312
  "primary_ip4": None,
2312
2313
  "primary_ip6": None,
2313
- "cluster": None,
2314
+ "clusters": [cluster.pk],
2314
2315
  "secrets_group": secrets_groups[1].pk,
2315
2316
  "virtual_chassis": None,
2316
2317
  "vc_position": None,
@@ -2333,7 +2334,7 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
2333
2334
  "status": statuses[2].pk,
2334
2335
  "location": locations[1].pk,
2335
2336
  "rack": racks[1].pk,
2336
- "cluster": cluster.pk,
2337
+ "add_clusters": [cluster.pk],
2337
2338
  "comments": "An older device",
2338
2339
  "position": None,
2339
2340
  "face": DeviceFaceChoices.FACE_FRONT,
@@ -2354,7 +2355,7 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
2354
2355
  add_vdc_url = reverse("dcim:virtualdevicecontext_add")
2355
2356
  return_url = device.get_absolute_url()
2356
2357
  expected_add_vdc_button_html = f"""
2357
- <a href="{add_vdc_url}?device={device.id}&amp;return_url={return_url}" class="btn btn-primary btn-xs">
2358
+ <a href="{add_vdc_url}?device={device.id}&amp;return_url={return_url}" class="btn btn-primary btn-sm">
2358
2359
  <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add virtual device context
2359
2360
  </a>
2360
2361
  """
@@ -2512,7 +2513,7 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
2512
2513
  self.assertEqual(self.interfaces[0].ip_addresses.all().count(), 0)
2513
2514
 
2514
2515
  with self.subTest("Assert Cannot assign IPAddress(Existing IP) without permission"):
2515
- # Assert Assign Exsisting IPAddress
2516
+ # Assert Assign Existing IPAddress
2516
2517
  response = self.client.post(**assign_ip_request, follow=True)
2517
2518
  self.assertBodyContains(response, f"Interface with id &quot;{self.interfaces[1].pk}&quot; not found")
2518
2519
  self.interfaces[1].refresh_from_db()
@@ -2626,6 +2627,15 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
2626
2627
  response = self.client.get(url)
2627
2628
  self.assertHttpStatus(response, 200)
2628
2629
 
2630
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2631
+ def test_device_vpnendpoints(self):
2632
+ self.add_permissions("vpn.view_vpntunnelendpoint")
2633
+ device = Device.objects.first()
2634
+
2635
+ url = reverse("dcim:device_vpnendpoints", kwargs={"pk": device.pk})
2636
+ response = self.client.get(url)
2637
+ self.assertHttpStatus(response, 200)
2638
+
2629
2639
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2630
2640
  def test_device_napalm_tabs(self):
2631
2641
  self.add_permissions("dcim.napalm_read_device")
@@ -2898,7 +2908,7 @@ class ModuleTestCase(ViewTestCases.PrimaryObjectViewTestCase):
2898
2908
  self.assertEqual(self.interfaces[0].ip_addresses.all().count(), 0)
2899
2909
 
2900
2910
  with self.subTest("Assert Cannot assign IPAddress(Existing IP) without permission"):
2901
- # Assert Assign Exsisting IPAddress
2911
+ # Assert Assign Existing IPAddress
2902
2912
  response = self.client.post(**assign_ip_request, follow=True)
2903
2913
  self.assertBodyContains(response, f"Interface with id &quot;{self.interfaces[1].pk}&quot; not found")
2904
2914
  self.interfaces[1].refresh_from_db()
@@ -4331,7 +4341,7 @@ class VirtualChassisTestCase(ViewTestCases.PrimaryObjectViewTestCase):
4331
4341
  Interface.objects.create(device=self.devices[2], name="device 2 interface 1", status=interface_status)
4332
4342
  Interface.objects.create(device=self.devices[2], name="device 2 interface 2", status=interface_status)
4333
4343
  response = self.client.get(reverse("dcim:device_interfaces", kwargs={"pk": self.devices[0].pk}))
4334
- self.assertBodyContains(response, 'Interfaces <span class="badge">6</span>')
4344
+ self.assertBodyContains(response, 'Interfaces <span class="badge bg-primary">6</span>')
4335
4345
  self.assertBodyContains(response, "device 1 interface 1")
4336
4346
  self.assertBodyContains(response, "device 1 interface 2")
4337
4347
  self.assertBodyContains(response, "device 2 interface 1")
@@ -4582,7 +4592,8 @@ class PathTraceViewTestCase(ModelViewTestCase):
4582
4592
  url = reverse("dcim:rearport_trace", args=[obj.pk])
4583
4593
  cablepath_id = CablePath.objects.first().id
4584
4594
  response = self.client.get(url + f"?cablepath_id={cablepath_id}")
4585
- self.assertBodyContains(response, "<h1>Cable Trace for Rear Port Rear Port 1</h1>", html=True)
4595
+ self.assertBodyContains(response, "Rear Port 1")
4596
+ self.assertBodyContains(response, "eth0")
4586
4597
 
4587
4598
 
4588
4599
  class DeviceRedundancyGroupTestCase(ViewTestCases.PrimaryObjectViewTestCase):
nautobot/dcim/utils.py CHANGED
@@ -49,9 +49,9 @@ def cable_status_color_css(record):
49
49
  return ""
50
50
  else:
51
51
  CABLE_STATUS_TO_CSS_CLASS = {
52
- ColorChoices.COLOR_GREEN: "success",
53
- ColorChoices.COLOR_AMBER: "warning",
54
- ColorChoices.COLOR_CYAN: "info",
52
+ ColorChoices.COLOR_GREEN: "table-success",
53
+ ColorChoices.COLOR_AMBER: "table-warning",
54
+ ColorChoices.COLOR_CYAN: "table-info",
55
55
  }
56
56
  status_color = record.cable.get_status_color().strip("#")
57
57
  return CABLE_STATUS_TO_CSS_CLASS.get(status_color, "")