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
nautobot/extras/views.py CHANGED
@@ -15,7 +15,6 @@ from django.template.defaultfilters import urlencode
15
15
  from django.template.loader import get_template, TemplateDoesNotExist
16
16
  from django.urls import reverse
17
17
  from django.urls.exceptions import NoReverseMatch
18
- from django.utils import timezone
19
18
  from django.utils.encoding import iri_to_uri
20
19
  from django.utils.html import format_html, format_html_join
21
20
  from django.utils.http import url_has_allowed_host_and_scheme
@@ -29,11 +28,10 @@ from rest_framework.response import Response
29
28
 
30
29
  from nautobot.core.choices import ButtonActionColorChoices
31
30
  from nautobot.core.constants import PAGINATE_COUNT_DEFAULT
32
- from nautobot.core.events import publish_event
33
31
  from nautobot.core.exceptions import FilterSetFieldNotFound
34
- from nautobot.core.forms import restrict_form_fields
32
+ from nautobot.core.forms import ApprovalForm, restrict_form_fields
35
33
  from nautobot.core.models.querysets import count_related
36
- from nautobot.core.models.utils import pretty_print_query, serialize_object_v2
34
+ from nautobot.core.models.utils import pretty_print_query
37
35
  from nautobot.core.tables import ButtonsColumn
38
36
  from nautobot.core.templatetags import helpers
39
37
  from nautobot.core.ui import object_detail
@@ -70,7 +68,7 @@ from nautobot.core.views.mixins import (
70
68
  ObjectPermissionRequiredMixin,
71
69
  )
72
70
  from nautobot.core.views.paginator import EnhancedPaginator, get_paginate_count
73
- from nautobot.core.views.utils import get_obj_from_context, prepare_cloned_fields
71
+ from nautobot.core.views.utils import common_detail_view_context, get_obj_from_context, prepare_cloned_fields
74
72
  from nautobot.core.views.viewsets import NautobotUIViewSet
75
73
  from nautobot.dcim.models import Controller, Device, Interface, Module, Rack, VirtualDeviceContext
76
74
  from nautobot.dcim.tables import (
@@ -82,15 +80,24 @@ from nautobot.dcim.tables import (
82
80
  VirtualDeviceContextTable,
83
81
  )
84
82
  from nautobot.extras.context_managers import deferred_change_logging_for_bulk_operation
85
- from nautobot.extras.utils import fixup_filterset_query_params, get_base_template, get_worker_count
83
+ from nautobot.extras.templatetags.approvals import render_approval_workflow_state
84
+ from nautobot.extras.utils import (
85
+ fixup_filterset_query_params,
86
+ get_base_template,
87
+ get_pending_approval_workflow_stages,
88
+ get_worker_count,
89
+ )
86
90
  from nautobot.ipam.models import IPAddress, Prefix, VLAN
87
91
  from nautobot.ipam.tables import IPAddressTable, PrefixTable, VLANTable
88
92
  from nautobot.virtualization.models import VirtualMachine, VMInterface
89
93
  from nautobot.virtualization.tables import VirtualMachineTable, VMInterfaceTable
94
+ from nautobot.vpn.models import VPN, VPNProfile, VPNTunnel, VPNTunnelEndpoint
95
+ from nautobot.vpn.tables import VPNProfileTable, VPNTable, VPNTunnelEndpointTable, VPNTunnelTable
90
96
 
91
97
  from . import filters, forms, jobs_ui, tables
92
98
  from .api import serializers
93
99
  from .choices import (
100
+ ApprovalWorkflowStateChoices,
94
101
  DynamicGroupTypeChoices,
95
102
  JobExecutionType,
96
103
  JobQueueTypeChoices,
@@ -103,6 +110,11 @@ from .datasources import (
103
110
  )
104
111
  from .jobs import get_job
105
112
  from .models import (
113
+ ApprovalWorkflow,
114
+ ApprovalWorkflowDefinition,
115
+ ApprovalWorkflowStage,
116
+ ApprovalWorkflowStageDefinition,
117
+ ApprovalWorkflowStageResponse,
106
118
  ComputedField,
107
119
  ConfigContext,
108
120
  ConfigContextSchema,
@@ -144,6 +156,509 @@ from .registry import registry
144
156
 
145
157
  logger = logging.getLogger(__name__)
146
158
 
159
+ #
160
+ # Approval Workflows
161
+ #
162
+
163
+
164
+ class ApprovalWorkflowDefinitionUIViewSet(NautobotUIViewSet):
165
+ """ViewSet for ApprovalWorkflowDefinition."""
166
+
167
+ bulk_update_form_class = forms.ApprovalWorkflowDefinitionBulkEditForm
168
+ filterset_class = filters.ApprovalWorkflowDefinitionFilterSet
169
+ filterset_form_class = forms.ApprovalWorkflowDefinitionFilterForm
170
+ form_class = forms.ApprovalWorkflowDefinitionForm
171
+ queryset = ApprovalWorkflowDefinition.objects.all()
172
+ serializer_class = serializers.ApprovalWorkflowDefinitionSerializer
173
+ table_class = tables.ApprovalWorkflowDefinitionTable
174
+
175
+ object_detail_content = object_detail.ObjectDetailContent(
176
+ panels=[
177
+ object_detail.ObjectFieldsPanel(
178
+ weight=100,
179
+ section=SectionChoices.LEFT_HALF,
180
+ fields="__all__",
181
+ ),
182
+ object_detail.ObjectsTablePanel(
183
+ weight=100,
184
+ table_class=tables.ApprovalWorkflowStageDefinitionTable,
185
+ table_filter="approval_workflow_definition",
186
+ section=SectionChoices.RIGHT_HALF,
187
+ exclude_columns=["approval_workflow_definition", "actions"],
188
+ add_button_route=None,
189
+ table_title="Stages",
190
+ ),
191
+ object_detail.ObjectsTablePanel(
192
+ weight=200,
193
+ table_class=tables.ApprovalWorkflowTable,
194
+ table_filter="approval_workflow_definition",
195
+ section=SectionChoices.FULL_WIDTH,
196
+ exclude_columns=["object_under_review_content_type", "approval_workflow_definition"],
197
+ add_button_route=None,
198
+ table_title="Workflows",
199
+ ),
200
+ ],
201
+ )
202
+
203
+ def get_extra_context(self, request, instance):
204
+ ctx = super().get_extra_context(request, instance)
205
+ if self.action in ("create", "update"):
206
+ if request.POST:
207
+ ctx["stages"] = forms.ApprovalWorkflowStageDefinitionFormSet(data=request.POST, instance=instance)
208
+ else:
209
+ ctx["stages"] = forms.ApprovalWorkflowStageDefinitionFormSet(instance=instance)
210
+
211
+ return ctx
212
+
213
+ def form_save(self, form, **kwargs):
214
+ obj = super().form_save(form, **kwargs)
215
+
216
+ # Process the formset for stages
217
+ ctx = self.get_extra_context(self.request, obj)
218
+ stages = ctx["stages"]
219
+ if stages.is_valid():
220
+ stages.save()
221
+ else:
222
+ raise ValidationError(stages.errors)
223
+
224
+ return obj
225
+
226
+
227
+ class ApprovalWorkflowStageDefinitionUIViewSet(NautobotUIViewSet):
228
+ """ViewSet for ApprovalWorkflowStageDefinition."""
229
+
230
+ bulk_update_form_class = forms.ApprovalWorkflowStageDefinitionBulkEditForm
231
+ filterset_class = filters.ApprovalWorkflowStageDefinitionFilterSet
232
+ filterset_form_class = forms.ApprovalWorkflowStageDefinitionFilterForm
233
+ form_class = forms.ApprovalWorkflowStageDefinitionForm
234
+ queryset = ApprovalWorkflowStageDefinition.objects.all()
235
+ serializer_class = serializers.ApprovalWorkflowStageDefinitionSerializer
236
+ table_class = tables.ApprovalWorkflowStageDefinitionTable
237
+
238
+ object_detail_content = object_detail.ObjectDetailContent(
239
+ panels=[
240
+ object_detail.ObjectFieldsPanel(
241
+ weight=100,
242
+ section=SectionChoices.LEFT_HALF,
243
+ fields="__all__",
244
+ ),
245
+ ],
246
+ )
247
+
248
+
249
+ class ApprovalWorkflowUIViewSet(
250
+ ObjectDetailViewMixin,
251
+ ObjectListViewMixin,
252
+ ObjectDestroyViewMixin,
253
+ ObjectBulkDestroyViewMixin,
254
+ ObjectChangeLogViewMixin,
255
+ ObjectNotesViewMixin,
256
+ ):
257
+ """ViewSet for ApprovalWorkflow."""
258
+
259
+ filterset_class = filters.ApprovalWorkflowFilterSet
260
+ filterset_form_class = forms.ApprovalWorkflowFilterForm
261
+ queryset = ApprovalWorkflow.objects.all()
262
+ serializer_class = serializers.ApprovalWorkflowSerializer
263
+ table_class = tables.ApprovalWorkflowTable
264
+ action_buttons = ()
265
+
266
+ class ApprovalWorkflowPanel(object_detail.ObjectFieldsPanel):
267
+ def __init__(self, **kwargs):
268
+ super().__init__(
269
+ fields=(
270
+ "approval_workflow_definition",
271
+ "object_under_review",
272
+ "current_state",
273
+ "decision_date",
274
+ "user",
275
+ ),
276
+ value_transforms={
277
+ "current_state": [render_approval_workflow_state],
278
+ },
279
+ hide_if_unset=("decision_date"),
280
+ **kwargs,
281
+ )
282
+
283
+ def render_key(self, key, value, context):
284
+ obj = get_obj_from_context(context)
285
+
286
+ if key == "object_under_review":
287
+ return helpers.bettertitle(obj.object_under_review_content_type.model_class()._meta.verbose_name)
288
+ if key == "user":
289
+ return "Requesting User"
290
+ if key == "decision_date":
291
+ if obj.current_state == ApprovalWorkflowStateChoices.APPROVED:
292
+ return "Approval Date"
293
+ elif obj.current_state == ApprovalWorkflowStateChoices.DENIED:
294
+ return "Denial Date"
295
+
296
+ return super().render_key(key, value, context)
297
+
298
+ def render_value(self, key, value, context):
299
+ obj = get_obj_from_context(context)
300
+ if key == "user":
301
+ if not obj.user:
302
+ return obj.user_name
303
+
304
+ return super().render_value(key, value, context)
305
+
306
+ object_detail_content = object_detail.ObjectDetailContent(
307
+ panels=[
308
+ ApprovalWorkflowPanel(
309
+ weight=100,
310
+ section=SectionChoices.LEFT_HALF,
311
+ ),
312
+ object_detail.ObjectsTablePanel(
313
+ weight=200,
314
+ table_title="Stages",
315
+ table_class=tables.RelatedApprovalWorkflowStageTable,
316
+ table_filter="approval_workflow",
317
+ section=SectionChoices.RIGHT_HALF,
318
+ exclude_columns=["approval_workflow"],
319
+ add_button_route=None,
320
+ ),
321
+ object_detail.ObjectsTablePanel(
322
+ weight=200,
323
+ table_title="Responses",
324
+ table_class=tables.RelatedApprovalWorkflowStageResponseTable,
325
+ table_filter="approval_workflow_stage__approval_workflow",
326
+ section=SectionChoices.RIGHT_HALF,
327
+ exclude_columns=["approval_workflow"],
328
+ add_button_route=None,
329
+ ),
330
+ ],
331
+ )
332
+
333
+
334
+ class ApprovalWorkflowStageUIViewSet(
335
+ ObjectDetailViewMixin,
336
+ ObjectListViewMixin,
337
+ ObjectDestroyViewMixin,
338
+ ObjectBulkDestroyViewMixin,
339
+ ObjectChangeLogViewMixin,
340
+ ObjectNotesViewMixin,
341
+ ):
342
+ """ViewSet for ApprovalWorkflowStage."""
343
+
344
+ filterset_class = filters.ApprovalWorkflowStageFilterSet
345
+ filterset_form_class = forms.ApprovalWorkflowStageFilterForm
346
+ queryset = ApprovalWorkflowStage.objects.all()
347
+ serializer_class = serializers.ApprovalWorkflowStageSerializer
348
+ table_class = tables.ApprovalWorkflowStageTable
349
+ action_buttons = ()
350
+
351
+ class ApprovalWorkflowStagePanel(object_detail.ObjectFieldsPanel):
352
+ def __init__(self, **kwargs):
353
+ super().__init__(
354
+ fields=(
355
+ "approval_workflow",
356
+ "state",
357
+ "decision_date",
358
+ "approver_group",
359
+ "min_approvers",
360
+ ),
361
+ value_transforms={
362
+ "state": [render_approval_workflow_state],
363
+ },
364
+ hide_if_unset=("decision_date"),
365
+ ignore_nonexistent_fields=True,
366
+ **kwargs,
367
+ )
368
+
369
+ def render_key(self, key, value, context):
370
+ obj = get_obj_from_context(context)
371
+
372
+ if key == "approval_workflow":
373
+ return "Approval Workflow"
374
+ if key == "decision_date":
375
+ if obj.state == ApprovalWorkflowStateChoices.APPROVED:
376
+ return "Approval Date"
377
+ elif obj.state == ApprovalWorkflowStateChoices.DENIED:
378
+ return "Denial Date"
379
+ if key == "min_approvers":
380
+ return "Minimum Number of Approvers Needed"
381
+
382
+ return super().render_key(key, value, context)
383
+
384
+ def render_value(self, key, value, context):
385
+ if key == "approver_group":
386
+ user_html = format_html(
387
+ "<span>{}</span><ul>{}</ul>",
388
+ value,
389
+ format_html_join("\n", "<li>{}</li>", ((user,) for user in value.user_set.all())),
390
+ )
391
+ return user_html
392
+
393
+ return super().render_value(key, value, context)
394
+
395
+ def get_data(self, context):
396
+ obj = get_obj_from_context(context)
397
+ data = super().get_data(context)
398
+ data["approver_group"] = obj.approval_workflow_stage_definition.approver_group
399
+ data["min_approvers"] = obj.approval_workflow_stage_definition.min_approvers
400
+ return data
401
+
402
+ object_detail_content = object_detail.ObjectDetailContent(
403
+ panels=[
404
+ ApprovalWorkflowStagePanel(
405
+ weight=100,
406
+ section=SectionChoices.LEFT_HALF,
407
+ ),
408
+ object_detail.ObjectsTablePanel(
409
+ weight=200,
410
+ table_class=tables.ApprovalWorkflowStageResponseTable,
411
+ table_filter="approval_workflow_stage",
412
+ section=SectionChoices.RIGHT_HALF,
413
+ exclude_columns=["approval_workflow_stage"],
414
+ table_title="Responses",
415
+ ),
416
+ ],
417
+ )
418
+
419
+ @action(detail=True, url_path="approve", methods=["get", "post"])
420
+ def approve(self, request, *args, **kwargs):
421
+ """
422
+ Approve the approval workflow stage response.
423
+ """
424
+ instance = self.get_object()
425
+
426
+ try:
427
+ approval_workflow_stage_response = ApprovalWorkflowStageResponse.objects.get(
428
+ approval_workflow_stage=instance,
429
+ user=request.user,
430
+ )
431
+ except ApprovalWorkflowStageResponse.DoesNotExist:
432
+ approval_workflow_stage_response = ApprovalWorkflowStageResponse.objects.create(
433
+ approval_workflow_stage=instance,
434
+ user=request.user,
435
+ )
436
+
437
+ if request.method == "GET":
438
+ obj = approval_workflow_stage_response
439
+ form = ApprovalForm(initial={"comments": obj.comments})
440
+
441
+ object_under_review = instance.approval_workflow.object_under_review
442
+ template_name = getattr(object_under_review, "get_approval_template", lambda: None)()
443
+ if not template_name:
444
+ template_name = "extras/approval_workflow/approve.html"
445
+
446
+ return render(
447
+ request,
448
+ template_name,
449
+ {
450
+ "obj": obj.approval_workflow_stage,
451
+ "object_under_review": obj.approval_workflow_stage.approval_workflow.object_under_review,
452
+ "form": form,
453
+ "obj_type": ApprovalWorkflowStage._meta.verbose_name,
454
+ "return_url": self.get_return_url(request, obj),
455
+ "card_class": "success",
456
+ "button_class": "success",
457
+ },
458
+ )
459
+ approval_workflow_stage_response.comments = request.data.get("comments")
460
+ approval_workflow_stage_response.state = ApprovalWorkflowStateChoices.APPROVED
461
+ approval_workflow_stage_response.save()
462
+ instance.refresh_from_db()
463
+ messages.success(request, f"You approved {instance}.")
464
+ return redirect(self.get_return_url(request))
465
+
466
+ @action(detail=True, url_path="deny", methods=["get", "post"])
467
+ def deny(self, request, *args, **kwargs):
468
+ """
469
+ Deny the approval workflow stage response.
470
+ """
471
+ instance = self.get_object()
472
+
473
+ try:
474
+ approval_workflow_stage_response = ApprovalWorkflowStageResponse.objects.get(
475
+ approval_workflow_stage=instance,
476
+ user=request.user,
477
+ )
478
+ except ApprovalWorkflowStageResponse.DoesNotExist:
479
+ approval_workflow_stage_response = ApprovalWorkflowStageResponse.objects.create(
480
+ approval_workflow_stage=instance,
481
+ user=request.user,
482
+ state=ApprovalWorkflowStateChoices.PENDING,
483
+ )
484
+
485
+ if request.method == "GET":
486
+ obj = approval_workflow_stage_response
487
+ form = ApprovalForm(initial={"comments": obj.comments})
488
+
489
+ return render(
490
+ request,
491
+ "extras/approval_workflow/deny.html",
492
+ {
493
+ "obj": obj.approval_workflow_stage,
494
+ "object_under_review": obj.approval_workflow_stage.approval_workflow.object_under_review,
495
+ "form": form,
496
+ "obj_type": ApprovalWorkflowStage._meta.verbose_name,
497
+ "return_url": self.get_return_url(request, obj),
498
+ },
499
+ )
500
+ approval_workflow_stage_response.comments = request.data.get("comments")
501
+ approval_workflow_stage_response.state = ApprovalWorkflowStateChoices.DENIED
502
+ approval_workflow_stage_response.save()
503
+ instance.refresh_from_db()
504
+ messages.success(request, f"You denied {instance}.")
505
+ return redirect(self.get_return_url(request))
506
+
507
+
508
+ class ApprovalWorkflowStageResponseUIViewSet(
509
+ ObjectBulkDestroyViewMixin,
510
+ ObjectDestroyViewMixin,
511
+ ):
512
+ """ViewSet for ApprovalWorkflowStageResponse."""
513
+
514
+ filterset_class = filters.ApprovalWorkflowStageResponseFilterSet
515
+ filterset_form_class = forms.ApprovalWorkflowStageResponseFilterForm
516
+ queryset = ApprovalWorkflowStageResponse.objects.all()
517
+ serializer_class = serializers.ApprovalWorkflowStageResponseSerializer
518
+ table_class = tables.ApprovalWorkflowStageResponseTable
519
+ object_detail_content = None
520
+
521
+
522
+ class ApproverDashboardView(ObjectListViewMixin):
523
+ """
524
+ View for the dashboard of approval workflow stages waiting for the current user to approve.
525
+ """
526
+
527
+ queryset = ApprovalWorkflowStage.objects.all()
528
+ filterset_class = filters.ApprovalWorkflowStageFilterSet
529
+ filterset_form_class = forms.ApprovalWorkflowStageFilterForm
530
+ table_class = tables.ApproverDashboardTable
531
+ template_name = "extras/approval_dashboard.html"
532
+ action_buttons = ()
533
+
534
+ def get_template_name(self):
535
+ """
536
+ Override the template names to use the custom dashboard template.
537
+ """
538
+ return self.template_name
539
+
540
+ def get_extra_context(self, request, instance):
541
+ """
542
+ Get the extra context for the dashboard view.
543
+ """
544
+ context = super().get_extra_context(request, instance)
545
+ context["title"] = "My Approvals"
546
+ context["approval_view"] = True
547
+ return context
548
+
549
+ def get_queryset(self):
550
+ """
551
+ Filter the queryset to only include approval workflow stages that are pending approval
552
+ and are assigned to the current user for approval.
553
+ """
554
+ return get_pending_approval_workflow_stages(self.request.user, super().get_queryset())
555
+
556
+ def list(self, request, *args, **kwargs):
557
+ """
558
+ Override the list method to display a helpful message regarding the page.
559
+ """
560
+ messages.info(
561
+ request,
562
+ "You are viewing a dashboard of approval workflow stages that are pending for your approval.",
563
+ )
564
+ return super().list(request, *args, **kwargs)
565
+
566
+
567
+ class ApproveeDashboardView(ObjectListViewMixin):
568
+ """
569
+ View for the dashboard of approval workflows trigger by the current user.
570
+ """
571
+
572
+ queryset = ApprovalWorkflow.objects.all()
573
+ filterset_class = filters.ApprovalWorkflowFilterSet
574
+ filterset_form_class = forms.ApprovalWorkflowFilterForm
575
+ table_class = tables.ApprovalWorkflowTable
576
+ template_name = "extras/approval_dashboard.html"
577
+ action_buttons = ()
578
+
579
+ def get_template_name(self):
580
+ """
581
+ Override the template names to use the custom dashboard template.
582
+ """
583
+ return self.template_name
584
+
585
+ def get_extra_context(self, request, instance):
586
+ """
587
+ Get the extra context for the dashboard view.
588
+ """
589
+ context = super().get_extra_context(request, instance)
590
+ context["title"] = "My Requests"
591
+ return context
592
+
593
+ def get_queryset(self):
594
+ """
595
+ Filter the queryset to only include workflows that triggered by the current users.
596
+ """
597
+ user = self.request.user
598
+ if user.is_anonymous:
599
+ return ApprovalWorkflow.objects.none()
600
+ queryset = super().get_queryset()
601
+ return queryset.filter(user=user).order_by("created")
602
+
603
+ def list(self, request, *args, **kwargs):
604
+ """
605
+ Override the list method to display a helpful message regarding the page.
606
+ """
607
+ messages.info(
608
+ request,
609
+ "You are viewing a dashboard of approval workflows that are requested by you.",
610
+ )
611
+ return super().list(request, *args, **kwargs)
612
+
613
+
614
+ class ObjectApprovalWorkflowView(generic.GenericView):
615
+ """
616
+ Present an pending approval workflow attached to a particular object.
617
+
618
+ base_template: Specify to explicitly identify the base object detail template to render.
619
+ If not provided, "<app>/<model>.html", "<app>/<model>_retrieve.html", or "generic/object_retrieve.html"
620
+ will be used, as per `get_base_template()`.
621
+ """
622
+
623
+ base_template: Optional[str] = None
624
+
625
+ def get(self, request, model, **kwargs):
626
+ # Handle QuerySet restriction of parent object if needed
627
+
628
+ if hasattr(model.objects, "restrict"):
629
+ obj = get_object_or_404(model.objects.restrict(request.user, "view"), **kwargs)
630
+ else:
631
+ obj = get_object_or_404(model, **kwargs)
632
+
633
+ # Gather all changes for this object (and its related objects)
634
+ approval_workflow = ApprovalWorkflow.objects.get(object_under_review_object_id=obj.pk)
635
+ stage_table = tables.RelatedApprovalWorkflowStageTable(
636
+ ApprovalWorkflowStage.objects.filter(approval_workflow=approval_workflow),
637
+ )
638
+ stage_table.columns.hide("approval_workflow")
639
+ response_table = tables.RelatedApprovalWorkflowStageResponseTable(
640
+ ApprovalWorkflowStageResponse.objects.filter(approval_workflow_stage__approval_workflow=approval_workflow)
641
+ )
642
+
643
+ base_template = get_base_template(self.base_template, model)
644
+
645
+ return render(
646
+ request,
647
+ "extras/object_approvalworkflow.html",
648
+ {
649
+ "object": obj,
650
+ "verbose_name": helpers.bettertitle(obj._meta.verbose_name),
651
+ "verbose_name_plural": obj._meta.verbose_name_plural,
652
+ "approval_workflow": approval_workflow,
653
+ "base_template": base_template,
654
+ "active_tab": "approval_workflow",
655
+ "default_time_zone": get_current_timezone(),
656
+ "stage_table": stage_table,
657
+ "response_table": response_table,
658
+ **common_detail_view_context(request, obj),
659
+ },
660
+ )
661
+
147
662
 
148
663
  #
149
664
  # Computed Fields
@@ -470,6 +985,7 @@ class ContactAssociationUIViewSet(
470
985
  serializer_class = serializers.ContactAssociationSerializer
471
986
  table_class = tables.AssociatedContactsTable
472
987
  non_filter_params = ("export", "page", "per_page", "sort")
988
+ object_detail_content = None
473
989
 
474
990
 
475
991
  class ObjectContactTeamMixin:
@@ -1194,7 +1710,7 @@ class GitRepositoryUIViewSet(NautobotUIViewSet):
1194
1710
  context = {
1195
1711
  **super().get_extra_context(request, instance),
1196
1712
  "result": job_result or {},
1197
- "base_template": "extras/configcontextschema_retrieve.html",
1713
+ "base_template": "extras/gitrepository_retrieve.html",
1198
1714
  "object": instance,
1199
1715
  "active_tab": "result",
1200
1716
  "verbose_name": instance._meta.verbose_name,
@@ -1384,6 +1900,42 @@ class JobRunView(ObjectPermissionRequiredMixin, View):
1384
1900
 
1385
1901
  return job_model
1386
1902
 
1903
+ def _handle_approval_workflow_response(self, request, scheduled_job, return_url):
1904
+ """Handle response for jobs requiring approval workflow."""
1905
+ messages.success(request, f"Job '{scheduled_job.name}' successfully submitted for approval")
1906
+ return redirect(return_url or reverse("extras:scheduledjob_approvalworkflow", args=[scheduled_job.pk]))
1907
+
1908
+ def _handle_scheduled_job_response(self, request, scheduled_job, return_url):
1909
+ """Handle response for successfully scheduled jobs."""
1910
+ messages.success(request, f"Job {scheduled_job.name} successfully scheduled")
1911
+ return redirect(return_url or "extras:scheduledjob_list")
1912
+
1913
+ def _handle_immediate_execution(
1914
+ self, request, job_model, job_class, job_form, profile, ignore_singleton_lock, job_queue, return_url
1915
+ ):
1916
+ """Handle immediate job execution."""
1917
+ job_kwargs = job_class.prepare_job_kwargs(job_form.cleaned_data)
1918
+ job_result = JobResult.enqueue_job(
1919
+ job_model,
1920
+ request.user,
1921
+ profile=profile,
1922
+ ignore_singleton_lock=ignore_singleton_lock,
1923
+ job_queue=job_queue,
1924
+ **job_class.serialize_data(job_kwargs),
1925
+ )
1926
+
1927
+ if return_url:
1928
+ messages.info(
1929
+ request,
1930
+ format_html(
1931
+ 'Job enqueued. <a href="{}">Click here for the results.</a>',
1932
+ job_result.get_absolute_url(),
1933
+ ),
1934
+ )
1935
+ return redirect(return_url)
1936
+
1937
+ return redirect("extras:jobresult", pk=job_result.pk)
1938
+
1387
1939
  def get(self, request, class_path=None, pk=None):
1388
1940
  job_model = self._get_job_model_or_404(class_path, pk)
1389
1941
 
@@ -1470,13 +2022,6 @@ class JobRunView(ObjectPermissionRequiredMixin, View):
1470
2022
  and request.POST.get("_schedule_type") != JobExecutionType.TYPE_IMMEDIATELY
1471
2023
  ):
1472
2024
  messages.error(request, "Unable to schedule job: Job may have sensitive input variables.")
1473
- elif job_model.has_sensitive_variables and job_model.approval_required:
1474
- messages.error(
1475
- request,
1476
- "Unable to run or schedule job: "
1477
- "This job is flagged as possibly having sensitive variables but is also flagged as requiring approval."
1478
- "One of these two flags must be removed before this job can be scheduled or run.",
1479
- )
1480
2025
  elif job_form is not None and job_form.is_valid() and schedule_form.is_valid():
1481
2026
  job_queue = job_form.cleaned_data.pop("_job_queue", None)
1482
2027
  if job_queue is None:
@@ -1497,52 +2042,56 @@ class JobRunView(ObjectPermissionRequiredMixin, View):
1497
2042
  ignore_singleton_lock = job_form.cleaned_data.pop("_ignore_singleton_lock", False)
1498
2043
  schedule_type = schedule_form.cleaned_data["_schedule_type"]
1499
2044
 
1500
- if (not dryrun and job_model.approval_required) or schedule_type in JobExecutionType.SCHEDULE_CHOICES:
1501
- scheduled_job = ScheduledJob.create_schedule(
1502
- job_model,
1503
- request.user,
1504
- name=schedule_form.cleaned_data.get("_schedule_name"),
1505
- start_time=schedule_form.cleaned_data.get("_schedule_start_time"),
1506
- interval=schedule_type,
1507
- crontab=schedule_form.cleaned_data.get("_recurrence_custom_time"),
1508
- approval_required=job_model.approval_required,
1509
- job_queue=job_queue,
1510
- profile=profile,
1511
- ignore_singleton_lock=ignore_singleton_lock,
1512
- **job_class.serialize_data(job_form.cleaned_data),
2045
+ scheduled_job = ScheduledJob.create_schedule(
2046
+ job_model,
2047
+ request.user,
2048
+ name=schedule_form.cleaned_data.get("_schedule_name"),
2049
+ start_time=schedule_form.cleaned_data.get("_schedule_start_time"),
2050
+ interval=schedule_type,
2051
+ crontab=schedule_form.cleaned_data.get("_recurrence_custom_time"),
2052
+ job_queue=job_queue,
2053
+ profile=profile,
2054
+ ignore_singleton_lock=ignore_singleton_lock,
2055
+ validated_save=False,
2056
+ **job_class.serialize_data(job_form.cleaned_data),
2057
+ )
2058
+ scheduled_job_has_approval_workflow = scheduled_job.has_approval_workflow_definition()
2059
+ is_scheduled = schedule_type in JobExecutionType.SCHEDULE_CHOICES
2060
+ if job_model.has_sensitive_variables and scheduled_job_has_approval_workflow:
2061
+ messages.error(
2062
+ request,
2063
+ "Unable to run or schedule job: "
2064
+ "This job is flagged as possibly having sensitive variables but also has an applicable approval workflow definition."
2065
+ "Modify or remove the approval workflow definition or modify the job to set `has_sensitive_variables` to False.",
1513
2066
  )
1514
-
1515
- if job_model.approval_required:
1516
- messages.success(request, f"Job {scheduled_job.name} successfully submitted for approval")
1517
- return redirect(return_url or "extras:scheduledjob_approval_queue_list")
1518
- else:
1519
- messages.success(request, f"Job {scheduled_job.name} successfully scheduled")
1520
- return redirect(return_url or "extras:scheduledjob_list")
1521
-
1522
2067
  else:
1523
- # Enqueue job for immediate execution
1524
- job_kwargs = job_class.prepare_job_kwargs(job_form.cleaned_data)
1525
- job_result = JobResult.enqueue_job(
2068
+ if dryrun and not is_scheduled:
2069
+ # Enqueue job for immediate execution when dryrun and (no schedule, no has_sensitive_variables)
2070
+ return self._handle_immediate_execution(
2071
+ request, job_model, job_class, job_form, profile, ignore_singleton_lock, job_queue, return_url
2072
+ )
2073
+ # Step 1: Check if approval is required
2074
+ if scheduled_job_has_approval_workflow:
2075
+ scheduled_job.validated_save()
2076
+ return self._handle_approval_workflow_response(request, scheduled_job, return_url)
2077
+
2078
+ # Step 3: If approval is not required
2079
+ if is_scheduled:
2080
+ scheduled_job.validated_save()
2081
+ return self._handle_scheduled_job_response(request, scheduled_job, return_url)
2082
+
2083
+ # Step 4: Immediate execution (no schedule, no approval)
2084
+ return self._handle_immediate_execution(
2085
+ request,
1526
2086
  job_model,
1527
- request.user,
1528
- profile=profile,
1529
- ignore_singleton_lock=ignore_singleton_lock,
1530
- job_queue=job_queue,
1531
- **job_class.serialize_data(job_kwargs),
2087
+ job_class,
2088
+ job_form,
2089
+ profile,
2090
+ ignore_singleton_lock,
2091
+ job_queue,
2092
+ return_url,
1532
2093
  )
1533
2094
 
1534
- if return_url:
1535
- messages.info(
1536
- request,
1537
- format_html(
1538
- 'Job enqueued. <a href="{}">Click here for the results.</a>',
1539
- job_result.get_absolute_url(),
1540
- ),
1541
- )
1542
- return redirect(return_url)
1543
-
1544
- return redirect("extras:jobresult", pk=job_result.pk)
1545
-
1546
2095
  if return_url:
1547
2096
  return redirect(return_url)
1548
2097
 
@@ -1603,7 +2152,6 @@ class JobView(generic.ObjectView):
1603
2152
  section=SectionChoices.RIGHT_HALF,
1604
2153
  label="Properties",
1605
2154
  fields=[
1606
- "approval_required",
1607
2155
  "supports_dryrun",
1608
2156
  "dryrun_default",
1609
2157
  "read_only",
@@ -1659,140 +2207,6 @@ class JobBulkDeleteView(generic.BulkDeleteView):
1659
2207
  table = tables.JobTable
1660
2208
 
1661
2209
 
1662
- class JobApprovalRequestView(generic.ObjectView):
1663
- """
1664
- This view handles requests to view and approve a Job execution request.
1665
- It renders the Job's form in much the same way as `JobView` except all
1666
- form fields are disabled and actions on the form relate to approval of the
1667
- job's execution, rather than initial job form input.
1668
- """
1669
-
1670
- queryset = ScheduledJob.objects.needs_approved()
1671
- template_name = "extras/job_approval_request.html"
1672
- additional_permissions = ("extras.view_job",)
1673
-
1674
- def get_extra_context(self, request, instance):
1675
- """
1676
- Render the job form with data from the scheduled_job instance, but mark all fields as disabled.
1677
- We don't care to actually get any data back from the form as we will not ever change it.
1678
- Instead, we offer the user three submit buttons, dry-run, approve, and deny, which we act upon in the post.
1679
- """
1680
- job_model = instance.job_model
1681
- if job_model is not None:
1682
- job_class = get_job(job_model.class_path, reload=True)
1683
- else:
1684
- # 2.0 TODO: remove this fallback?
1685
- job_class = get_job(instance.job_class)
1686
-
1687
- if job_class is not None:
1688
- # Render the form with all fields disabled
1689
- initial = instance.kwargs
1690
- initial["_job_queue"] = instance.job_queue
1691
- initial["_profile"] = instance.celery_kwargs.get("profile", False)
1692
- job_form = job_class().as_form(initial=initial, approval_view=True)
1693
- else:
1694
- job_form = None
1695
-
1696
- return {"job_form": job_form, **super().get_extra_context(request, instance)}
1697
-
1698
- def post(self, request, pk):
1699
- """
1700
- Act upon one of the 3 submit button actions from the user.
1701
-
1702
- dry-run will immediately enqueue the job with commit=False and send the user to the normal JobResult view
1703
- deny will delete the scheduled_job instance
1704
- approve will mark the scheduled_job as approved, allowing the schedular to schedule the job execution task
1705
- """
1706
- scheduled_job = get_object_or_404(ScheduledJob, pk=pk)
1707
-
1708
- post_data = request.POST
1709
-
1710
- deny = "_deny" in post_data
1711
- approve = "_approve" in post_data
1712
- force_approve = "_force_approve" in post_data
1713
- dry_run = "_dry_run" in post_data
1714
-
1715
- job_model = scheduled_job.job_model
1716
- job_class = get_job(job_model.class_path, reload=True)
1717
-
1718
- if dry_run:
1719
- # To dry-run a job, a user needs the same permissions that would be needed to run the job directly
1720
- if job_model is None:
1721
- messages.error(request, "There is no job associated with this request? Cannot run it!")
1722
- elif not job_model.runnable:
1723
- messages.error(request, "This job cannot be run at this time")
1724
- elif not JobModel.objects.check_perms(self.request.user, instance=job_model, action="run"):
1725
- messages.error(request, "You do not have permission to run this job")
1726
- elif not job_model.supports_dryrun:
1727
- messages.error(request, "This job does not support dryrun")
1728
- else:
1729
- # Immediately enqueue the job and send the user to the normal JobResult view
1730
- job_kwargs = job_class.prepare_job_kwargs(scheduled_job.kwargs or {})
1731
- job_kwargs["dryrun"] = True
1732
- job_result = JobResult.enqueue_job(
1733
- job_model,
1734
- request.user,
1735
- celery_kwargs=scheduled_job.celery_kwargs,
1736
- **job_class.serialize_data(job_kwargs),
1737
- )
1738
-
1739
- return redirect("extras:jobresult", pk=job_result.pk)
1740
- elif deny:
1741
- if not (
1742
- self.queryset.check_perms(request.user, instance=scheduled_job, action="delete")
1743
- and job_model is not None
1744
- and JobModel.objects.check_perms(request.user, instance=job_model, action="approve")
1745
- ):
1746
- messages.error(request, "You do not have permission to deny this request.")
1747
- else:
1748
- # Delete the scheduled_job instance
1749
- publish_event_payload = {"data": serialize_object_v2(scheduled_job)}
1750
- scheduled_job.delete()
1751
- if request.user == scheduled_job.user:
1752
- messages.error(request, f"Approval request for {scheduled_job.name} was revoked")
1753
- else:
1754
- messages.error(request, f"Approval of {scheduled_job.name} was denied")
1755
-
1756
- publish_event(topic="nautobot.jobs.approval.denied", payload=publish_event_payload)
1757
-
1758
- return redirect("extras:scheduledjob_approval_queue_list")
1759
-
1760
- elif approve or force_approve:
1761
- if job_model is None:
1762
- messages.error(request, "There is no job associated with this request? Cannot run it!")
1763
- elif not (
1764
- self.queryset.check_perms(request.user, instance=scheduled_job, action="change")
1765
- and JobModel.objects.check_perms(request.user, instance=job_model, action="approve")
1766
- ):
1767
- messages.error(request, "You do not have permission to approve this request.")
1768
- elif request.user == scheduled_job.user:
1769
- # The requestor *cannot* approve their own job
1770
- messages.error(request, "You cannot approve your own job request!")
1771
- else:
1772
- # Mark the scheduled_job as approved, allowing the schedular to schedule the job execution task
1773
- if scheduled_job.one_off and scheduled_job.start_time < timezone.now() and not force_approve:
1774
- return render(request, "extras/job_approval_confirmation.html", {"scheduled_job": scheduled_job})
1775
- scheduled_job.approved_by_user = request.user
1776
- scheduled_job.approved_at = timezone.now()
1777
- scheduled_job.save()
1778
-
1779
- publish_event_payload = {"data": serialize_object_v2(scheduled_job)}
1780
- publish_event(topic="nautobot.jobs.approval.approved", payload=publish_event_payload)
1781
-
1782
- messages.success(request, f"{scheduled_job.name} was approved and will now begin execution")
1783
-
1784
- return redirect("extras:scheduledjob_approval_queue_list")
1785
-
1786
- return render(
1787
- request,
1788
- self.get_template_name(),
1789
- {
1790
- "object": scheduled_job,
1791
- **self.get_extra_context(request, scheduled_job),
1792
- },
1793
- )
1794
-
1795
-
1796
2210
  class JobQueueUIViewSet(NautobotUIViewSet):
1797
2211
  bulk_update_form_class = forms.JobQueueBulkEditForm
1798
2212
  filterset_form_class = forms.JobQueueFilterForm
@@ -2100,34 +2514,49 @@ class ScheduledJobBulkDeleteView(generic.BulkDeleteView):
2100
2514
  filterset = filters.ScheduledJobFilterSet
2101
2515
 
2102
2516
 
2103
- class ScheduledJobApprovalQueueListView(generic.ObjectListView):
2104
- queryset = ScheduledJob.objects.needs_approved()
2105
- table = tables.ScheduledJobApprovalQueueTable
2106
- filterset = filters.ScheduledJobFilterSet
2107
- filterset_form = forms.ScheduledJobFilterForm
2108
- action_buttons = ()
2109
- template_name = "extras/scheduled_jobs_approval_queue_list.html"
2110
-
2111
-
2112
2517
  class ScheduledJobView(generic.ObjectView):
2113
2518
  queryset = ScheduledJob.objects.all()
2114
2519
 
2115
2520
  def get_extra_context(self, request, instance):
2521
+ context = super().get_extra_context(request, instance)
2522
+
2523
+ # Add job class labels
2116
2524
  job_class = get_job(instance.task)
2117
2525
  labels = {}
2118
2526
  if job_class is not None:
2119
2527
  for name, var in job_class._get_vars().items():
2120
2528
  field = var.as_field()
2121
- if field.label:
2122
- labels[name] = field.label
2123
- else:
2124
- labels[name] = pretty_name(name)
2125
- return {
2126
- "labels": labels,
2127
- "job_class_found": (job_class is not None),
2128
- "default_time_zone": get_current_timezone(),
2129
- **super().get_extra_context(request, instance),
2130
- }
2529
+ labels[name] = field.label or pretty_name(name)
2530
+
2531
+ context.update(
2532
+ {
2533
+ "labels": labels,
2534
+ "job_class_found": (job_class is not None),
2535
+ "default_time_zone": get_current_timezone(),
2536
+ }
2537
+ )
2538
+
2539
+ # Add approval workflow table
2540
+ approval_workflows = instance.associated_approval_workflows.all()
2541
+ approval_workflows_count = approval_workflows.count()
2542
+ approval_workflow_table = tables.ApprovalWorkflowTable(
2543
+ data=approval_workflows,
2544
+ user=request.user,
2545
+ exclude=["object_under_review", "object_under_review_content_type"],
2546
+ )
2547
+
2548
+ RequestConfig(
2549
+ request, paginate={"paginator_class": EnhancedPaginator, "per_page": get_paginate_count(request)}
2550
+ ).configure(approval_workflow_table)
2551
+
2552
+ context.update(
2553
+ {
2554
+ "approval_workflows_count": approval_workflows_count,
2555
+ "approval_workflow_table": approval_workflow_table,
2556
+ }
2557
+ )
2558
+
2559
+ return context
2131
2560
 
2132
2561
 
2133
2562
  class ScheduledJobDeleteView(generic.ObjectDeleteView):
@@ -2304,6 +2733,12 @@ class ObjectChangeUIViewSet(ObjectDetailViewMixin, ObjectListViewMixin):
2304
2733
  table_class = tables.ObjectChangeTable
2305
2734
  action_buttons = ("export",)
2306
2735
 
2736
+ def __init__(self, *args, **kwargs):
2737
+ super().__init__(*args, **kwargs)
2738
+ self.object_detail_content = object_detail.ObjectDetailContent()
2739
+ # Remove "Advanced" tab while keeping the main.
2740
+ self.object_detail_content.tabs = self.object_detail_content.tabs[:1]
2741
+
2307
2742
  # 2.0 TODO: Remove this remapping and solve it at the `BaseFilterSet` as it is addressing a breaking change.
2308
2743
  def get(self, request, *args, **kwargs):
2309
2744
  # Remappings below allow previous queries of time_before and time_after to use
@@ -2774,6 +3209,30 @@ class RoleUIViewSet(viewsets.NautobotUIViewSet):
2774
3209
  vdc_table.columns.hide("role")
2775
3210
  RequestConfig(request, paginate).configure(vdc_table)
2776
3211
  context["vdc_table"] = vdc_table
3212
+ if ContentType.objects.get_for_model(VPN) in context["content_types"]:
3213
+ vpns = instance.vpns.restrict(request.user, "view")
3214
+ vpn_table = VPNTable(vpns)
3215
+ vpn_table.columns.hide("role")
3216
+ RequestConfig(request, paginate).configure(vpn_table)
3217
+ context["vpn_table"] = vpn_table
3218
+ if ContentType.objects.get_for_model(VPNProfile) in context["content_types"]:
3219
+ vpn_profiles = instance.vpn_profiles.restrict(request.user, "view")
3220
+ vpn_profile_table = VPNProfileTable(vpn_profiles)
3221
+ vpn_profile_table.columns.hide("role")
3222
+ RequestConfig(request, paginate).configure(vpn_profile_table)
3223
+ context["vpn_profile_table"] = vpn_profile_table
3224
+ if ContentType.objects.get_for_model(VPNTunnel) in context["content_types"]:
3225
+ vpn_tunnels = instance.vpn_tunnels.restrict(request.user, "view")
3226
+ vpn_tunnel_table = VPNTunnelTable(vpn_tunnels)
3227
+ vpn_tunnel_table.columns.hide("role")
3228
+ RequestConfig(request, paginate).configure(vpn_tunnel_table)
3229
+ context["vpn_tunnel_table"] = vpn_tunnel_table
3230
+ if ContentType.objects.get_for_model(VPNTunnelEndpoint) in context["content_types"]:
3231
+ vpn_tunnel_endpoints = instance.vpn_tunnel_endpoints.restrict(request.user, "view")
3232
+ vpn_tunnel_endpoint_table = VPNTunnelEndpointTable(vpn_tunnel_endpoints)
3233
+ vpn_tunnel_endpoint_table.columns.hide("role")
3234
+ RequestConfig(request, paginate).configure(vpn_tunnel_endpoint_table)
3235
+ context["vpn_tunnel_endpoint_table"] = vpn_tunnel_endpoint_table
2777
3236
  return context
2778
3237
 
2779
3238