nautobot 3.0.0a2__py3-none-any.whl → 3.0.0rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (659) hide show
  1. nautobot/apps/choices.py +4 -2
  2. nautobot/apps/filters.py +7 -9
  3. nautobot/apps/models.py +2 -2
  4. nautobot/apps/ui.py +13 -1
  5. nautobot/apps/utils.py +8 -0
  6. nautobot/circuits/filters.py +3 -2
  7. nautobot/circuits/navigation.py +3 -2
  8. nautobot/circuits/templates/circuits/circuit_create.html +3 -3
  9. nautobot/circuits/templates/circuits/circuittermination_create.html +9 -24
  10. nautobot/circuits/templates/circuits/inc/circuit_termination_cable_fragment.html +6 -6
  11. nautobot/circuits/templates/circuits/inc/speed_widget.html +12 -12
  12. nautobot/circuits/tests/integration/test_circuit.py +10 -13
  13. nautobot/circuits/tests/integration/test_circuits_bulk_operations.py +0 -3
  14. nautobot/circuits/views.py +6 -2
  15. nautobot/cloud/filters.py +1 -1
  16. nautobot/cloud/navigation.py +3 -2
  17. nautobot/core/api/schema.py +1 -1
  18. nautobot/core/api/serializers.py +6 -1
  19. nautobot/core/api/urls.py +2 -0
  20. nautobot/core/api/views.py +12 -0
  21. nautobot/core/apps/__init__.py +11 -10
  22. nautobot/core/celery/__init__.py +3 -5
  23. nautobot/core/checks.py +46 -0
  24. nautobot/core/choices.py +1 -1
  25. nautobot/core/cli/bootstrap_v3_to_v5.py +105 -13
  26. nautobot/core/cli/migrate_deprecated_templates.py +227 -0
  27. nautobot/core/constants.py +3 -0
  28. nautobot/core/context_processors.py +9 -1
  29. nautobot/core/filters.py +4 -0
  30. nautobot/core/forms/__init__.py +2 -0
  31. nautobot/core/forms/forms.py +1 -1
  32. nautobot/core/forms/widgets.py +21 -2
  33. nautobot/core/jobs/__init__.py +62 -3
  34. nautobot/core/jobs/groups.py +31 -1
  35. nautobot/core/management/commands/generate_test_data.py +28 -9
  36. nautobot/core/models/__init__.py +11 -0
  37. nautobot/core/models/generics.py +9 -1
  38. nautobot/core/models/tree_queries.py +10 -5
  39. nautobot/core/models/utils.py +1 -1
  40. nautobot/core/settings.py +35 -19
  41. nautobot/core/settings.yaml +17 -33
  42. nautobot/core/signals.py +12 -1
  43. nautobot/core/tables.py +13 -6
  44. nautobot/core/templates/40x.html +1 -1
  45. nautobot/core/templates/500.html +2 -2
  46. nautobot/core/templates/admin/base.html +1 -2
  47. nautobot/core/templates/admin/change_list.html +9 -12
  48. nautobot/core/templates/admin/config/config.html +12 -12
  49. nautobot/core/templates/admin/index.html +3 -3
  50. nautobot/core/templates/base_django.html +1 -2
  51. nautobot/core/templates/buttons/export.html +1 -1
  52. nautobot/core/templates/components/button/dropdown.html +5 -3
  53. nautobot/core/templates/components/panel/body_wrapper_generic_table.html +1 -1
  54. nautobot/core/templates/components/panel/header_extra_content_table.html +1 -1
  55. nautobot/core/templates/components/panel/panel.html +3 -3
  56. nautobot/core/templates/components/tab/content_wrapper.html +6 -7
  57. nautobot/core/templates/components/tab/label_wrapper_distinct_view.html +1 -1
  58. nautobot/core/templates/echarts/echarts.html +22 -9
  59. nautobot/core/templates/generic/object_bulk_add_component.html +2 -1
  60. nautobot/core/templates/generic/object_bulk_create.html +6 -5
  61. nautobot/core/templates/generic/object_bulk_delete.html +1 -1
  62. nautobot/core/templates/generic/object_bulk_destroy.html +3 -3
  63. nautobot/core/templates/generic/object_bulk_edit.html +1 -1
  64. nautobot/core/templates/generic/object_bulk_import.html +1 -1
  65. nautobot/core/templates/generic/object_bulk_remove.html +2 -2
  66. nautobot/core/templates/generic/object_bulk_update.html +5 -4
  67. nautobot/core/templates/generic/object_create.html +5 -4
  68. nautobot/core/templates/generic/object_delete.html +1 -1
  69. nautobot/core/templates/generic/object_detail.html +1 -1
  70. nautobot/core/templates/generic/object_edit.html +1 -1
  71. nautobot/core/templates/generic/object_import.html +2 -1
  72. nautobot/core/templates/generic/object_list.html +12 -4
  73. nautobot/core/templates/generic/object_notes.html +5 -3
  74. nautobot/core/templates/generic/object_retrieve.html +4 -5
  75. nautobot/core/templates/graphene/graphiql.html +7 -8
  76. nautobot/core/templates/home.html +1 -1
  77. nautobot/core/templates/import_success.html +2 -1
  78. nautobot/core/templates/inc/computed_fields/panel_data.html +1 -1
  79. nautobot/core/templates/inc/created_updated.html +7 -3
  80. nautobot/core/templates/inc/custom_fields/panel_data.html +1 -1
  81. nautobot/core/templates/inc/footer.html +3 -1
  82. nautobot/core/templates/inc/form_static_field.html +6 -0
  83. nautobot/core/templates/inc/header.html +11 -1
  84. nautobot/core/templates/inc/image_attachments.html +2 -1
  85. nautobot/core/templates/inc/media.html +14 -0
  86. nautobot/core/templates/inc/nav_menu.html +3 -9
  87. nautobot/core/templates/inc/object_details_advanced_panel.html +2 -2
  88. nautobot/core/templates/inc/search_panel.html +4 -4
  89. nautobot/core/templates/login.html +4 -2
  90. nautobot/core/templates/nautobot_config.py.j2 +6 -11
  91. nautobot/core/templates/redoc_ui.html +7 -0
  92. nautobot/core/templates/rest_framework/api.html +103 -2
  93. nautobot/core/templates/search.html +1 -1
  94. nautobot/core/templates/swagger_ui.html +17 -3
  95. nautobot/core/templates/system_jobs/import_objects.html +1 -2
  96. nautobot/core/templates/utilities/confirmation_form.html +2 -2
  97. nautobot/core/templates/utilities/obj_table.html +10 -2
  98. nautobot/core/templates/utilities/render_field.html +7 -7
  99. nautobot/core/templates/utilities/render_jinja2.html +2 -2
  100. nautobot/core/templates/utilities/templatetags/filter_form_drawer.html +37 -4
  101. nautobot/core/templates/utilities/theme_preview.html +19 -3
  102. nautobot/core/templates/widgets/number_input_with_choices.html +44 -0
  103. nautobot/core/templates/widgets/selectwithdisabled_option.html +3 -1
  104. nautobot/core/templatetags/helpers.py +76 -18
  105. nautobot/core/testing/api.py +68 -9
  106. nautobot/core/testing/filters.py +0 -23
  107. nautobot/core/testing/integration.py +41 -17
  108. nautobot/core/testing/mixins.py +2 -0
  109. nautobot/core/testing/utils.py +18 -4
  110. nautobot/core/testing/views.py +104 -13
  111. nautobot/core/tests/integration/test_app_home.py +34 -30
  112. nautobot/core/tests/integration/test_app_navbar.py +3 -0
  113. nautobot/core/tests/integration/test_filters.py +48 -11
  114. nautobot/core/tests/integration/test_theme.py +22 -21
  115. nautobot/core/tests/nautobot_config.py +3 -0
  116. nautobot/core/tests/nautobot_config_without_example_apps.py +4 -0
  117. nautobot/core/tests/runner.py +8 -1
  118. nautobot/core/tests/test_api.py +5 -3
  119. nautobot/core/tests/test_breadcrumbs.py +27 -28
  120. nautobot/core/tests/test_checks.py +28 -0
  121. nautobot/core/tests/test_cli.py +40 -0
  122. nautobot/core/tests/test_config.py +2 -1
  123. nautobot/core/tests/test_forms.py +55 -13
  124. nautobot/core/tests/test_jobs.py +144 -3
  125. nautobot/core/tests/test_nautobot_server.py +2 -0
  126. nautobot/core/tests/test_navigations.py +76 -1
  127. nautobot/core/tests/test_patch_social_django.py +42 -0
  128. nautobot/core/tests/test_renderers.py +59 -0
  129. nautobot/core/tests/test_settings_schema.py +1 -0
  130. nautobot/core/tests/test_tables.py +3 -1
  131. nautobot/core/tests/test_templatetags_helpers.py +62 -13
  132. nautobot/core/tests/test_templatetags_ui_framework.py +4 -4
  133. nautobot/core/tests/test_titles.py +0 -16
  134. nautobot/core/tests/test_tree_queries.py +14 -1
  135. nautobot/core/tests/test_ui.py +123 -4
  136. nautobot/core/tests/test_utils.py +72 -5
  137. nautobot/core/tests/test_views.py +159 -31
  138. nautobot/core/ui/breadcrumbs.py +70 -29
  139. nautobot/core/ui/bulk_buttons.py +1 -1
  140. nautobot/core/ui/choices.py +143 -27
  141. nautobot/core/ui/constants.py +76 -12
  142. nautobot/core/ui/echarts.py +15 -20
  143. nautobot/core/ui/object_detail.py +143 -55
  144. nautobot/core/ui/titles.py +3 -6
  145. nautobot/core/urls.py +20 -9
  146. nautobot/core/utils/cache.py +2 -1
  147. nautobot/core/utils/filtering.py +28 -18
  148. nautobot/core/utils/lookup.py +49 -8
  149. nautobot/core/utils/module_loading.py +21 -0
  150. nautobot/core/utils/patch_social_django.py +128 -0
  151. nautobot/core/views/__init__.py +38 -1
  152. nautobot/core/views/generic.py +3 -3
  153. nautobot/core/views/mixins.py +45 -22
  154. nautobot/core/views/renderers.py +4 -3
  155. nautobot/core/views/viewsets.py +2 -1
  156. nautobot/data_validation/apps.py +1 -5
  157. nautobot/data_validation/custom_validators.py +4 -4
  158. nautobot/data_validation/filters.py +1 -1
  159. nautobot/data_validation/forms.py +40 -0
  160. nautobot/data_validation/migrations/0001_initial.py +0 -7
  161. nautobot/data_validation/migrations/0002_data_migration_from_app.py +3 -14
  162. nautobot/data_validation/models.py +16 -7
  163. nautobot/data_validation/navigation.py +8 -1
  164. nautobot/data_validation/tables.py +12 -5
  165. nautobot/data_validation/templates/data_validation/datacompliance_tab.html +1 -0
  166. nautobot/data_validation/templates/data_validation/device_constraints.html +61 -0
  167. nautobot/data_validation/tests/__init__.py +2 -2
  168. nautobot/data_validation/tests/migrations/test_migrations.py +83 -3
  169. nautobot/data_validation/tests/test_data_compliance_rules.py +12 -7
  170. nautobot/data_validation/tests/test_filters.py +8 -6
  171. nautobot/data_validation/tests/test_models.py +15 -0
  172. nautobot/data_validation/tests/test_views.py +190 -32
  173. nautobot/data_validation/urls.py +2 -5
  174. nautobot/data_validation/views.py +73 -40
  175. nautobot/dcim/api/serializers.py +3 -13
  176. nautobot/dcim/apps.py +4 -0
  177. nautobot/dcim/choices.py +65 -0
  178. nautobot/dcim/constants.py +7 -0
  179. nautobot/dcim/custom_validators.py +84 -0
  180. nautobot/dcim/factory.py +1 -1
  181. nautobot/dcim/filter_mixins.py +353 -4
  182. nautobot/dcim/{filters/__init__.py → filters.py} +15 -36
  183. nautobot/dcim/forms.py +90 -4
  184. nautobot/dcim/migrations/0075_interface_duplex_interface_speed_and_more.py +32 -0
  185. nautobot/dcim/migrations/{0075_add_deviceclusterassignment.py → 0076_add_deviceclusterassignment.py} +1 -1
  186. nautobot/dcim/migrations/{0076_device_cluster_to_clusters_data_migration.py → 0077_device_cluster_to_clusters_data_migration.py} +1 -1
  187. nautobot/dcim/migrations/{0077_remove_device_cluster.py → 0078_remove_device_cluster.py} +1 -1
  188. nautobot/dcim/migrations/0079_remove_device_location_tenant_name_uniqueness.py +16 -0
  189. nautobot/dcim/migrations/0080_device_name_data_migration.py +59 -0
  190. nautobot/dcim/migrations/0081_alter_device_device_redundancy_group_priority_and_more.py +25 -0
  191. nautobot/dcim/models/device_component_templates.py +33 -1
  192. nautobot/dcim/models/device_components.py +98 -64
  193. nautobot/dcim/models/devices.py +30 -20
  194. nautobot/dcim/navigation.py +7 -6
  195. nautobot/dcim/tables/devices.py +18 -0
  196. nautobot/dcim/tables/devicetypes.py +8 -1
  197. nautobot/dcim/tables/racks.py +0 -2
  198. nautobot/dcim/tables/template_code.py +15 -15
  199. nautobot/dcim/templates/dcim/cable_connect.html +28 -112
  200. nautobot/dcim/templates/dcim/cable_trace.html +0 -4
  201. nautobot/dcim/templates/dcim/{cable_edit.html → cable_update.html} +1 -1
  202. nautobot/dcim/templates/dcim/consoleport.html +7 -6
  203. nautobot/dcim/templates/dcim/consoleserverport.html +7 -6
  204. nautobot/dcim/templates/dcim/device/config.html +2 -2
  205. nautobot/dcim/templates/dcim/device/lldp_neighbors.html +1 -1
  206. nautobot/dcim/templates/dcim/device/status.html +8 -8
  207. nautobot/dcim/templates/dcim/device.html +1 -1
  208. nautobot/dcim/templates/dcim/device_component_add.html +2 -2
  209. nautobot/dcim/templates/dcim/device_create.html +5 -3
  210. nautobot/dcim/templates/dcim/device_interface_delete.html +1 -1
  211. nautobot/dcim/templates/dcim/device_list.html +73 -10
  212. nautobot/dcim/templates/dcim/devicebay.html +1 -1
  213. nautobot/dcim/templates/dcim/devicebay_populate.html +2 -2
  214. nautobot/dcim/templates/dcim/devicetype_component_add.html +2 -2
  215. nautobot/dcim/templates/dcim/footer_convert_to_contact_or_team_record.html +14 -0
  216. nautobot/dcim/templates/dcim/frontport.html +10 -9
  217. nautobot/dcim/templates/dcim/inc/devicetype_component_table.html +1 -1
  218. nautobot/dcim/templates/dcim/inc/edit_form_softwareversion_js.html +2 -2
  219. nautobot/dcim/templates/dcim/inc/moduletype_component_table.html +1 -1
  220. nautobot/dcim/templates/dcim/inc/rack_elevation.html +1 -1
  221. nautobot/dcim/templates/dcim/interface.html +35 -7
  222. nautobot/dcim/templates/dcim/interface_bulk_delete.html +1 -1
  223. nautobot/dcim/templates/dcim/interface_edit.html +2 -0
  224. nautobot/dcim/templates/dcim/inventoryitem.html +1 -1
  225. nautobot/dcim/templates/dcim/inventoryitem_add.html +3 -1
  226. nautobot/dcim/templates/dcim/inventoryitem_bulk_delete.html +1 -1
  227. nautobot/dcim/templates/dcim/inventoryitem_edit.html +3 -1
  228. nautobot/dcim/templates/dcim/module/base.html +49 -9
  229. nautobot/dcim/templates/dcim/module_consoleports.html +1 -1
  230. nautobot/dcim/templates/dcim/module_consoleserverports.html +1 -1
  231. nautobot/dcim/templates/dcim/module_frontports.html +1 -1
  232. nautobot/dcim/templates/dcim/module_interfaces.html +1 -1
  233. nautobot/dcim/templates/dcim/module_list.html +57 -8
  234. nautobot/dcim/templates/dcim/module_modulebays.html +1 -1
  235. nautobot/dcim/templates/dcim/module_poweroutlets.html +1 -1
  236. nautobot/dcim/templates/dcim/module_powerports.html +1 -1
  237. nautobot/dcim/templates/dcim/module_rearports.html +1 -1
  238. nautobot/dcim/templates/dcim/modulefamily_retrieve.html +1 -1
  239. nautobot/dcim/templates/dcim/moduletype_list.html +2 -2
  240. nautobot/dcim/templates/dcim/moduletype_retrieve.html +49 -9
  241. nautobot/dcim/templates/dcim/platform_create.html +1 -1
  242. nautobot/dcim/templates/dcim/poweroutlet.html +1 -1
  243. nautobot/dcim/templates/dcim/powerport.html +6 -5
  244. nautobot/dcim/templates/dcim/rack_elevation_list.html +17 -5
  245. nautobot/dcim/templates/dcim/rack_retrieve.html +22 -15
  246. nautobot/dcim/templates/dcim/rearport.html +8 -7
  247. nautobot/dcim/templates/dcim/trace/cable.html +1 -1
  248. nautobot/dcim/templates/dcim/virtualchassis_add_member.html +16 -14
  249. nautobot/dcim/templates/dcim/virtualchassis_update.html +15 -7
  250. nautobot/dcim/tests/integration/test_controller.py +4 -6
  251. nautobot/dcim/tests/integration/test_controller_managed_device_group.py +1 -5
  252. nautobot/dcim/tests/integration/test_create_device.py +0 -2
  253. nautobot/dcim/tests/integration/test_device_bulk_operations.py +1 -3
  254. nautobot/dcim/tests/integration/test_fileinputpicker.py +6 -10
  255. nautobot/dcim/tests/integration/test_location_bulk_operations.py +0 -2
  256. nautobot/dcim/tests/integration/test_module_bay_position.py +3 -4
  257. nautobot/dcim/tests/test_api.py +194 -6
  258. nautobot/dcim/tests/test_custom_validators.py +229 -0
  259. nautobot/dcim/tests/test_filters.py +55 -7
  260. nautobot/dcim/tests/test_forms.py +110 -8
  261. nautobot/dcim/tests/test_graphql.py +44 -1
  262. nautobot/dcim/tests/test_models.py +328 -4
  263. nautobot/dcim/tests/test_tables.py +160 -0
  264. nautobot/dcim/tests/test_views.py +132 -29
  265. nautobot/dcim/urls.py +64 -21
  266. nautobot/dcim/utils.py +3 -3
  267. nautobot/dcim/views.py +777 -397
  268. nautobot/extras/api/views.py +60 -45
  269. nautobot/extras/choices.py +2 -13
  270. nautobot/extras/datasources/git.py +3 -1
  271. nautobot/extras/{filters/mixins.py → filter_mixins.py} +1 -1
  272. nautobot/extras/{filters/customfields.py → filter_mixins_customfields.py} +42 -6
  273. nautobot/extras/{filters/__init__.py → filters.py} +33 -48
  274. nautobot/extras/forms/forms.py +14 -15
  275. nautobot/extras/forms/mixins.py +0 -41
  276. nautobot/extras/jobs.py +2 -0
  277. nautobot/extras/jobs_ui.py +4 -3
  278. nautobot/extras/management/__init__.py +11 -0
  279. nautobot/extras/management/commands/refresh_dynamic_group_member_caches.py +4 -1
  280. nautobot/extras/migrations/0127_approval_workflow_models.py +6 -6
  281. nautobot/extras/migrations/0129_jobresult_debug_log_count_jobresult_error_log_count_and_more.py +37 -0
  282. nautobot/extras/migrations/0130_jobresult_generate_log_entry_counts.py +42 -0
  283. nautobot/extras/migrations/0131_configcontext_device_families.py +18 -0
  284. nautobot/extras/models/__init__.py +1 -2
  285. nautobot/extras/models/approvals.py +33 -14
  286. nautobot/extras/models/change_logging.py +4 -0
  287. nautobot/extras/models/contacts.py +2 -0
  288. nautobot/extras/models/groups.py +44 -5
  289. nautobot/extras/models/jobs.py +60 -4
  290. nautobot/extras/models/mixins.py +28 -0
  291. nautobot/extras/models/models.py +23 -2
  292. nautobot/extras/models/secrets.py +1 -0
  293. nautobot/extras/models/statuses.py +0 -15
  294. nautobot/extras/navigation.py +13 -9
  295. nautobot/extras/plugins/__init__.py +33 -55
  296. nautobot/extras/plugins/marketplace_manifest.yml +49 -1
  297. nautobot/extras/plugins/tables.py +3 -3
  298. nautobot/extras/plugins/urls.py +2 -21
  299. nautobot/extras/plugins/utils.py +1 -33
  300. nautobot/extras/plugins/views.py +0 -9
  301. nautobot/extras/querysets.py +8 -0
  302. nautobot/extras/signals.py +20 -19
  303. nautobot/extras/tables.py +64 -68
  304. nautobot/extras/templates/django_ajax_tables/ajax_wrapper.html +2 -0
  305. nautobot/extras/templates/extras/approval_dashboard.html +7 -5
  306. nautobot/extras/templates/extras/approvalworkflowdefinition_update.html +4 -2
  307. nautobot/extras/templates/extras/approvalworkflowstage_retrieve.html +20 -12
  308. nautobot/extras/templates/extras/configcontext_update.html +1 -0
  309. nautobot/extras/templates/extras/configcontextschema_validation.html +2 -2
  310. nautobot/extras/templates/extras/dynamicgroup_retrieve.html +11 -5
  311. nautobot/extras/templates/extras/dynamicgroup_update.html +1 -1
  312. nautobot/extras/templates/extras/gitrepository_result.html +0 -2
  313. nautobot/extras/templates/extras/inc/approval_buttons_column.html +20 -6
  314. nautobot/extras/templates/extras/inc/bulk_edit_overridable_field.html +8 -7
  315. nautobot/extras/templates/extras/inc/configcontext_format.html +10 -3
  316. nautobot/extras/templates/extras/inc/graphqlquery_execute.html +71 -0
  317. nautobot/extras/templates/extras/inc/job_tiles.html +15 -3
  318. nautobot/extras/templates/extras/inc/json_format.html +10 -3
  319. nautobot/extras/templates/extras/inc/overridable_field.html +13 -12
  320. nautobot/extras/templates/extras/job.html +29 -12
  321. nautobot/extras/templates/extras/job_bulk_edit.html +18 -0
  322. nautobot/extras/templates/extras/job_edit.html +52 -46
  323. nautobot/extras/templates/extras/job_list.html +29 -25
  324. nautobot/extras/templates/extras/marketplace.html +5 -9
  325. nautobot/extras/templates/extras/object_configcontext.html +1 -1
  326. nautobot/extras/templates/extras/object_dynamicgroups.html +2 -2
  327. nautobot/extras/templates/extras/objectchange_retrieve.html +19 -39
  328. nautobot/extras/templates/extras/plugin_detail.html +29 -24
  329. nautobot/extras/templates/extras/plugins_list.html +16 -26
  330. nautobot/extras/templates/extras/role_retrieve.html +64 -0
  331. nautobot/extras/templates/extras/scheduledjob.html +4 -2
  332. nautobot/extras/templates/extras/secret_create.html +1 -1
  333. nautobot/extras/templatetags/custom_links.py +12 -12
  334. nautobot/extras/templatetags/job_buttons.py +14 -12
  335. nautobot/extras/test_jobs/invalid_import.py +9 -0
  336. nautobot/extras/test_jobs/log_counts_by_level.py +23 -0
  337. nautobot/extras/test_jobs/missing_import.py +11 -0
  338. nautobot/extras/tests/integration/test_computedfields.py +8 -9
  339. nautobot/extras/tests/integration/test_configcontextschema.py +27 -26
  340. nautobot/extras/tests/integration/test_customfields.py +9 -10
  341. nautobot/extras/tests/integration/test_dynamicgroups.py +12 -9
  342. nautobot/extras/tests/integration/test_plugin_banner.py +3 -0
  343. nautobot/extras/tests/integration/test_plugins.py +18 -6
  344. nautobot/extras/tests/integration/test_relationships.py +0 -2
  345. nautobot/extras/tests/test_api.py +90 -18
  346. nautobot/extras/tests/test_approvals.py +38 -38
  347. nautobot/extras/tests/test_changelog.py +59 -5
  348. nautobot/extras/tests/test_customfields.py +22 -13
  349. nautobot/extras/tests/test_customfields_filters.py +479 -0
  350. nautobot/extras/tests/test_dynamicgroups.py +39 -1
  351. nautobot/extras/tests/test_filters.py +57 -22
  352. nautobot/extras/tests/test_forms.py +18 -21
  353. nautobot/extras/tests/test_jobs.py +25 -4
  354. nautobot/extras/tests/test_migrations.py +1 -0
  355. nautobot/extras/tests/test_models.py +51 -33
  356. nautobot/extras/tests/test_plugins.py +36 -10
  357. nautobot/extras/tests/test_utils.py +3 -4
  358. nautobot/extras/tests/test_views.py +52 -112
  359. nautobot/extras/urls.py +0 -14
  360. nautobot/extras/views.py +164 -71
  361. nautobot/ipam/factory.py +7 -0
  362. nautobot/ipam/filter_mixins.py +38 -0
  363. nautobot/ipam/filters.py +53 -38
  364. nautobot/ipam/formfields.py +1 -1
  365. nautobot/ipam/forms.py +6 -3
  366. nautobot/ipam/migrations/0030_ipam__namespaces.py +13 -0
  367. nautobot/ipam/migrations/0031_ipam___data_migrations.py +4 -1
  368. nautobot/ipam/migrations/0054_namespace_tenant.py +25 -0
  369. nautobot/ipam/models.py +29 -2
  370. nautobot/ipam/navigation.py +3 -2
  371. nautobot/ipam/signals.py +71 -0
  372. nautobot/ipam/tables.py +19 -6
  373. nautobot/ipam/templates/ipam/inc/toggle_available.html +10 -10
  374. nautobot/ipam/templates/ipam/inc/vlangroup_header.html +1 -0
  375. nautobot/ipam/templates/ipam/ipaddress.html +14 -0
  376. nautobot/ipam/templates/ipam/ipaddress_merge.html +3 -3
  377. nautobot/ipam/templates/ipam/ipaddresstointerface_retrieve.html +1 -0
  378. nautobot/ipam/templates/ipam/namespace_ip_addresses.html +1 -1
  379. nautobot/ipam/templates/ipam/namespace_prefixes.html +1 -1
  380. nautobot/ipam/templates/ipam/namespace_update.html +15 -0
  381. nautobot/ipam/templates/ipam/namespace_vrfs.html +1 -1
  382. nautobot/ipam/templates/ipam/prefix_delete.html +1 -1
  383. nautobot/ipam/templates/ipam/prefix_list.html +14 -13
  384. nautobot/ipam/templates/ipam/vlan_interfaces.html +1 -1
  385. nautobot/ipam/templates/ipam/vlan_vminterfaces.html +1 -1
  386. nautobot/ipam/tests/migration/test_migrations.py +89 -0
  387. nautobot/ipam/tests/test_api.py +13 -6
  388. nautobot/ipam/tests/test_filters.py +36 -1
  389. nautobot/ipam/tests/test_forms.py +1 -1
  390. nautobot/ipam/tests/test_models.py +44 -2
  391. nautobot/ipam/tests/test_tables.py +1 -2
  392. nautobot/ipam/tests/test_utils.py +1 -1
  393. nautobot/ipam/tests/test_views.py +13 -14
  394. nautobot/ipam/ui.py +0 -17
  395. nautobot/ipam/utils/migrations.py +16 -2
  396. nautobot/ipam/utils/testing.py +9 -3
  397. nautobot/ipam/views.py +53 -11
  398. nautobot/load_balancers/__init__.py +0 -0
  399. nautobot/load_balancers/api/__init__.py +1 -0
  400. nautobot/load_balancers/api/serializers.py +75 -0
  401. nautobot/load_balancers/api/urls.py +23 -0
  402. nautobot/load_balancers/api/views.py +61 -0
  403. nautobot/load_balancers/apps.py +17 -0
  404. nautobot/load_balancers/choices.py +167 -0
  405. nautobot/load_balancers/filters.py +225 -0
  406. nautobot/load_balancers/forms.py +532 -0
  407. nautobot/load_balancers/management/commands/__init__.py +0 -0
  408. nautobot/load_balancers/management/commands/generate_load_balancer_models_test_data.py +38 -0
  409. nautobot/load_balancers/migrations/0001_initial.py +465 -0
  410. nautobot/load_balancers/migrations/0002_create_default_statuses_pool_members.py +31 -0
  411. nautobot/load_balancers/migrations/__init__.py +0 -0
  412. nautobot/load_balancers/models.py +423 -0
  413. nautobot/load_balancers/navigation.py +80 -0
  414. nautobot/load_balancers/tables.py +255 -0
  415. nautobot/load_balancers/tests/__init__.py +474 -0
  416. nautobot/load_balancers/tests/test_api.py +353 -0
  417. nautobot/load_balancers/tests/test_filters.py +134 -0
  418. nautobot/load_balancers/tests/test_forms.py +266 -0
  419. nautobot/load_balancers/tests/test_models.py +195 -0
  420. nautobot/load_balancers/tests/test_views.py +229 -0
  421. nautobot/load_balancers/urls.py +17 -0
  422. nautobot/load_balancers/views.py +248 -0
  423. nautobot/project-static/dist/css/github-dark.min.css +10 -0
  424. nautobot/project-static/dist/css/github.min.css +10 -0
  425. nautobot/project-static/dist/css/nautobot.css +1 -11
  426. nautobot/project-static/dist/css/nautobot.css.map +1 -1
  427. nautobot/project-static/dist/js/libraries.js +1 -1
  428. nautobot/project-static/dist/js/libraries.js.map +1 -1
  429. nautobot/project-static/dist/js/nautobot.js +1 -1
  430. nautobot/project-static/dist/js/nautobot.js.map +1 -1
  431. nautobot/project-static/js/cabletrace.js +1 -1
  432. nautobot/project-static/js/forms.js +13 -0
  433. nautobot/project-static/js/interface_filtering.js +20 -16
  434. nautobot/project-static/nautobot-icons/battery-3.svg +3 -0
  435. nautobot/project-static/nautobot-icons/bus-globe.svg +3 -0
  436. nautobot/project-static/nautobot-icons/bus-shield-check.svg +3 -0
  437. nautobot/project-static/nautobot-icons/bus-shield.svg +3 -0
  438. nautobot/project-static/nautobot-icons/cloud.svg +1 -1
  439. nautobot/project-static/nautobot-icons/control-panel.svg +1 -1
  440. nautobot/project-static/nautobot-icons/device-lifecycle.svg +1 -1
  441. nautobot/project-static/nautobot-icons/elements.svg +1 -1
  442. nautobot/project-static/nautobot-icons/extensibility.svg +3 -0
  443. nautobot/project-static/nautobot-icons/hammer.svg +1 -1
  444. nautobot/project-static/nautobot-icons/organization.svg +3 -0
  445. nautobot/project-static/nautobot-icons/secrets.svg +1 -1
  446. nautobot/project-static/nautobot-icons/security.svg +3 -0
  447. nautobot/project-static/nautobot-icons/server.svg +1 -1
  448. nautobot/project-static/nautobot-icons/star-filled.svg +1 -1
  449. nautobot/project-static/nautobot-icons/star.svg +1 -1
  450. nautobot/tenancy/api/serializers.py +1 -0
  451. nautobot/tenancy/api/views.py +2 -1
  452. nautobot/tenancy/{filters/__init__.py → filters.py} +2 -10
  453. nautobot/tenancy/navigation.py +3 -1
  454. nautobot/tenancy/tests/test_filters.py +0 -2
  455. nautobot/tenancy/views.py +2 -1
  456. nautobot/ui/package-lock.json +87 -4
  457. nautobot/ui/package.json +2 -1
  458. nautobot/ui/src/js/collapse.js +3 -3
  459. nautobot/ui/src/js/nautobot.js +16 -1
  460. nautobot/ui/src/js/select2.js +53 -2
  461. nautobot/ui/src/scss/colors.scss +1 -1
  462. nautobot/ui/src/scss/nautobot.scss +112 -30
  463. nautobot/ui/webpack.config.js +13 -0
  464. nautobot/users/templates/users/preferences.html +11 -2
  465. nautobot/users/templates/users/profile.html +45 -12
  466. nautobot/users/templates/users/sessionkey_delete.html +1 -1
  467. nautobot/users/tests/test_api.py +4 -0
  468. nautobot/users/views.py +4 -2
  469. nautobot/virtualization/filters.py +6 -1
  470. nautobot/virtualization/models.py +1 -68
  471. nautobot/virtualization/navigation.py +3 -2
  472. nautobot/virtualization/templates/virtualization/virtual_machine_vminterface_delete.html +1 -1
  473. nautobot/virtualization/templates/virtualization/virtualmachine_list.html +2 -2
  474. nautobot/virtualization/templates/virtualization/virtualmachine_update.html +3 -1
  475. nautobot/virtualization/tests/test_api.py +3 -0
  476. nautobot/virtualization/tests/test_filters.py +10 -1
  477. nautobot/virtualization/tests/test_models.py +45 -4
  478. nautobot/virtualization/views.py +4 -1
  479. nautobot/vpn/__init__.py +0 -0
  480. nautobot/vpn/api/serializers.py +113 -0
  481. nautobot/vpn/api/urls.py +19 -0
  482. nautobot/vpn/api/views.py +70 -0
  483. nautobot/vpn/apps.py +8 -0
  484. nautobot/vpn/choices.py +171 -0
  485. nautobot/vpn/factory.py +219 -0
  486. nautobot/vpn/filters.py +234 -0
  487. nautobot/vpn/forms.py +487 -0
  488. nautobot/vpn/homepage.py +19 -0
  489. nautobot/vpn/migrations/0001_initial.py +541 -0
  490. nautobot/vpn/migrations/0002_populate_defaults.py +199 -0
  491. nautobot/vpn/migrations/__init__.py +0 -0
  492. nautobot/vpn/models.py +535 -0
  493. nautobot/vpn/navigation.py +98 -0
  494. nautobot/vpn/tables.py +383 -0
  495. nautobot/vpn/templates/vpn/vpnprofile_create.html +150 -0
  496. nautobot/vpn/tests/__init__.py +0 -0
  497. nautobot/vpn/tests/test_api.py +336 -0
  498. nautobot/vpn/tests/test_filters.py +139 -0
  499. nautobot/vpn/tests/test_forms.py +293 -0
  500. nautobot/vpn/tests/test_models.py +147 -0
  501. nautobot/vpn/tests/test_views.py +300 -0
  502. nautobot/vpn/urls.py +16 -0
  503. nautobot/vpn/views.py +495 -0
  504. nautobot/wireless/navigation.py +3 -2
  505. nautobot/wireless/tests/integration/test_radio_profile.py +1 -5
  506. nautobot/wireless/tests/test_api.py +1 -1
  507. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/METADATA +15 -15
  508. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/RECORD +514 -572
  509. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/entry_points.txt +1 -0
  510. nautobot/circuits/templates/circuits/circuit.html +0 -2
  511. nautobot/circuits/templates/circuits/circuit_edit.html +0 -2
  512. nautobot/circuits/templates/circuits/circuit_retrieve.html +0 -2
  513. nautobot/circuits/templates/circuits/circuit_update.html +0 -1
  514. nautobot/circuits/templates/circuits/circuittermination.html +0 -2
  515. nautobot/circuits/templates/circuits/circuittermination_edit.html +0 -2
  516. nautobot/circuits/templates/circuits/circuittermination_retrieve.html +0 -2
  517. nautobot/circuits/templates/circuits/circuittermination_update.html +0 -1
  518. nautobot/circuits/templates/circuits/circuittype.html +0 -2
  519. nautobot/circuits/templates/circuits/circuittype_retrieve.html +0 -2
  520. nautobot/circuits/templates/circuits/inc/circuit_termination.html +0 -85
  521. nautobot/circuits/templates/circuits/provider.html +0 -2
  522. nautobot/circuits/templates/circuits/provider_edit.html +0 -2
  523. nautobot/circuits/templates/circuits/provider_retrieve.html +0 -1
  524. nautobot/circuits/templates/circuits/provider_update.html +0 -1
  525. nautobot/circuits/templates/circuits/providernetwork.html +0 -2
  526. nautobot/circuits/templates/circuits/providernetwork_retrieve.html +0 -2
  527. nautobot/cloud/templates/cloud/cloudaccount_retrieve.html +0 -2
  528. nautobot/cloud/templates/cloud/cloudnetwork_retrieve.html +0 -2
  529. nautobot/cloud/templates/cloud/cloudresourcetype_retrieve.html +0 -2
  530. nautobot/cloud/templates/cloud/cloudservice_retrieve.html +0 -2
  531. nautobot/core/templates/buttons/import.html +0 -9
  532. nautobot/data_validation/template_content.py +0 -42
  533. nautobot/data_validation/templates/data_validation/datacompliance_retrieve.html +0 -1
  534. nautobot/dcim/filters/mixins.py +0 -354
  535. nautobot/dcim/templates/dcim/controller/base.html +0 -2
  536. nautobot/dcim/templates/dcim/controller_retrieve.html +0 -2
  537. nautobot/dcim/templates/dcim/controller_wirelessnetworks.html +0 -2
  538. nautobot/dcim/templates/dcim/controllermanageddevicegroup_retrieve.html +0 -2
  539. nautobot/dcim/templates/dcim/device/base.html +0 -2
  540. nautobot/dcim/templates/dcim/device/consoleports.html +0 -2
  541. nautobot/dcim/templates/dcim/device/consoleserverports.html +0 -2
  542. nautobot/dcim/templates/dcim/device/devicebays.html +0 -2
  543. nautobot/dcim/templates/dcim/device/frontports.html +0 -2
  544. nautobot/dcim/templates/dcim/device/interfaces.html +0 -2
  545. nautobot/dcim/templates/dcim/device/inventory.html +0 -2
  546. nautobot/dcim/templates/dcim/device/modulebays.html +0 -2
  547. nautobot/dcim/templates/dcim/device/poweroutlets.html +0 -2
  548. nautobot/dcim/templates/dcim/device/powerports.html +0 -2
  549. nautobot/dcim/templates/dcim/device/rearports.html +0 -2
  550. nautobot/dcim/templates/dcim/device/wireless.html +0 -2
  551. nautobot/dcim/templates/dcim/device_component.html +0 -2
  552. nautobot/dcim/templates/dcim/device_edit.html +0 -2
  553. nautobot/dcim/templates/dcim/devicefamily_retrieve.html +0 -2
  554. nautobot/dcim/templates/dcim/deviceredundancygroup_retrieve.html +0 -2
  555. nautobot/dcim/templates/dcim/devicetype.html +0 -2
  556. nautobot/dcim/templates/dcim/devicetype_edit.html +0 -2
  557. nautobot/dcim/templates/dcim/devicetype_retrieve.html +0 -2
  558. nautobot/dcim/templates/dcim/inc/device_napalm_tabs.html +0 -1
  559. nautobot/dcim/templates/dcim/interfaceredundancygroup_retrieve.html +0 -2
  560. nautobot/dcim/templates/dcim/location.html +0 -2
  561. nautobot/dcim/templates/dcim/location_edit.html +0 -2
  562. nautobot/dcim/templates/dcim/location_retrieve.html +0 -243
  563. nautobot/dcim/templates/dcim/locationtype.html +0 -2
  564. nautobot/dcim/templates/dcim/locationtype_retrieve.html +0 -2
  565. nautobot/dcim/templates/dcim/manufacturer.html +0 -2
  566. nautobot/dcim/templates/dcim/modulebay_retrieve.html +0 -1
  567. nautobot/dcim/templates/dcim/platform.html +0 -2
  568. nautobot/dcim/templates/dcim/powerfeed.html +0 -2
  569. nautobot/dcim/templates/dcim/powerfeed_retrieve.html +0 -2
  570. nautobot/dcim/templates/dcim/powerpanel.html +0 -2
  571. nautobot/dcim/templates/dcim/powerpanel_edit.html +0 -2
  572. nautobot/dcim/templates/dcim/powerpanel_retrieve.html +0 -2
  573. nautobot/dcim/templates/dcim/rack.html +0 -2
  574. nautobot/dcim/templates/dcim/rack_edit.html +0 -2
  575. nautobot/dcim/templates/dcim/rackgroup.html +0 -2
  576. nautobot/dcim/templates/dcim/rackreservation.html +0 -2
  577. nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +0 -2
  578. nautobot/dcim/templates/dcim/softwareversion_retrieve.html +0 -2
  579. nautobot/dcim/templates/dcim/virtualchassis.html +0 -2
  580. nautobot/dcim/templates/dcim/virtualchassis_add.html +0 -2
  581. nautobot/dcim/templates/dcim/virtualchassis_edit.html +0 -2
  582. nautobot/dcim/templates/dcim/virtualchassis_retrieve.html +0 -2
  583. nautobot/dcim/templates/dcim/virtualdevicecontext_retrieve.html +0 -2
  584. nautobot/dcim/ui.py +0 -29
  585. nautobot/extras/templates/extras/computedfield.html +0 -2
  586. nautobot/extras/templates/extras/computedfield_retrieve.html +0 -2
  587. nautobot/extras/templates/extras/configcontext.html +0 -2
  588. nautobot/extras/templates/extras/configcontext_edit.html +0 -2
  589. nautobot/extras/templates/extras/configcontext_retrieve.html +0 -2
  590. nautobot/extras/templates/extras/configcontextschema.html +0 -2
  591. nautobot/extras/templates/extras/configcontextschema_edit.html +0 -2
  592. nautobot/extras/templates/extras/contact_retrieve.html +0 -2
  593. nautobot/extras/templates/extras/customfield.html +0 -2
  594. nautobot/extras/templates/extras/customfield_edit.html +0 -2
  595. nautobot/extras/templates/extras/customfield_retrieve.html +0 -2
  596. nautobot/extras/templates/extras/customlink.html +0 -2
  597. nautobot/extras/templates/extras/dynamicgroup.html +0 -2
  598. nautobot/extras/templates/extras/dynamicgroup_edit.html +0 -2
  599. nautobot/extras/templates/extras/exporttemplate.html +0 -2
  600. nautobot/extras/templates/extras/gitrepository.html +0 -2
  601. nautobot/extras/templates/extras/gitrepository_object_edit.html +0 -2
  602. nautobot/extras/templates/extras/graphqlquery.html +0 -2
  603. nautobot/extras/templates/extras/graphqlquery_list.html +0 -1
  604. nautobot/extras/templates/extras/graphqlquery_retrieve.html +0 -97
  605. nautobot/extras/templates/extras/job_detail.html +0 -2
  606. nautobot/extras/templates/extras/jobbutton_retrieve.html +0 -2
  607. nautobot/extras/templates/extras/jobhook.html +0 -2
  608. nautobot/extras/templates/extras/jobqueue_retrieve.html +0 -2
  609. nautobot/extras/templates/extras/jobresult.html +0 -2
  610. nautobot/extras/templates/extras/metadatatype_retrieve.html +0 -2
  611. nautobot/extras/templates/extras/note.html +0 -2
  612. nautobot/extras/templates/extras/note_retrieve.html +0 -1
  613. nautobot/extras/templates/extras/object_changelog.html +0 -2
  614. nautobot/extras/templates/extras/object_notes.html +0 -2
  615. nautobot/extras/templates/extras/objectchange.html +0 -2
  616. nautobot/extras/templates/extras/objectchange_list.html +0 -3
  617. nautobot/extras/templates/extras/relationship.html +0 -1
  618. nautobot/extras/templates/extras/secret.html +0 -1
  619. nautobot/extras/templates/extras/secret_edit.html +0 -1
  620. nautobot/extras/templates/extras/secretsgroup.html +0 -2
  621. nautobot/extras/templates/extras/secretsgroup_edit.html +0 -2
  622. nautobot/extras/templates/extras/secretsgroup_retrieve.html +0 -2
  623. nautobot/extras/templates/extras/status.html +0 -2
  624. nautobot/extras/templates/extras/tag.html +0 -2
  625. nautobot/extras/templates/extras/tag_edit.html +0 -2
  626. nautobot/extras/templates/extras/tag_retrieve.html +0 -2
  627. nautobot/extras/templates/extras/team_retrieve.html +0 -2
  628. nautobot/ipam/templates/ipam/inc/prefix_header_extra_content_table.html +0 -4
  629. nautobot/ipam/templates/ipam/namespace_retrieve.html +0 -1
  630. nautobot/ipam/templates/ipam/prefix.html +0 -2
  631. nautobot/ipam/templates/ipam/prefix_edit.html +0 -1
  632. nautobot/ipam/templates/ipam/prefix_retrieve.html +0 -2
  633. nautobot/ipam/templates/ipam/rir.html +0 -2
  634. nautobot/ipam/templates/ipam/routetarget.html +0 -1
  635. nautobot/ipam/templates/ipam/service.html +0 -2
  636. nautobot/ipam/templates/ipam/service_edit.html +0 -2
  637. nautobot/ipam/templates/ipam/service_retrieve.html +0 -2
  638. nautobot/ipam/templates/ipam/vlan.html +0 -2
  639. nautobot/ipam/templates/ipam/vlan_edit.html +0 -2
  640. nautobot/ipam/templates/ipam/vlan_retrieve.html +0 -2
  641. nautobot/ipam/templates/ipam/vlangroup.html +0 -2
  642. nautobot/ipam/templates/ipam/vrf.html +0 -1
  643. nautobot/tenancy/templates/tenancy/tenant.html +0 -2
  644. nautobot/tenancy/templates/tenancy/tenant_edit.html +0 -2
  645. nautobot/tenancy/templates/tenancy/tenantgroup.html +0 -2
  646. nautobot/tenancy/templates/tenancy/tenantgroup_retrieve.html +0 -1
  647. nautobot/virtualization/templates/virtualization/clustergroup.html +0 -2
  648. nautobot/virtualization/templates/virtualization/clustertype.html +0 -2
  649. nautobot/virtualization/templates/virtualization/virtualmachine.html +0 -2
  650. nautobot/virtualization/templates/virtualization/virtualmachine_edit.html +0 -2
  651. nautobot/virtualization/templates/virtualization/virtualmachine_retrieve.html +0 -2
  652. nautobot/wireless/templates/wireless/radioprofile_retrieve.html +0 -2
  653. nautobot/wireless/templates/wireless/supporteddatarate_retrieve.html +0 -2
  654. nautobot/wireless/templates/wireless/wirelessnetwork_retrieve.html +0 -2
  655. /nautobot/dcim/templates/dcim/{cable.html → cable_retrieve.html} +0 -0
  656. /nautobot/tenancy/{filters/mixins.py → filter_mixins.py} +0 -0
  657. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/LICENSE.txt +0 -0
  658. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/NOTICE +0 -0
  659. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/WHEEL +0 -0
@@ -1,5 +1,4 @@
1
1
  from django.contrib.contenttypes.models import ContentType
2
- from django.test import tag
3
2
 
4
3
  from nautobot.core.testing.integration import ObjectDetailsMixin, ObjectsListMixin, SeleniumTestCase
5
4
  from nautobot.dcim.models import Device, DeviceType, Location, LocationType, Manufacturer
@@ -16,7 +15,6 @@ class ConfigContextSchemaTestCase(SeleniumTestCase, ObjectDetailsMixin, ObjectsL
16
15
  super().setUp()
17
16
  self.login_as_superuser()
18
17
 
19
- @tag("fix_in_v3")
20
18
  def test_create_valid_config_context_schema(self):
21
19
  """
22
20
  Given a clean slate, navigate to and fill out the form for a valid schema object
@@ -34,13 +32,12 @@ class ConfigContextSchemaTestCase(SeleniumTestCase, ObjectDetailsMixin, ObjectsL
34
32
  self.fill_input("name", "Integration Schema 1")
35
33
  self.fill_input("description", "Description")
36
34
  self.fill_input("data_schema", '{"type": "object", "properties": {"a": {"type": "string"}}}')
37
- self.browser.find_by_text("Create").click()
35
+ self.browser.find_by_xpath("//button[normalize-space()='Create']").click()
38
36
 
39
37
  # Verify form redirect
40
38
  self.assertTrue(self.browser.is_text_present("Created config context schema Integration Schema 1"))
41
39
  self.assertTrue(self.browser.is_text_present("Edit"))
42
40
 
43
- @tag("fix_in_v3")
44
41
  def test_create_invalid_config_context_schema(self):
45
42
  """
46
43
  Given a clean slate, navigate to and fill out the form for an invalid schema object
@@ -61,14 +58,13 @@ class ConfigContextSchemaTestCase(SeleniumTestCase, ObjectDetailsMixin, ObjectsL
61
58
  self.fill_input("name", "Integration Schema 2")
62
59
  self.fill_input("description", "Description")
63
60
  self.fill_input("data_schema", '{"type": "object", "properties": {"a": {"type": "not a valid type"}}}')
64
- self.browser.find_by_text("Create").click()
61
+ self.browser.find_by_xpath("//button[normalize-space()='Create']").click()
65
62
 
66
63
  # Verify validation error raised to user within form
67
64
  self.assertTrue(self.browser.is_text_present("'not a valid type' is not valid under any of the given schemas"))
68
65
  self.assertTrue(self.browser.is_text_present("Add a new config context schema"))
69
66
  self.assertEqual(self.browser.find_by_name("name").first.value, "Integration Schema 2")
70
67
 
71
- @tag("fix_in_v3")
72
68
  def test_validation_tab(self):
73
69
  """
74
70
  Given a config context schema that is assigned to a config context, and device, and a VM with valid context data
@@ -137,60 +133,65 @@ class ConfigContextSchemaTestCase(SeleniumTestCase, ObjectDetailsMixin, ObjectsL
137
133
 
138
134
  # Assert Validation states
139
135
  self.assertEqual(
140
- len(self.browser.find_by_xpath("//div[@class[contains(., 'panel')]]//tbody/tr")), 3
136
+ len(self.browser.find_by_xpath("//div[@class[contains(., 'card')]]//tbody/tr")), 3
141
137
  ) # 3 rows (config context, device, virtual machine)
142
- for row in self.browser.find_by_xpath("//div[@class[contains(., 'panel')]]//tbody/tr"):
138
+ for row in self.browser.find_by_xpath("//div[@class[contains(., 'card')]]//tbody/tr"):
143
139
  self.assertEqual(
144
140
  row.find_by_tag("td")[-2].html,
145
- '<span class="text-success"><i class="mdi mdi-check-bold" title="" data-original-title="Yes"></i></span>',
141
+ '<span class="text-success"><i class="mdi mdi-check-bold" title="Yes"></i></span>',
146
142
  )
147
143
 
148
144
  # Edit the schema
149
- self.browser.links.find_by_partial_text("Edit").click()
145
+ self.switch_tab("Config Context")
146
+ self.click_button("#edit-button")
150
147
  # Change property "a" to be type string
151
148
  self.fill_input(
152
149
  "data_schema",
153
150
  '{"type": "object", "properties": {"a": {"type": "string"}, "b": {"type": "integer"}, "c": {"type": "integer"}}, "additionalProperties": false}',
154
151
  )
155
- self.browser.find_by_text("Update").click()
152
+ self.browser.find_by_xpath("//button[normalize-space()='Update']").click()
156
153
 
157
154
  # Navigate to ConfigContextSchema Validation tab
158
- self.browser.links.find_by_text("Validation").click()
155
+ self.switch_tab("Validation")
159
156
 
160
157
  # Assert Validation states
161
158
  self.assertEqual(
162
- len(self.browser.find_by_xpath("//div[@class[contains(., 'panel')]]//tbody/tr")), 3
159
+ len(self.browser.find_by_xpath("//div[@class[contains(., 'card')]]//tbody/tr")), 3
163
160
  ) # 3 rows (config context, device, virtual machine)
164
- for row in self.browser.find_by_xpath("//div[@class[contains(., 'panel')]]//tbody/tr"):
161
+ for row in self.browser.find_by_xpath("//div[@class[contains(., 'card')]]//tbody/tr"):
165
162
  self.assertEqual(
166
163
  row.find_by_tag("td")[-2].html,
167
- '<span class="text-danger"><i class="mdi mdi-close-thick" title="" data-original-title="No"></i></span><span class="text-danger">123 is not of type \'string\'</span>',
164
+ '<span class="text-danger"><i class="mdi mdi-close-thick" title="No"></i></span><span class="text-danger">123 is not of type \'string\'</span>',
168
165
  )
169
166
 
170
167
  # Edit the device local context data and redirect back to the validation tab
171
- self.browser.find_by_xpath("//div[@class[contains(., 'panel')]]//tbody/tr")[1].find_by_tag("td")[
172
- -1
173
- ].find_by_tag("a").click()
168
+ self.browser.find_by_xpath("//div[@class[contains(., 'card')]]//tbody/tr")[1].find_by_tag("td")[-1].find_by_tag(
169
+ "button"
170
+ ).click()
171
+ menu = self.browser.find_by_xpath("//ul[contains(@class, 'dropdown-menu') and contains(@class, 'show')]")
172
+ menu.is_visible(wait_time=5)
173
+ menu.find_by_tag("a").click()
174
+
174
175
  # Update the property "a" to be a string
175
176
  self.fill_input("local_config_context_data", '{"a": "foo", "b": 456, "c": 777}')
176
- self.browser.find_by_text("Update").click()
177
+ self.browser.find_by_xpath("//button[normalize-space()='Update']").click()
177
178
 
178
179
  # Assert Validation states
179
180
  self.assertEqual(
180
- len(self.browser.find_by_xpath("//div[@class[contains(., 'panel')]]//tbody/tr")), 3
181
+ len(self.browser.find_by_xpath("//div[@class[contains(., 'card')]]//tbody/tr")), 3
181
182
  ) # 3 rows (config context, device, virtual machine)
182
183
  # Config context still fails
183
184
  self.assertEqual(
184
- self.browser.find_by_xpath("//div[@class[contains(., 'panel')]]//tbody/tr")[0].find_by_tag("td")[-2].html,
185
- '<span class="text-danger"><i class="mdi mdi-close-thick" title="" data-original-title="No"></i></span><span class="text-danger">123 is not of type \'string\'</span>',
185
+ self.browser.find_by_xpath("//div[@class[contains(., 'card')]]//tbody/tr")[0].find_by_tag("td")[-2].html,
186
+ '<span class="text-danger"><i class="mdi mdi-close-thick" title="No"></i></span><span class="text-danger">123 is not of type \'string\'</span>',
186
187
  )
187
188
  # Device now passes
188
189
  self.assertEqual(
189
- self.browser.find_by_xpath("//div[@class[contains(., 'panel')]]//tbody/tr")[1].find_by_tag("td")[-2].html,
190
- '<span class="text-success"><i class="mdi mdi-check-bold" title="" data-original-title="Yes"></i></span>',
190
+ self.browser.find_by_xpath("//div[@class[contains(., 'card')]]//tbody/tr")[1].find_by_tag("td")[-2].html,
191
+ '<span class="text-success"><i class="mdi mdi-check-bold" title="Yes"></i></span>',
191
192
  )
192
193
  # Virtual machine still fails
193
194
  self.assertEqual(
194
- self.browser.find_by_xpath("//div[@class[contains(., 'panel')]]//tbody/tr")[2].find_by_tag("td")[-2].html,
195
- '<span class="text-danger"><i class="mdi mdi-close-thick" title="" data-original-title="No"></i></span><span class="text-danger">123 is not of type \'string\'</span>',
195
+ self.browser.find_by_xpath("//div[@class[contains(., 'card')]]//tbody/tr")[2].find_by_tag("td")[-2].html,
196
+ '<span class="text-danger"><i class="mdi mdi-close-thick" title="No"></i></span><span class="text-danger">123 is not of type \'string\'</span>',
196
197
  )
@@ -1,5 +1,4 @@
1
1
  from django.contrib.contenttypes.models import ContentType
2
- from django.test import tag
3
2
  from django.urls import reverse
4
3
  from selenium.webdriver.common.keys import Keys
5
4
 
@@ -111,7 +110,11 @@ class CustomFieldTestCase(SeleniumTestCase, ObjectDetailsMixin):
111
110
  self.assertEqual(len(table.find_by_css(".formset_row-custom_field_choices")), 5)
112
111
 
113
112
  # And 6 after clicking "Add another..."
114
- self.browser.find_by_css(".add-row").click()
113
+ self.click_button(".add-row")
114
+ # Before proceeding with further assertions and interactions, wait for the 6th row to appear in the DOM.
115
+ self.browser.is_element_present_by_css(
116
+ "#custom-field-choices .formset_row-custom_field_choices:nth-of-type(6)", wait_time=5
117
+ )
115
118
  rows = table.find_by_css(".formset_row-custom_field_choices")
116
119
  self.assertEqual(len(rows), 6)
117
120
  self.fill_input("custom_field_choices-5-value", "choice3")
@@ -157,7 +160,6 @@ class CustomFieldTestCase(SeleniumTestCase, ObjectDetailsMixin):
157
160
  self.assertTrue(self.browser.is_text_present("Modified custom field"))
158
161
  self.assertTrue(self.browser.is_text_present("new_choice"))
159
162
 
160
- @tag("fix_in_v3")
161
163
  def test_update_type_select_create_delete_choices(self):
162
164
  """
163
165
  Test edit existing field, deleting first choice, adding a new row and saving that as a new choice.
@@ -172,13 +174,13 @@ class CustomFieldTestCase(SeleniumTestCase, ObjectDetailsMixin):
172
174
 
173
175
  # Gather the rows, delete the first one, add a new one.
174
176
  table = self.browser.find_by_id("custom-field-choices")
175
- self.browser.find_by_css(".add-row").click() # Add a new row
177
+ self.click_button(".add-row") # Add a new row
176
178
  rows = table.find_by_css(".formset_row-custom_field_choices")
177
179
  rows.first.find_by_css(".delete-row").click() # Delete first row
178
180
 
179
181
  # Fill the new row, save it, assert correctness.
180
182
  self.fill_input("custom_field_choices-5-value", "new_choice") # Fill the last row
181
- self.browser.find_by_text("Update").click()
183
+ self.browser.find_by_xpath("//button[normalize-space()='Update']").click()
182
184
  self.assertEqual(self.browser.url, detail_url)
183
185
  self.assertTrue(self.browser.is_text_present("Modified custom field"))
184
186
  self.assertTrue(self.browser.is_text_present("new_choice"))
@@ -252,7 +254,6 @@ class CustomFieldTestCase(SeleniumTestCase, ObjectDetailsMixin):
252
254
  # Confirm the JSON data is visible
253
255
  self.assertTrue(self.browser.is_text_present("Test JSON Value"))
254
256
 
255
- @tag("fix_in_v3")
256
257
  def test_json_type_with_invalid_json(self):
257
258
  """
258
259
  This test creates a custom field with a type of "json".
@@ -274,7 +275,7 @@ class CustomFieldTestCase(SeleniumTestCase, ObjectDetailsMixin):
274
275
  active_web_element = self.browser.driver.switch_to.active_element
275
276
  # Type invalid JSON data into the form
276
277
  active_web_element.send_keys('{test_json_key: "Test Invalid JSON Value"}')
277
- self.browser.find_by_xpath(".//button[contains(text(), 'Update')]").click()
278
+ self.browser.find_by_xpath("//button[normalize-space()='Update']").click()
278
279
  self.assertTrue(self.browser.is_text_present("Enter a valid JSON"))
279
280
 
280
281
  def test_saving_object_after_its_custom_field_deleted(self):
@@ -303,9 +304,7 @@ class CustomFieldTestCase(SeleniumTestCase, ObjectDetailsMixin):
303
304
  # Visit the device edit page
304
305
  self.browser.visit(f"{self.live_server_url}{reverse('dcim:device_edit', kwargs={'pk': device.pk})}")
305
306
  # Get the first item selected on the custom field
306
- self.browser.execute_script(
307
- 'document.querySelector(\'label:has(+ * [name="cf_test_selection_field"])\').scrollIntoView({ behavior: "instant" });'
308
- )
307
+ self.scroll_element_into_view(css='label:has(+ * [name="cf_test_selection_field"])')
309
308
  self.browser.find_by_xpath(".//label[contains(text(), 'Device Selection Field')]").click()
310
309
  active_web_element = self.browser.driver.switch_to.active_element
311
310
  active_web_element.send_keys(Keys.ENTER)
@@ -1,16 +1,15 @@
1
1
  from django.contrib.contenttypes.models import ContentType
2
- from django.test import tag
3
2
  from django.urls import reverse
4
3
  from selenium.webdriver.common.keys import Keys
5
4
 
6
- from nautobot.core.testing.integration import SeleniumTestCase
5
+ from nautobot.core.testing.integration import ObjectsListMixin, SeleniumTestCase
7
6
  from nautobot.dcim.models import Device
8
7
  from nautobot.extras.models import DynamicGroup
9
8
 
10
9
  from . import create_test_device
11
10
 
12
11
 
13
- class DynamicGroupTestCase(SeleniumTestCase):
12
+ class DynamicGroupTestCase(SeleniumTestCase, ObjectsListMixin):
14
13
  """
15
14
  Integration test to check nautobot.extras.models.DynamicGroup add/edit functionality.
16
15
  """
@@ -19,7 +18,6 @@ class DynamicGroupTestCase(SeleniumTestCase):
19
18
  super().setUp()
20
19
  self.login_as_superuser()
21
20
 
22
- @tag("fix_in_v3")
23
21
  def test_create_and_update(self):
24
22
  """
25
23
  Test initial add and then update of a new DynamicGroup
@@ -32,7 +30,7 @@ class DynamicGroupTestCase(SeleniumTestCase):
32
30
  self.click_navbar_entry("Organization", "Dynamic Groups")
33
31
 
34
32
  # Click add button
35
- self.browser.find_by_id("add-button").click()
33
+ self.click_add_item()
36
34
 
37
35
  # Fill out the form.
38
36
  name = "devices-active"
@@ -40,19 +38,20 @@ class DynamicGroupTestCase(SeleniumTestCase):
40
38
  self.browser.select("content_type", ct_label)
41
39
 
42
40
  # Click that "Create" button
43
- self.browser.find_by_text("Create").click()
41
+ self.click_edit_form_create_button()
44
42
 
45
43
  # Verify form redirect and presence of content.
46
44
  self.assertTrue(self.browser.is_text_present(f"Created dynamic group {name}"))
47
45
  self.assertTrue(self.browser.is_text_present("Edit"))
48
46
 
49
47
  # Edit the newly created DynamicGroup (Click that "Edit" button)
50
- self.browser.find_by_id("edit-button").click()
48
+ self.click_button("#edit-button")
51
49
 
52
50
  # Find the "Status" dynamic multi-select and type into it. Xpath is used
53
51
  # to find the next "input" after the "status" select field.
54
52
  status_field = self.browser.find_by_name("filter-status").first
55
53
  status_input = status_field.find_by_xpath("./following::input[1]").first
54
+ self.scroll_element_into_view(element=status_input)
56
55
  status_input.click() # Force focus on the input field to bring it on-screen
57
56
 
58
57
  # Fill in "Status: Active".
@@ -61,7 +60,7 @@ class DynamicGroupTestCase(SeleniumTestCase):
61
60
  status_input.type(Keys.ENTER)
62
61
 
63
62
  # Click that "Update" button
64
- self.browser.find_by_text("Update").click()
63
+ self.browser.find_by_xpath("//button[normalize-space()='Update']").click()
65
64
 
66
65
  # Verify form redirect and presence of content.
67
66
  self.assertTrue(self.browser.is_text_present(f"Modified dynamic group {name}"))
@@ -69,8 +68,12 @@ class DynamicGroupTestCase(SeleniumTestCase):
69
68
 
70
69
  # And just a cursory check to make sure that the filter worked.
71
70
  group = DynamicGroup.objects.get(name=name)
72
- self.assertEqual(group.count, Device.objects.filter(status__name="Active").count())
73
71
  self.assertEqual(group.filter, {"status": ["Active"]})
72
+ # Because we don't auto-refresh the members on UI create/update any more:
73
+ # TODO: a more complete integration test could click the "Refresh Members" JobButton, wait until the job completes,
74
+ # and so forth, rather than doing so programmatically here:
75
+ group.update_cached_members()
76
+ self.assertEqual(group.count, Device.objects.filter(status__name="Active").count())
74
77
 
75
78
  # Verify dynamic group shows up on device detail tab
76
79
  self.browser.visit(
@@ -1,6 +1,9 @@
1
+ from django.test import tag
2
+
1
3
  from nautobot.core.testing.integration import SeleniumTestCase
2
4
 
3
5
 
6
+ @tag("example_app")
4
7
  class PluginBannerTestCase(SeleniumTestCase):
5
8
  """Integration test for rendering of plugin-injected banner content."""
6
9
 
@@ -3,7 +3,7 @@ import os
3
3
  import tempfile
4
4
 
5
5
  from django.contrib.contenttypes.models import ContentType
6
- from django.test import override_settings
6
+ from django.test import override_settings, tag
7
7
  from django.urls import reverse
8
8
 
9
9
  from nautobot.circuits.models import (
@@ -18,9 +18,8 @@ from nautobot.extras.choices import WebhookHttpMethodChoices
18
18
  from nautobot.extras.context_managers import web_request_context
19
19
  from nautobot.extras.models import Status, Webhook
20
20
 
21
- from example_app.models import ExampleModel
22
-
23
21
 
22
+ @tag("example_app")
24
23
  class AppWebhookTest(SeleniumTestCase):
25
24
  """
26
25
  This test case proves that Apps can use the webhook functions when making changes on a model.
@@ -28,6 +27,9 @@ class AppWebhookTest(SeleniumTestCase):
28
27
 
29
28
  def setUp(self):
30
29
  super().setUp()
30
+
31
+ from example_app.models import ExampleModel
32
+
31
33
  tempdir = tempfile.gettempdir()
32
34
  for f in os.listdir(tempdir):
33
35
  if f.startswith("test_app_webhook_"):
@@ -61,6 +63,8 @@ class AppWebhookTest(SeleniumTestCase):
61
63
  """
62
64
  Test that webhooks are correctly triggered by an App model create.
63
65
  """
66
+ from example_app.models import ExampleModel
67
+
64
68
  self.update_headers("test_app_webhook_create")
65
69
  # Make change to model
66
70
  with web_request_context(self.user):
@@ -73,6 +77,8 @@ class AppWebhookTest(SeleniumTestCase):
73
77
  """
74
78
  Test that webhooks are correctly triggered by an App model update.
75
79
  """
80
+ from example_app.models import ExampleModel
81
+
76
82
  self.update_headers("test_app_webhook_update")
77
83
  obj = ExampleModel.objects.create(name="foo", number=100)
78
84
 
@@ -88,6 +94,8 @@ class AppWebhookTest(SeleniumTestCase):
88
94
  """
89
95
  Test that webhooks are correctly triggered by an App model delete.
90
96
  """
97
+ from example_app.models import ExampleModel
98
+
91
99
  self.update_headers(os.path.join(tempfile.gettempdir(), "test_app_webhook_delete"))
92
100
  obj = ExampleModel.objects.create(name="foo", number=100)
93
101
 
@@ -102,6 +110,8 @@ class AppWebhookTest(SeleniumTestCase):
102
110
  """
103
111
  Verify that webhook body_template is correctly used.
104
112
  """
113
+ from example_app.models import ExampleModel
114
+
105
115
  self.update_headers("test_app_webhook_with_body")
106
116
 
107
117
  self.webhook.body_template = '{"message": "{{ event }}"}'
@@ -117,6 +127,7 @@ class AppWebhookTest(SeleniumTestCase):
117
127
  os.remove(os.path.join(tempfile.gettempdir(), "test_app_webhook_with_body"))
118
128
 
119
129
 
130
+ @tag("example_app")
120
131
  class AppDocumentationTest(SeleniumTestCase):
121
132
  """
122
133
  Integration tests for ensuring App provided docs are supported.
@@ -129,16 +140,16 @@ class AppDocumentationTest(SeleniumTestCase):
129
140
  def test_object_edit_help_provided(self):
130
141
  """The ExampleModel object provides model documentation, this test ensures the help link is rendered."""
131
142
  self.browser.visit(f"{self.live_server_url}{reverse('plugins:example_app:examplemodel_add')}")
132
-
133
- self.assertTrue(self.browser.links.find_by_partial_href("example_app/docs/models/examplemodel.html"))
143
+ self.assertTrue(self.browser.links.find_by_partial_href("docs/example-app/models/examplemodel.html"))
134
144
 
135
145
  def test_object_edit_help_not_provided(self):
136
146
  """The AnotherExampleModel object doesn't provide model documentation, this test ensures no help link is provided."""
137
147
  self.browser.visit(f"{self.live_server_url}{reverse('plugins:example_app:anotherexamplemodel_add')}")
138
148
 
139
- self.assertFalse(self.browser.links.find_by_partial_href("example_app/docs/models/anotherexamplemodel.html"))
149
+ self.assertFalse(self.browser.links.find_by_partial_href("/docs/example-app/models/anotherexamplemodel.html"))
140
150
 
141
151
 
152
+ @tag("example_app")
142
153
  class AppReturnUrlTestCase(SeleniumTestCase):
143
154
  """
144
155
  Integration tests for reversing App return urls.
@@ -159,6 +170,7 @@ class AppReturnUrlTestCase(SeleniumTestCase):
159
170
  self.assertEqual(element["href"], f"{self.live_server_url}{reverse('plugins:example_app:examplemodel_list')}")
160
171
 
161
172
 
173
+ @tag("example_app")
162
174
  class AppTabsTestCase(SeleniumTestCase, ObjectDetailsMixin):
163
175
  """
164
176
  Integration tests for extra object detail UI tabs.
@@ -1,5 +1,4 @@
1
1
  from django.contrib.contenttypes.models import ContentType
2
- from django.test import tag
3
2
  from django.urls import reverse
4
3
 
5
4
  from nautobot.core.testing.integration import ObjectDetailsMixin, SeleniumTestCase
@@ -19,7 +18,6 @@ class RelationshipsTestCase(SeleniumTestCase, ObjectDetailsMixin):
19
18
  super().setUp()
20
19
  self.login_as_superuser()
21
20
 
22
- @tag("fix_in_v3")
23
21
  def test_relationship_advanced_ui(self):
24
22
  """
25
23
  This test creates a device and a relationship for that device.
@@ -10,7 +10,7 @@ from django.contrib.auth import get_user_model
10
10
  from django.contrib.auth.models import Group
11
11
  from django.contrib.contenttypes.models import ContentType
12
12
  from django.core.files.uploadedfile import SimpleUploadedFile
13
- from django.test import override_settings
13
+ from django.test import override_settings, tag
14
14
  from django.urls import reverse
15
15
  from django.utils.timezone import make_aware, now
16
16
  from rest_framework import status
@@ -144,7 +144,7 @@ class ApprovalWorkflowStageTest(
144
144
 
145
145
  cls.approval_workflow_definitions = [
146
146
  ApprovalWorkflowDefinition.objects.create(
147
- name=f"Test Approval Workflow {i}", model_content_type=cls.scheduledjob_ct, priority=i
147
+ name=f"Test Approval Workflow {i}", model_content_type=cls.scheduledjob_ct, weight=i
148
148
  )
149
149
  for i in range(4)
150
150
  ]
@@ -160,7 +160,7 @@ class ApprovalWorkflowStageTest(
160
160
  cls.approval_workflow_stage_definitions = [
161
161
  ApprovalWorkflowStageDefinition.objects.create(
162
162
  approval_workflow_definition=cls.approval_workflow_definitions[i],
163
- weight=i * 100,
163
+ sequence=i * 100,
164
164
  name=f"Test Approval Workflow Stage {i} Definition",
165
165
  min_approvers=1,
166
166
  denial_message="Stage Denial Message",
@@ -403,7 +403,7 @@ class ApprovalWorkflowStageTest(
403
403
 
404
404
  approval_workflow_stage_definition_2 = ApprovalWorkflowStageDefinition.objects.create(
405
405
  approval_workflow_definition=approval_workflow.approval_workflow_definition,
406
- weight=200,
406
+ sequence=200,
407
407
  name="Approval Workflow Stage Definition 2",
408
408
  min_approvers=1,
409
409
  denial_message="Stage 2 Denial Message",
@@ -496,7 +496,7 @@ class ApprovalWorkflowStageTest(
496
496
 
497
497
  approval_workflow_stage_definition_2 = ApprovalWorkflowStageDefinition.objects.create(
498
498
  approval_workflow_definition=approval_workflow.approval_workflow_definition,
499
- weight=200,
499
+ sequence=200,
500
500
  name="Approval Workflow Stage Definition 2",
501
501
  min_approvers=1,
502
502
  denial_message="Stage 2 Denial Message",
@@ -645,7 +645,7 @@ class ApprovalWorkflowStageTest(
645
645
  approver_group_2 = Group.objects.create(name="Approver Group 2")
646
646
  approval_workflow_stage_definition_approver_group_2 = ApprovalWorkflowStageDefinition.objects.create(
647
647
  approval_workflow_definition=self.approval_workflow_definitions[3],
648
- weight=100,
648
+ sequence=100,
649
649
  name="Test Approval Workflow Stage 1 Definition",
650
650
  min_approvers=1,
651
651
  denial_message="Stage Denial Message",
@@ -986,6 +986,9 @@ class ContactTest(APIViewTestCases.APIViewTestCase):
986
986
  bulk_update_data = {
987
987
  "address": "Carnegie Hall, New York, NY",
988
988
  }
989
+ validation_excluded_fields = [
990
+ "teams", # M2M field, excluded by default
991
+ ]
989
992
 
990
993
  @classmethod
991
994
  def setUpTestData(cls):
@@ -1793,6 +1796,7 @@ class GitRepositoryTest(APIViewTestCases.APIViewTestCase):
1793
1796
  self.assertEqual(response.data["message"], f"Repository {self.repos[0].name} sync job added to queue.")
1794
1797
  self.assertIsInstance(response.data["job_result"], dict)
1795
1798
 
1799
+ @tag("example_app")
1796
1800
  def test_create_with_app_provided_contents(self):
1797
1801
  """Test that `provided_contents` published by an App works."""
1798
1802
  self.add_permissions("extras.add_gitrepository")
@@ -2215,7 +2219,7 @@ class JobTest(
2215
2219
  ApprovalWorkflowDefinition.objects.create(
2216
2220
  name="Test Approval Workflow Definition 1",
2217
2221
  model_content_type=ContentType.objects.get_for_model(ScheduledJob),
2218
- priority=0,
2222
+ weight=0,
2219
2223
  )
2220
2224
 
2221
2225
  # Do the stuff.
@@ -2248,6 +2252,69 @@ class JobTest(
2248
2252
  self.assertTrue(schedule.approval_required)
2249
2253
  self.assertEqual(schedule.kwargs["var4"], str(device_role.pk))
2250
2254
 
2255
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2256
+ @mock.patch("nautobot.extras.api.views.get_worker_count")
2257
+ @mock.patch("nautobot.extras.models.jobs.JobResult.enqueue_job")
2258
+ def test_run_job_filtered_approval(self, mock_enqueue_job, mock_get_worker_count):
2259
+ """
2260
+ Run a job with a defined approval workflow whose filter should or should not include it.
2261
+ """
2262
+ workflow = ApprovalWorkflowDefinition(
2263
+ name="Test Approval Workflow Definition 1",
2264
+ model_content_type=ContentType.objects.get_for_model(ScheduledJob),
2265
+ weight=0,
2266
+ model_constraints={"job_model__job_class_name": "APITestJob"},
2267
+ )
2268
+ workflow.validated_save()
2269
+
2270
+ # Do the stuff.
2271
+ mock_get_worker_count.return_value = 1
2272
+ self.add_permissions("extras.run_job")
2273
+ device_role = Role.objects.get_for_model(Device).first()
2274
+ job_data = {
2275
+ "var1": "FooBar",
2276
+ "var2": 123,
2277
+ "var3": False,
2278
+ "var4": device_role.pk,
2279
+ }
2280
+
2281
+ data = {
2282
+ "data": job_data,
2283
+ # schedule is omitted
2284
+ }
2285
+
2286
+ url = self.get_run_url()
2287
+ response = self.client.post(url, data, format="json", **self.header)
2288
+ self.assertHttpStatus(response, self.run_success_response_status)
2289
+
2290
+ # Assert that a JobResult for this job was NOT created.
2291
+ self.assertFalse(JobResult.objects.filter(name=self.job_model.name).exists())
2292
+
2293
+ # Assert that we have an immediate ScheduledJob and that it matches the job_model.
2294
+ schedule = ScheduledJob.objects.last()
2295
+ self.assertIsNotNone(schedule)
2296
+ self.assertEqual(schedule.interval, JobExecutionType.TYPE_FUTURE)
2297
+ self.assertTrue(schedule.approval_required)
2298
+ self.assertEqual(schedule.kwargs["var4"], str(device_role.pk))
2299
+ mock_enqueue_job.assert_not_called()
2300
+
2301
+ # Change the workflow definition so that it no longer applies to this job model
2302
+ workflow.model_constraints = {"job_model__job_class_name__istartswith": "SomeOtherJob"}
2303
+ workflow.validated_save()
2304
+
2305
+ mock_enqueue_job.return_value = None
2306
+ deserialized_data = self.job_class.deserialize_data(job_data)
2307
+ response = self.client.post(url, data, format="json", **self.header)
2308
+ self.assertHttpStatus(response, self.run_success_response_status)
2309
+ expected_enqueue_job_args = (self.job_model, self.user)
2310
+ expected_enqueue_job_kwargs = {
2311
+ "job_queue": self.job_model.default_job_queue,
2312
+ **self.job_class.serialize_data(deserialized_data),
2313
+ }
2314
+ mock_enqueue_job.assert_called_with(*expected_enqueue_job_args, **expected_enqueue_job_kwargs)
2315
+ # No new scheduled job should be created
2316
+ self.assertEqual(schedule, ScheduledJob.objects.last())
2317
+
2251
2318
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2252
2319
  @mock.patch("nautobot.extras.api.views.get_worker_count")
2253
2320
  @mock.patch("nautobot.extras.models.jobs.JobResult.enqueue_job")
@@ -2481,13 +2548,14 @@ class JobTest(
2481
2548
  "Unable to schedule job: Job may have sensitive input variables",
2482
2549
  )
2483
2550
 
2551
+ @tag("example_app")
2484
2552
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2485
2553
  @mock.patch("nautobot.extras.api.views.get_worker_count")
2486
2554
  def test_run_a_job_with_sensitive_variables_when_approval_workflow_defined(self, mock_get_worker_count):
2487
2555
  ApprovalWorkflowDefinition.objects.create(
2488
2556
  name="Test Approval Workflow Definition 1",
2489
2557
  model_content_type=ContentType.objects.get_for_model(ScheduledJob),
2490
- priority=0,
2558
+ weight=0,
2491
2559
  )
2492
2560
 
2493
2561
  mock_get_worker_count.return_value = 1
@@ -2702,7 +2770,7 @@ class JobTest(
2702
2770
  ApprovalWorkflowDefinition.objects.create(
2703
2771
  name="Test Approval Workflow Definition 1",
2704
2772
  model_content_type=ContentType.objects.get_for_model(ScheduledJob),
2705
- priority=0,
2773
+ weight=0,
2706
2774
  )
2707
2775
 
2708
2776
  mock_get_worker_count.return_value = 1
@@ -2802,7 +2870,7 @@ class JobTest(
2802
2870
  ApprovalWorkflowDefinition.objects.create(
2803
2871
  name="Approval Definition",
2804
2872
  model_content_type=ContentType.objects.get_for_model(ScheduledJob),
2805
- priority=0,
2873
+ weight=0,
2806
2874
  )
2807
2875
 
2808
2876
  start_time = now() + timedelta(minutes=1)
@@ -3274,6 +3342,7 @@ class UserSavedViewAssociationTest(APIViewTestCases.APIViewTestCase):
3274
3342
  class ScheduledJobTest(
3275
3343
  APIViewTestCases.GetObjectViewTestCase,
3276
3344
  APIViewTestCases.ListObjectsViewTestCase,
3345
+ APIViewTestCases.DeleteObjectViewTestCase,
3277
3346
  ):
3278
3347
  model = ScheduledJob
3279
3348
  choices_fields = []
@@ -4744,9 +4813,9 @@ class TagTest(APIViewTestCases.APIViewTestCase):
4744
4813
  data = {**self.create_data[0], "content_types": [Manufacturer._meta.label_lower]}
4745
4814
  response = self.client.post(self._get_list_url(), data, format="json", **self.header)
4746
4815
 
4747
- tag = Tag.objects.filter(name=data["name"])
4816
+ tags = Tag.objects.filter(name=data["name"])
4748
4817
  self.assertHttpStatus(response, 400)
4749
- self.assertFalse(tag.exists())
4818
+ self.assertFalse(tags.exists())
4750
4819
  self.assertIn(f"Invalid content type: {Manufacturer._meta.label_lower}", response.data["content_types"])
4751
4820
 
4752
4821
  def test_create_tags_without_content_types(self):
@@ -4783,9 +4852,9 @@ class TagTest(APIViewTestCases.APIViewTestCase):
4783
4852
  """Test updating a tag without changing its content-types."""
4784
4853
  self.add_permissions("extras.change_tag")
4785
4854
 
4786
- tag = Tag.objects.exclude(content_types=ContentType.objects.get_for_model(Location)).first()
4787
- tag_content_types = list(tag.content_types.all())
4788
- url = self._get_detail_url(tag)
4855
+ tag_instance = Tag.objects.exclude(content_types=ContentType.objects.get_for_model(Location)).first()
4856
+ tag_content_types = list(tag_instance.content_types.all())
4857
+ url = self._get_detail_url(tag_instance)
4789
4858
  data = {"color": ColorChoices.COLOR_LIME}
4790
4859
 
4791
4860
  response = self.client.patch(url, data, format="json", **self.header)
@@ -4795,9 +4864,9 @@ class TagTest(APIViewTestCases.APIViewTestCase):
4795
4864
  sorted(response.data["content_types"]), sorted([f"{ct.app_label}.{ct.model}" for ct in tag_content_types])
4796
4865
  )
4797
4866
 
4798
- tag.refresh_from_db()
4799
- self.assertEqual(tag.color, ColorChoices.COLOR_LIME)
4800
- self.assertEqual(list(tag.content_types.all()), tag_content_types)
4867
+ tag_instance.refresh_from_db()
4868
+ self.assertEqual(tag_instance.color, ColorChoices.COLOR_LIME)
4869
+ self.assertEqual(list(tag_instance.content_types.all()), tag_content_types)
4801
4870
 
4802
4871
 
4803
4872
  #
@@ -4810,6 +4879,9 @@ class TeamTest(APIViewTestCases.APIViewTestCase):
4810
4879
  bulk_update_data = {
4811
4880
  "address": "Carnegie Hall, New York, NY",
4812
4881
  }
4882
+ validation_excluded_fields = [
4883
+ "contacts", # M2M field, excluded by default
4884
+ ]
4813
4885
 
4814
4886
  @classmethod
4815
4887
  def setUpTestData(cls):