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
@@ -7,7 +7,9 @@ from django.db.models import Model
7
7
  from django.test import override_settings, tag
8
8
  from django.urls import reverse
9
9
  from django.utils.functional import classproperty
10
+ from selenium.webdriver.common.by import By
10
11
  from selenium.webdriver.common.keys import Keys
12
+ from selenium.webdriver.support.expected_conditions import element_to_be_clickable
11
13
  from selenium.webdriver.support.wait import WebDriverWait
12
14
  from splinter.browser import Browser
13
15
  from splinter.exceptions import ElementDoesNotExist
@@ -64,13 +66,14 @@ class ObjectsListMixin:
64
66
  """
65
67
  Click bulk delete from dropdown menu on bottom of the items table list.
66
68
  """
67
- self.browser.execute_script(
68
- "document.querySelector('#bulk-action-buttons button[type=\"submit\"]').scrollIntoView()"
69
- )
69
+ self.scroll_element_into_view(css='#bulk-action-buttons button[type="submit"]')
70
70
  self.browser.find_by_xpath(
71
71
  '//*[@id="bulk-action-buttons"]//button[@type="submit"]/following-sibling::button[1]'
72
72
  ).click()
73
- self.browser.find_by_css('#bulk-action-buttons button[name="_delete"]').click()
73
+ bulk_delete_button = self.browser.find_by_css('#bulk-action-buttons button[name="_delete"]')
74
+ bulk_delete_button.is_visible(wait_time=5)
75
+ self.scroll_element_into_view(element=bulk_delete_button)
76
+ bulk_delete_button.click()
74
77
 
75
78
  def click_bulk_delete_all(self):
76
79
  """
@@ -94,7 +97,7 @@ class ObjectsListMixin:
94
97
  """
95
98
  Click add item button on top of the items table list.
96
99
  """
97
- self.click_button("#add_button")
100
+ self.click_button("#add-button")
98
101
 
99
102
  def click_table_link(self, row=1, column=2):
100
103
  """By default, tries to click column next to checkbox to go to the details page."""
@@ -108,7 +111,7 @@ class ObjectsListMixin:
108
111
  objects_table_container = self.browser.find_by_xpath('//*[@id="object_list_form"]')
109
112
  try:
110
113
  objects_table = objects_table_container.find_by_tag("tbody")
111
- return len(objects_table.find_by_tag("tr"))
114
+ return len(objects_table.find_by_xpath(".//tr[not(count(td[@colspan])=1)]"))
112
115
  except ElementDoesNotExist:
113
116
  return 0
114
117
 
@@ -129,7 +132,7 @@ class ObjectDetailsMixin:
129
132
  By default, it's not using the exact match, because on the UI we're often adding
130
133
  additional tags, relationships or units.
131
134
  """
132
- panel_xpath = f'//*[@id="main"]//div[@class="card-header"][contains(normalize-space(), "{panel_label}")]/following-sibling::table'
135
+ panel_xpath = f'//*[@id="main"]//div[contains(@class, "card-header") and contains(normalize-space(), "{panel_label}")]/following-sibling::div[contains(@class, "collapse")]/table'
133
136
  value = self.browser.find_by_xpath(f'{panel_xpath}//td[text()="{field_label}"]/following-sibling::td[1]').text
134
137
 
135
138
  if exact_match:
@@ -144,7 +147,7 @@ class ObjectDetailsMixin:
144
147
  TODO: remove after all panels will be moved to UI Components Framework or new Bootstrap 5 templates.
145
148
  """
146
149
  panel_xpath = f'//*[@id="main"]//div[@class="card-header"][contains(normalize-space(), "{panel_label}")]'
147
- expand_button_xpath = f"{panel_xpath}/button[normalize-space()='Expand All']"
150
+ expand_button_xpath = f"{panel_xpath}/button[normalize-space()='Expand All Groups']"
148
151
  expand_button = self.browser.find_by_xpath(expand_button_xpath)
149
152
  if not expand_button.is_empty():
150
153
  expand_button.click()
@@ -236,7 +239,7 @@ class BulkOperationsMixin:
236
239
  button_text = self.browser.find_by_xpath('//button[@name="_confirm" and @type="submit"]').text
237
240
  self.assertIn(f"Delete these {expected_count}", button_text)
238
241
 
239
- message_text = self.browser.find_by_id("confirm-bulk-deletion").find_by_xpath('//div[@class="panel-body"]').text
242
+ message_text = self.browser.find_by_id("confirm-bulk-deletion").find_by_xpath('//div[@class="card-body"]').text
240
243
  self.assertIn(f"The following operation will delete {expected_count}", message_text)
241
244
 
242
245
  def assertIsBulkDeleteJob(self):
@@ -374,10 +377,12 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
374
377
  sidenav_button = self.browser.find_by_xpath(f"{section_xpath}/button", wait_time=5)
375
378
  if not sidenav_button["aria-expanded"] == "true":
376
379
  sidenav_button.click()
377
- child_menu_xpath = f"{section_xpath}/div[@class='nb-sidenav-flyout']//a[@class='nb-sidenav-link' and normalize-space()='{child_menu_name}']"
380
+ child_menu_xpath = f"{section_xpath}/div[@class='nb-sidenav-flyout']//a[contains(@class, 'nb-sidenav-link') and normalize-space()='{child_menu_name}']"
378
381
  child_menu = self.browser.find_by_xpath(child_menu_xpath, wait_time=5)
382
+ old_url = self.browser.url
379
383
  child_menu.click()
380
384
 
385
+ WebDriverWait(self.browser, 30).until(lambda driver: driver.url != old_url)
381
386
  # Wait for body element to appear
382
387
  self.assertTrue(self.browser.is_element_present_by_tag("body", wait_time=5), "Page failed to load")
383
388
 
@@ -389,7 +394,7 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
389
394
  add_button.click()
390
395
 
391
396
  # Wait for body element to appear
392
- self.assertTrue(self.browser.is_element_present_by_tag("body", wait_time=5), "Page failed to load")
397
+ self.assertTrue(self.browser.is_element_present_by_name("_create", wait_time=5), "Page failed to load")
393
398
 
394
399
  def click_edit_form_create_button(self):
395
400
  """
@@ -399,7 +404,7 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
399
404
  add_button.click()
400
405
 
401
406
  # Wait for body element to appear
402
- self.assertTrue(self.browser.is_element_present_by_tag("body", wait_time=5), "Page failed to load")
407
+ self.assertTrue(self.browser.is_element_present_by_css(".alert-success", wait_time=5), "Page failed to load")
403
408
 
404
409
  def _fill_select2_field(self, field_name, value, search_box_class=None):
405
410
  """
@@ -409,7 +414,7 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
409
414
  search_box_class = "select2-search select2-search--dropdown"
410
415
 
411
416
  self.browser.find_by_xpath(f"//select[@id='id_{field_name}']//following-sibling::span").click()
412
- self.browser.execute_script(f"""document.querySelector('#id_{field_name}').scrollIntoView()""")
417
+ self.scroll_element_into_view(css=f"#id_{field_name}")
413
418
  search_box = self.browser.find_by_xpath(f"//*[@class='{search_box_class}']//input", wait_time=5)
414
419
  for _ in search_box.first.type(value, slowly=True):
415
420
  pass
@@ -453,17 +458,22 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
453
458
  search_box.first.type(Keys.ENTER)
454
459
 
455
460
  def click_button(self, query_selector):
456
- btn = self.browser.find_by_css(query_selector, wait_time=5)
461
+ self.browser.is_element_present_by_css(query_selector, wait_time=5)
457
462
  # Button might be visible but on the edge and then impossible to click due to vertical/horizontal scrolls
458
- self.browser.execute_script(f"document.querySelector('{query_selector}').scrollIntoView(true)")
463
+ self.scroll_element_into_view(css=query_selector)
464
+ # Scrolling may be asynchronous, wait until it's actually clickable.
465
+ WebDriverWait(self.browser.driver, 30).until(element_to_be_clickable((By.CSS_SELECTOR, query_selector)))
466
+ btn = self.browser.find_by_css(query_selector)
459
467
  btn.click()
460
468
 
461
469
  def fill_input(self, input_name, input_value):
462
470
  """
463
471
  Helper function to fill an input field. Solves issue with element could not be scrolled into view for some pages.
464
472
  """
465
- element = self.browser.find_by_name(input_name, wait_time=5)
466
- self.browser.execute_script("arguments[0].scrollIntoView(true);", element.first._element)
473
+ self.browser.is_element_present_by_name(input_name, wait_time=5)
474
+ element = self.browser.find_by_name(input_name)
475
+ self.scroll_element_into_view(element=element)
476
+ element.is_visible(wait_time=5)
467
477
  self.browser.execute_script("arguments[0].focus();", element.first._element)
468
478
  self.browser.fill(input_name, input_value)
469
479
 
@@ -473,6 +483,20 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
473
483
  self.login(self.user.username, self.password)
474
484
  self.logged_in = True
475
485
 
486
+ def scroll_element_into_view(self, element=None, css=None, xpath=None, block="start"):
487
+ """
488
+ Scroll element into view. Element can be expressed either as Splinter `ElementList`, `ElementAPI`, CSS query selector or XPath.
489
+ """
490
+ if css:
491
+ element = self.browser.find_by_css(css)
492
+ elif xpath:
493
+ element = self.browser.find_by_xpath(xpath)
494
+
495
+ self.browser.execute_script(
496
+ f"arguments[0].scrollIntoView({{ behavior: 'instant', block: '{block}' }});",
497
+ element.first._element if hasattr(element, "__iter__") else element._element,
498
+ )
499
+
476
500
 
477
501
  class BulkOperationsTestCases:
478
502
  """
@@ -19,6 +19,7 @@ from nautobot.core.testing import utils
19
19
  from nautobot.core.utils import permissions
20
20
  from nautobot.extras import management, models as extras_models
21
21
  from nautobot.extras.choices import JobResultStatusChoices
22
+ from nautobot.ipam.models import default_namespace_pk
22
23
  from nautobot.users import models as users_models
23
24
 
24
25
  # Use the proper swappable User model
@@ -81,6 +82,7 @@ class NautobotTestCaseMixin:
81
82
  """
82
83
  super().tearDown()
83
84
  cache.clear()
85
+ default_namespace_pk.set(None)
84
86
 
85
87
  def prepare_instance(self, instance):
86
88
  """
@@ -90,6 +90,20 @@ def extract_page_body(content):
90
90
  return content
91
91
 
92
92
 
93
+ def extract_page_title(content):
94
+ """
95
+ Given raw HTML content from an HTTP response, extract the page title section only.
96
+
97
+ <div id="page-title" ...>...</header>
98
+ """
99
+ try:
100
+ return re.findall(
101
+ r"<div class=\"col-4\" id=\"page-title\">(.*?)(?=<\/header)", content, flags=(re.MULTILINE | re.DOTALL)
102
+ )[0]
103
+ except IndexError:
104
+ return content
105
+
106
+
93
107
  @contextmanager
94
108
  def disable_warnings(logger_name):
95
109
  """
@@ -127,15 +141,15 @@ def generate_random_device_asset_tag_of_specified_size(size):
127
141
  def get_expected_menu_item_name(view_model) -> str:
128
142
  """Return the expected menu item name for a given model."""
129
143
  name_map = {
130
- "VM Interfaces": "Interfaces",
131
- "Object Changes": "Change Log",
144
+ "Approval Workflow Definitions": "Workflow Definitions",
145
+ "Approval Workflow Stages": "Approval Dashboard",
132
146
  "Controller Managed Device Groups": "Device Groups",
147
+ "Object Changes": "Change Log",
133
148
  "Min Max Validation Rules": "Min/Max Rules",
134
149
  "Regular Expression Validation Rules": "Regex Rules",
135
150
  "Required Validation Rules": "Required Rules",
136
151
  "Unique Validation Rules": "Unique Rules",
137
- "Approval Workflow Definitions": "Workflow Definitions",
138
- "Approval Workflow Stages": "Approval Dashboard",
152
+ "VM Interfaces": "Interfaces",
139
153
  }
140
154
 
141
155
  expected = bettertitle(view_model._meta.verbose_name_plural)
@@ -27,7 +27,10 @@ from nautobot.core.models.generics import PrimaryModel
27
27
  from nautobot.core.models.tree_queries import TreeModel
28
28
  from nautobot.core.templatetags import buttons, helpers
29
29
  from nautobot.core.testing import mixins, utils
30
+ from nautobot.core.testing.utils import extract_page_title
31
+ from nautobot.core.ui.object_detail import ObjectsTablePanel
30
32
  from nautobot.core.utils import lookup
33
+ from nautobot.core.views.mixins import NautobotViewSetMixin, PERMISSIONS_ACTION_MAP
31
34
  from nautobot.dcim.models.device_components import ComponentModel
32
35
  from nautobot.extras import choices as extras_choices, models as extras_models, querysets as extras_querysets
33
36
  from nautobot.extras.forms import CustomFieldModelFormMixin, RelationshipModelFormMixin
@@ -191,8 +194,10 @@ class ViewTestCases:
191
194
  # Try GET with model-level permission
192
195
  with CaptureQueriesContext(connection) as capture_queries_context:
193
196
  response = self.client.get(instance.get_absolute_url())
194
- # The object's display name or string representation should appear in the response body
195
- self.assertBodyContains(response, escape(getattr(instance, "display", str(instance))))
197
+
198
+ # The object's display name or string representation should appear in the header
199
+ expected_title = escape(getattr(instance, "page_title", str(instance)))
200
+ self.assertInHTML(expected_title, extract_page_title(response.content.decode(response.charset)))
196
201
 
197
202
  # If any Relationships are defined, they should appear in the response
198
203
  if self.relationships is not None:
@@ -362,18 +367,104 @@ class ViewTestCases:
362
367
 
363
368
  @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
364
369
  def test_custom_actions(self):
370
+ base_view = lookup.get_view_for_model(self.model)
371
+ if not issubclass(base_view, NautobotViewSetMixin):
372
+ self.skipTest(f"View {base_view} is not using NautobotUIViewSet")
373
+
374
+ instance = self._get_queryset().first()
375
+ for action_func in base_view.get_extra_actions():
376
+ if not action_func.detail:
377
+ continue
378
+ if "get" not in action_func.mapping:
379
+ continue
380
+ if action_func.url_name == "data-compliance" and not getattr(base_view, "object_detail_content", None):
381
+ continue
382
+ with self.subTest(action=action_func.url_name):
383
+ if action_func.url_name in self.custom_action_required_permissions:
384
+ required_permissions = self.custom_action_required_permissions[action_func.url_name]
385
+ else:
386
+ base_action = action_func.kwargs.get("custom_view_base_action")
387
+ if base_action is None:
388
+ if action_func.__name__ not in PERMISSIONS_ACTION_MAP:
389
+ self.fail(f"Missing custom_view_base_action for action {action_func.__name__}")
390
+ base_action = PERMISSIONS_ACTION_MAP[action_func.__name__]
391
+
392
+ required_permissions = [
393
+ f"{self.model._meta.app_label}.{base_action}_{self.model._meta.model_name}"
394
+ ]
395
+ required_permissions += action_func.kwargs.get("custom_view_additional_permissions", [])
396
+
397
+ try:
398
+ url = self._get_url(action_func.url_name, instance)
399
+ self.assertHttpStatus(self.client.get(url), [403, 404])
400
+ for permission in required_permissions[:-1]:
401
+ self.add_permissions(permission)
402
+ self.assertHttpStatus(self.client.get(url), [403, 404])
403
+
404
+ self.add_permissions(required_permissions[-1])
405
+ self.assertHttpStatus(self.client.get(url), 200)
406
+ finally:
407
+ # delete the permissions here so that we start from a clean slate on the next loop
408
+ self.remove_permissions(*required_permissions)
409
+
410
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
411
+ def test_body_content_table_list_url(self):
412
+ """
413
+ Testing that the badge links on related object panels are working as expected.
414
+ """
415
+ self.user.is_superuser = True
416
+ self.user.save()
365
417
  instance = self._get_queryset().first()
366
- for url_name, required_permissions in self.custom_action_required_permissions.items():
367
- url = reverse(url_name, kwargs={"pk": instance.pk})
368
- self.assertHttpStatus(self.client.get(url), 403)
369
- for permission in required_permissions[:-1]:
370
- self.add_permissions(permission)
371
- self.assertHttpStatus(self.client.get(url), 403)
372
-
373
- self.add_permissions(required_permissions[-1])
374
- self.assertHttpStatus(self.client.get(url), 200)
375
- # delete the permissions here so that repetitive calls to add_permissions do not create duplicate permissions.
376
- self.remove_permissions(*required_permissions)
418
+ if not instance:
419
+ # We should have a better mechanism to test against an empty instance, but this will remove blocker for now.
420
+ self.skipTest("No instances to test against.")
421
+ errors = []
422
+ model_name = self.model._meta.model_name
423
+
424
+ response = self.client.get(instance.get_absolute_url())
425
+ self.assertHttpStatus(response, 200)
426
+ context = response.context
427
+ if not context.get("object_detail_content"):
428
+ self.skipTest("Model is not using UIViewSet")
429
+ for tab in context["object_detail_content"].tabs:
430
+ if not tab.should_render(context):
431
+ continue
432
+ tab_label = f"'{tab.label}'" if tab.label else "main"
433
+ for panel in tab.panels:
434
+ if not isinstance(panel, ObjectsTablePanel) or panel.context_table_key:
435
+ continue
436
+ extra_context = panel.get_extra_context(context)
437
+ list_url = extra_context.get("body_content_table_list_url")
438
+ table_title = panel.label or extra_context.get("body_content_table_verbose_name_plural")
439
+ if not list_url:
440
+ # If `header_extra_content_template_path` is not set,
441
+ # we don't render the badge in the header nor the link
442
+ if not panel.header_extra_content_template_path or not panel.enable_related_link:
443
+ continue
444
+ errors.append(
445
+ (
446
+ f"Error on {model_name} {tab_label} tab: panel '{table_title}' badge link does not exist."
447
+ " Please ensure the related model has a list view, or override with a custom list URL via 'related_list_url_name=app:model_list'."
448
+ " If the link should not be enabled, you must explicitly set 'enable_related_link=False' on the ObjectsTablePanel."
449
+ )
450
+ )
451
+ continue
452
+ try:
453
+ list_response = self.client.get(list_url)
454
+ except Exception as e:
455
+ errors.append(
456
+ f"Error on {model_name} {tab_label} tab: panel '{table_title}' badge link '{list_url}': {e}"
457
+ )
458
+ else:
459
+ self.assertHttpStatus(list_response, 200)
460
+ for error in list_response.context["errors"]:
461
+ errors.append(
462
+ (
463
+ f"Error on {model_name} {tab_label} tab: panel '{table_title}' badge link '{list_url}': {error}."
464
+ )
465
+ )
466
+ if errors:
467
+ self.fail("\n".join(errors))
377
468
 
378
469
  class GetObjectChangelogViewTestCase(ModelViewTestCase):
379
470
  """
@@ -1,44 +1,48 @@
1
+ from django.test import tag
2
+
1
3
  from nautobot.circuits.models import Circuit, Provider
2
4
  from nautobot.core.testing.integration import SeleniumTestCase
3
5
  from nautobot.dcim.models import Location, PowerFeed, PowerPanel
4
6
  from nautobot.tenancy.models import Tenant
5
7
 
6
- from example_app.models import ExampleModel
7
-
8
8
 
9
+ @tag("example_app")
9
10
  class AppHomeTestCase(SeleniumTestCase):
10
11
  """Integration test the Example App homepage extensions."""
11
12
 
12
- layout = {
13
- "Organization": {
14
- "Locations": {"model": Location, "permission": "dcim.view_location"},
15
- "Example Models": {"model": ExampleModel, "permission": "example_app.view_examplemodel"},
16
- "Tenants": {"model": Tenant, "permission": "tenancy.view_tenant"},
17
- },
18
- "Example App Standard Panel": {
19
- "Example App Custom Item": {"permission": "example_app.view_examplemodel"},
20
- },
21
- "Power": {
22
- "Power Feeds": {"model": PowerFeed, "permission": "dcim.view_powerfeed"},
23
- "Power Panel": {"model": PowerPanel, "permission": "dcim.view_powerpanel"},
24
- },
25
- "Circuits": {
26
- "Providers": {"model": Provider, "permission": "circuits.view_provider"},
27
- "Circuits": {"model": Circuit, "permission": "circuits.view_circuit"},
28
- },
29
- }
30
-
31
- custom_panel_examplemodel = {
32
- "name": "Example App Custom Panel",
33
- "items": [
34
- "Example 1",
35
- "Example 2",
36
- "Example 3",
37
- ],
38
- }
39
-
40
13
  def setUp(self):
41
14
  super().setUp()
15
+
16
+ from example_app.models import ExampleModel
17
+
18
+ self.layout = {
19
+ "Organization": {
20
+ "Locations": {"model": Location, "permission": "dcim.view_location"},
21
+ "Example Models": {"model": ExampleModel, "permission": "example_app.view_examplemodel"},
22
+ "Tenants": {"model": Tenant, "permission": "tenancy.view_tenant"},
23
+ },
24
+ "Example App Standard Panel": {
25
+ "Example App Custom Item": {"permission": "example_app.view_examplemodel"},
26
+ },
27
+ "Power": {
28
+ "Power Feeds": {"model": PowerFeed, "permission": "dcim.view_powerfeed"},
29
+ "Power Panel": {"model": PowerPanel, "permission": "dcim.view_powerpanel"},
30
+ },
31
+ "Circuits": {
32
+ "Providers": {"model": Provider, "permission": "circuits.view_provider"},
33
+ "Circuits": {"model": Circuit, "permission": "circuits.view_circuit"},
34
+ },
35
+ }
36
+
37
+ self.custom_panel_examplemodel = {
38
+ "name": "Example App Custom Panel",
39
+ "items": [
40
+ "Example 1",
41
+ "Example 2",
42
+ "Example 3",
43
+ ],
44
+ }
45
+
42
46
  self.login(self.user.username, self.password)
43
47
 
44
48
  def tearDown(self):
@@ -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 AppNavBarTestCase(SeleniumTestCase):
5
8
  """Integration test the navigation menu."""
6
9
 
@@ -69,7 +69,7 @@ class ListViewFilterTestCase(SeleniumTestCase):
69
69
  filter_button.click()
70
70
 
71
71
  # assert the filter drawer has appeared
72
- self.assertTrue(filter_drawer.visible)
72
+ self.assertTrue(filter_drawer.is_visible(wait_time=10))
73
73
 
74
74
  # start typing a parent into select2
75
75
  location_type = LocationType.objects.filter(parent__isnull=True).first()
@@ -170,9 +170,7 @@ class ListViewFilterTestCase(SeleniumTestCase):
170
170
 
171
171
  # Open the filter drawer, configure filter and apply filter
172
172
  self.browser.find_by_id("id__filterbtn").click()
173
- self.browser.execute_script(
174
- f"document.querySelector('[name={text_field_name}]').scrollIntoView({{ behavior: 'instant', block: 'end' }})"
175
- )
173
+ self.scroll_element_into_view(css=f"[name={text_field_name}]", block="end")
176
174
  self.change_field_value(text_field_name, "example-text")
177
175
  self.change_field_value(integer_field_name, 4356)
178
176
  self.change_field_value(select_field_name, "SingleSelect Option A", field_type="select")
@@ -188,9 +186,7 @@ class ListViewFilterTestCase(SeleniumTestCase):
188
186
 
189
187
  # Assert on update of field in Default Filter the update is replicated on Advanced Filter
190
188
  self.browser.find_by_xpath("//a[@href='#default-filter']").click() # Go back to Basic tab
191
- self.browser.execute_script(
192
- f"document.querySelector('[name={text_field_name}]').scrollIntoView({{ behavior: 'instant', block: 'end' }})"
193
- )
189
+ self.scroll_element_into_view(css=f"[name={text_field_name}]", block="end")
194
190
  self.change_field_value(text_field_name, "test new")
195
191
  self.change_field_value(integer_field_name, 1111)
196
192
  self.change_field_value(select_field_name, "SingleSelect Option B", field_type="select")
@@ -260,9 +256,7 @@ class ListViewFilterTestCase(SeleniumTestCase):
260
256
  )
261
257
  dynamic_filter_add_button.click()
262
258
  self.browser.find_by_xpath("//a[@href='#default-filter']").click()
263
- self.browser.execute_script(
264
- f"document.querySelector('[name={text_field_name}]').scrollIntoView({{ behavior: 'instant', block: 'end' }})"
265
- )
259
+ self.scroll_element_into_view(css=f"[name={text_field_name}]", block="end")
266
260
  self.assertEqual(self.browser.find_by_name(text_field_name)[0].value, "test new update")
267
261
  self.assertEqual(self.browser.find_by_name(integer_field_name)[0].value, "8888")
268
262
  custom_select_values = self.browser.find_by_name(select_field_name)[0].find_by_tag("option")
@@ -325,7 +319,7 @@ class ListViewFilterTestCase(SeleniumTestCase):
325
319
  self.browser.find_by_xpath(apply_btn_xpath).click()
326
320
  filter_drawer = self.browser.find_by_id("FilterForm_drawer", wait_time=10)
327
321
  # Drawer is kept open
328
- self.assertTrue(filter_drawer.visible)
322
+ self.assertTrue(filter_drawer.is_visible(wait_time=10))
329
323
  # Assert the choice is applied
330
324
  self.browser.find_by_xpath(
331
325
  f"//span[@class='badge' and @data-nb-value='{tag_object.name}' and contains(text(),{tag_object.name})]"
@@ -334,3 +328,46 @@ class ListViewFilterTestCase(SeleniumTestCase):
334
328
  self.browser.find_by_xpath(
335
329
  "//a[@href='#advanced-filter']//span[contains(@class,'nb-btn-indicator') and contains(text(),'Some of the applied filters can only be viewed in Advanced')]"
336
330
  )
331
+
332
+ def test_selected_advanced_filter_automatic_application(self):
333
+ """Assert that selected advanced filter is still used even if not manually applied by user."""
334
+ # Go to the location list view
335
+ self.browser.visit(f"{self.live_server_url}{reverse('dcim:location_list')}")
336
+
337
+ # Open the filter drawer
338
+ self.browser.find_by_id("id__filterbtn").click()
339
+ # Go to advanced Tab
340
+ self.browser.find_by_xpath("//a[@href='#advanced-filter']").click()
341
+
342
+ # Click on the first column lookup field and select ASN
343
+ lookup_field_container = self.browser.find_by_id("select2-id_form-0-lookup_field-container")
344
+ self.assertTrue(lookup_field_container.is_visible(wait_time=10))
345
+ lookup_field_container.click()
346
+ self.browser.find_by_xpath(
347
+ "//ul[@id='select2-id_form-0-lookup_field-results']/li[contains(@class,'select2-results__option') "
348
+ "and contains(text(),'ASN')]"
349
+ ).click()
350
+
351
+ # Click on the second column lookup type and select exact
352
+ self.browser.find_by_id("select2-id_form-0-lookup_type-container").click()
353
+ self.browser.find_by_xpath(
354
+ "//ul[@id='select2-id_form-0-lookup_type-results']/li[contains(@class,'select2-results__option') "
355
+ "and contains(text(),'exact')]"
356
+ ).click()
357
+
358
+ # Fill ASN input value with "65001"
359
+ self.browser.find_by_xpath("//input[@id='id_for_asn']").fill("65001")
360
+
361
+ # Click "Apply Specified" button
362
+ self.browser.find_by_xpath("//form[@id='dynamic-filter-form']//button[@type='submit']").click()
363
+
364
+ # Wait for filters button indicator to appear, meaning that the page was reloaded and selected filters applied.
365
+ self.assertTrue(
366
+ self.browser.is_element_present_by_xpath(
367
+ "//button[@id='id__filterbtn']//span[@class='nb-btn-indicator']", wait_time=10
368
+ )
369
+ )
370
+
371
+ # Assert that the filter has been successfully applied to the URL, despite not being previously added to the
372
+ # selected filters list with "Add Filter" button.
373
+ self.assertEqual(self.browser.url, f"{self.live_server_url}{reverse('dcim:location_list')}" + "?asn=65001")
@@ -1,5 +1,3 @@
1
- from django.test import tag
2
-
3
1
  from nautobot.core.testing.integration import SeleniumTestCase
4
2
 
5
3
 
@@ -24,7 +22,6 @@ class ThemeTestCase(SeleniumTestCase):
24
22
  # Validate modal is not visible
25
23
  self.assertFalse(theme_modal[0].visible)
26
24
 
27
- @tag("fix_in_v3")
28
25
  def test_modal_rendered(self):
29
26
  """Modal should render when selecting the 'theme' button in the footer."""
30
27
 
@@ -35,30 +32,34 @@ class ThemeTestCase(SeleniumTestCase):
35
32
  self.assertEqual(len(self.browser.find_by_xpath("//div[@class[contains(., 'modal-backdrop')]]")), 1)
36
33
 
37
34
  # Validate modal is visible
38
- theme_modal = self.browser.find_by_xpath("//div[@id[contains(., 'theme_modal')]]")
39
- self.assertTrue(theme_modal[0].visible)
35
+ theme_modal = self.browser.find_by_xpath("//div[@id='theme_modal']")
36
+ self.assertTrue(theme_modal[0].is_visible(wait_time=5))
40
37
 
41
38
  # Validate 3 themes available to select
42
- self.assertEqual(
43
- len(self.browser.find_by_xpath("//div[@class[contains(., 'modal-body')]]//tbody/tr")), 1
44
- ) # 1 row
45
-
46
- columns = self.browser.find_by_xpath("//div[@class[contains(., 'modal-body')]]//tbody/tr/td")
39
+ columns = self.browser.find_by_xpath("//div[@class[contains(., 'modal-body')]]//dl/dt")
47
40
  self.assertEqual(len(columns), 3) # 3 columns (light, dark, system)
48
41
 
49
42
  # Validate 3 modes in order are light, dark, and system
50
- self.assertIn("light", columns[0].html)
51
- self.assertIn("dark", columns[1].html)
52
- self.assertIn("system", columns[2].html)
43
+ self.assertIn("Light", columns[0].html)
44
+ self.assertIn("Dark", columns[1].html)
45
+ self.assertIn("System", columns[2].html)
53
46
 
54
47
  # Validate only System theme is selected by default
55
- system_theme = self.browser.find_by_xpath(".//td[@id='td-light-theme']")
56
- self.assertFalse(system_theme[0].has_class("active-theme"))
57
- system_theme = self.browser.find_by_xpath(".//td[@id='td-dark-theme']")
58
- self.assertFalse(system_theme[0].has_class("active-theme"))
59
- system_theme = self.browser.find_by_xpath(".//td[@id='td-system-theme']")
60
- self.assertTrue(system_theme[0].has_class("active-theme"))
48
+ light_theme = self.browser.find_by_xpath(".//dd/button[@data-nb-theme='light']")
49
+ self.assertFalse(light_theme[0].has_class("border"))
50
+ self.assertFalse(light_theme[0].has_class("border-primary"))
51
+ dark_theme = self.browser.find_by_xpath(".//dd/button[@data-nb-theme='dark']")
52
+ self.assertFalse(dark_theme[0].has_class("border"))
53
+ self.assertFalse(dark_theme[0].has_class("border-primary"))
54
+ system_theme = self.browser.find_by_xpath(".//dd/button[@data-nb-theme='system']")
55
+ self.assertTrue(system_theme[0].has_class("border"))
56
+ self.assertTrue(system_theme[0].has_class("border-primary"))
57
+
58
+ # Why is it required to click the cancel button twice? I honestly don't know, but for some reason Selenium seems
59
+ # to have troubles here. The first press only focuses the cancel button, and only after clicking it for the
60
+ # second time, the modal closes successfully.
61
+ self.browser.find_by_xpath(".//button[@id='dismiss-modal-theme']").click()
62
+ self.browser.find_by_xpath(".//button[@id='dismiss-modal-theme']").click()
61
63
 
62
64
  # Validate Modal closes when cancel button clicked
63
- self.browser.find_by_xpath(".//button[@id='dismiss-modal-theme']").click()
64
- self.assertFalse(theme_modal[0].visible)
65
+ self.assertTrue(theme_modal[0].is_not_visible(wait_time=5))
@@ -10,6 +10,9 @@ from nautobot.core.settings_funcs import parse_redis_connection
10
10
 
11
11
  ALLOWED_HOSTS = ["nautobot.example.com"]
12
12
 
13
+ # Do *not* send anonymized install metrics when migration or post_upgrade management commands are run while testing
14
+ INSTALLATION_METRICS_ENABLED = False
15
+
13
16
  # Discover test jobs from within the Nautobot source code
14
17
  JOBS_ROOT = os.path.join(
15
18
  os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "extras", "test_jobs"
@@ -0,0 +1,4 @@
1
+ from nautobot.core.tests.nautobot_config import * # noqa: F403
2
+
3
+ # Do not enable example apps
4
+ PLUGINS = []