nautobot 3.0.0a3__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 (388) hide show
  1. nautobot/apps/choices.py +4 -0
  2. nautobot/apps/ui.py +4 -0
  3. nautobot/apps/utils.py +8 -0
  4. nautobot/circuits/tests/integration/test_circuits_bulk_operations.py +0 -3
  5. nautobot/circuits/views.py +6 -2
  6. nautobot/core/api/serializers.py +1 -1
  7. nautobot/core/api/urls.py +1 -0
  8. nautobot/core/api/views.py +4 -0
  9. nautobot/core/choices.py +1 -1
  10. nautobot/core/cli/bootstrap_v3_to_v5.py +36 -13
  11. nautobot/core/cli/migrate_deprecated_templates.py +36 -9
  12. nautobot/core/filters.py +4 -0
  13. nautobot/core/forms/__init__.py +2 -0
  14. nautobot/core/forms/widgets.py +21 -2
  15. nautobot/core/jobs/__init__.py +56 -0
  16. nautobot/core/management/commands/generate_test_data.py +3 -3
  17. nautobot/core/models/__init__.py +11 -0
  18. nautobot/core/models/utils.py +1 -1
  19. nautobot/core/settings.py +17 -7
  20. nautobot/core/settings.yaml +4 -26
  21. nautobot/core/templates/admin/base.html +1 -2
  22. nautobot/core/templates/admin/change_list.html +9 -12
  23. nautobot/core/templates/base_django.html +1 -2
  24. nautobot/core/templates/components/panel/header_extra_content_table.html +1 -1
  25. nautobot/core/templates/components/tab/content_wrapper.html +4 -4
  26. nautobot/core/templates/echarts/echarts.html +21 -8
  27. nautobot/core/templates/generic/object_bulk_create.html +2 -2
  28. nautobot/core/templates/generic/object_bulk_delete.html +1 -1
  29. nautobot/core/templates/generic/object_bulk_edit.html +1 -1
  30. nautobot/core/templates/generic/object_bulk_import.html +1 -1
  31. nautobot/core/templates/generic/object_delete.html +1 -1
  32. nautobot/core/templates/generic/object_detail.html +1 -1
  33. nautobot/core/templates/generic/object_edit.html +1 -1
  34. nautobot/core/templates/generic/object_retrieve.html +2 -2
  35. nautobot/core/templates/graphene/graphiql.html +0 -1
  36. nautobot/core/templates/inc/footer.html +3 -1
  37. nautobot/core/templates/inc/header.html +10 -0
  38. nautobot/core/templates/inc/media.html +14 -0
  39. nautobot/core/templates/inc/nav_menu.html +1 -8
  40. nautobot/core/templates/inc/object_details_advanced_panel.html +2 -2
  41. nautobot/core/templates/nautobot_config.py.j2 +0 -6
  42. nautobot/core/templates/rest_framework/api.html +103 -2
  43. nautobot/core/templates/utilities/templatetags/filter_form_drawer.html +33 -0
  44. nautobot/core/templates/utilities/theme_preview.html +3 -0
  45. nautobot/core/templates/widgets/number_input_with_choices.html +44 -0
  46. nautobot/core/templatetags/helpers.py +24 -12
  47. nautobot/core/testing/integration.py +24 -13
  48. nautobot/core/testing/utils.py +18 -4
  49. nautobot/core/testing/views.py +104 -17
  50. nautobot/core/tests/integration/test_filters.py +48 -11
  51. nautobot/core/tests/integration/test_theme.py +22 -21
  52. nautobot/core/tests/nautobot_config.py +3 -0
  53. nautobot/core/tests/runner.py +1 -2
  54. nautobot/core/tests/test_breadcrumbs.py +21 -21
  55. nautobot/core/tests/test_jobs.py +73 -6
  56. nautobot/core/tests/test_renderers.py +59 -0
  57. nautobot/core/tests/test_settings_schema.py +1 -0
  58. nautobot/core/tests/test_templatetags_helpers.py +9 -0
  59. nautobot/core/tests/test_titles.py +0 -16
  60. nautobot/core/tests/test_ui.py +122 -3
  61. nautobot/core/tests/test_utils.py +41 -1
  62. nautobot/core/ui/breadcrumbs.py +68 -17
  63. nautobot/core/ui/bulk_buttons.py +1 -1
  64. nautobot/core/ui/choices.py +49 -65
  65. nautobot/core/ui/echarts.py +15 -20
  66. nautobot/core/ui/object_detail.py +54 -46
  67. nautobot/core/ui/titles.py +3 -6
  68. nautobot/core/urls.py +8 -8
  69. nautobot/core/utils/filtering.py +11 -1
  70. nautobot/core/utils/lookup.py +46 -0
  71. nautobot/core/views/mixins.py +31 -20
  72. nautobot/core/views/renderers.py +2 -3
  73. nautobot/data_validation/migrations/0002_data_migration_from_app.py +3 -2
  74. nautobot/dcim/api/serializers.py +3 -0
  75. nautobot/dcim/choices.py +49 -0
  76. nautobot/dcim/constants.py +7 -0
  77. nautobot/dcim/factory.py +1 -1
  78. nautobot/dcim/filters.py +13 -1
  79. nautobot/dcim/forms.py +89 -3
  80. nautobot/dcim/migrations/0075_interface_duplex_interface_speed_and_more.py +32 -0
  81. nautobot/dcim/migrations/{0075_add_deviceclusterassignment.py → 0076_add_deviceclusterassignment.py} +1 -1
  82. nautobot/dcim/migrations/{0076_device_cluster_to_clusters_data_migration.py → 0077_device_cluster_to_clusters_data_migration.py} +1 -1
  83. nautobot/dcim/migrations/{0077_remove_device_cluster.py → 0078_remove_device_cluster.py} +1 -1
  84. nautobot/dcim/migrations/{0078_remove_device_location_tenant_name_uniqueness.py → 0079_remove_device_location_tenant_name_uniqueness.py} +1 -1
  85. nautobot/dcim/migrations/{0079_device_name_data_migration.py → 0080_device_name_data_migration.py} +1 -1
  86. nautobot/dcim/migrations/0081_alter_device_device_redundancy_group_priority_and_more.py +25 -0
  87. nautobot/dcim/models/device_component_templates.py +33 -1
  88. nautobot/dcim/models/device_components.py +22 -1
  89. nautobot/dcim/models/devices.py +17 -4
  90. nautobot/dcim/tables/devices.py +15 -0
  91. nautobot/dcim/tables/devicetypes.py +8 -1
  92. nautobot/dcim/tables/racks.py +0 -2
  93. nautobot/dcim/tables/template_code.py +1 -1
  94. nautobot/dcim/templates/dcim/cable_trace.html +0 -2
  95. nautobot/dcim/templates/dcim/consoleport.html +1 -1
  96. nautobot/dcim/templates/dcim/consoleserverport.html +1 -1
  97. nautobot/dcim/templates/dcim/devicebay.html +1 -1
  98. nautobot/dcim/templates/dcim/frontport.html +1 -1
  99. nautobot/dcim/templates/dcim/inc/devicetype_component_table.html +1 -1
  100. nautobot/dcim/templates/dcim/inc/moduletype_component_table.html +1 -1
  101. nautobot/dcim/templates/dcim/inc/rack_elevation.html +1 -1
  102. nautobot/dcim/templates/dcim/interface.html +9 -1
  103. nautobot/dcim/templates/dcim/interface_edit.html +2 -0
  104. nautobot/dcim/templates/dcim/inventoryitem.html +1 -1
  105. nautobot/dcim/templates/dcim/module_consoleports.html +1 -1
  106. nautobot/dcim/templates/dcim/module_consoleserverports.html +1 -1
  107. nautobot/dcim/templates/dcim/module_frontports.html +1 -1
  108. nautobot/dcim/templates/dcim/module_interfaces.html +1 -1
  109. nautobot/dcim/templates/dcim/module_modulebays.html +1 -1
  110. nautobot/dcim/templates/dcim/module_poweroutlets.html +1 -1
  111. nautobot/dcim/templates/dcim/module_powerports.html +1 -1
  112. nautobot/dcim/templates/dcim/module_rearports.html +1 -1
  113. nautobot/dcim/templates/dcim/moduletype_list.html +2 -2
  114. nautobot/dcim/templates/dcim/poweroutlet.html +1 -1
  115. nautobot/dcim/templates/dcim/powerport.html +1 -1
  116. nautobot/dcim/templates/dcim/rack_elevation_list.html +1 -1
  117. nautobot/dcim/templates/dcim/rack_retrieve.html +0 -11
  118. nautobot/dcim/templates/dcim/rearport.html +1 -1
  119. nautobot/dcim/templates/dcim/trace/cable.html +1 -1
  120. nautobot/dcim/templates/dcim/virtualchassis_update.html +1 -1
  121. nautobot/dcim/tests/integration/test_controller.py +3 -6
  122. nautobot/dcim/tests/integration/test_controller_managed_device_group.py +1 -5
  123. nautobot/dcim/tests/integration/test_create_device.py +0 -2
  124. nautobot/dcim/tests/integration/test_device_bulk_operations.py +1 -3
  125. nautobot/dcim/tests/integration/test_fileinputpicker.py +6 -10
  126. nautobot/dcim/tests/integration/test_location_bulk_operations.py +0 -2
  127. nautobot/dcim/tests/integration/test_module_bay_position.py +3 -4
  128. nautobot/dcim/tests/test_api.py +186 -6
  129. nautobot/dcim/tests/test_filters.py +43 -1
  130. nautobot/dcim/tests/test_forms.py +110 -8
  131. nautobot/dcim/tests/test_graphql.py +44 -1
  132. nautobot/dcim/tests/test_models.py +265 -0
  133. nautobot/dcim/tests/test_tables.py +160 -0
  134. nautobot/dcim/tests/test_views.py +69 -7
  135. nautobot/dcim/views.py +232 -126
  136. nautobot/extras/api/views.py +51 -44
  137. nautobot/extras/datasources/git.py +3 -1
  138. nautobot/extras/filters.py +19 -2
  139. nautobot/extras/forms/forms.py +9 -2
  140. nautobot/extras/jobs.py +2 -0
  141. nautobot/extras/jobs_ui.py +4 -3
  142. nautobot/extras/management/__init__.py +2 -0
  143. nautobot/extras/management/commands/refresh_dynamic_group_member_caches.py +4 -1
  144. nautobot/extras/migrations/0131_configcontext_device_families.py +18 -0
  145. nautobot/extras/models/approvals.py +11 -1
  146. nautobot/extras/models/change_logging.py +4 -0
  147. nautobot/extras/models/jobs.py +1 -3
  148. nautobot/extras/models/models.py +10 -2
  149. nautobot/extras/plugins/marketplace_manifest.yml +49 -1
  150. nautobot/extras/plugins/views.py +0 -5
  151. nautobot/extras/querysets.py +8 -0
  152. nautobot/extras/tables.py +12 -0
  153. nautobot/extras/templates/django_ajax_tables/ajax_wrapper.html +2 -0
  154. nautobot/extras/templates/extras/configcontext_update.html +1 -0
  155. nautobot/extras/templates/extras/dynamicgroup_update.html +1 -1
  156. nautobot/extras/templates/extras/objectchange_retrieve.html +0 -2
  157. nautobot/extras/templates/extras/plugin_detail.html +3 -3
  158. nautobot/extras/templates/extras/secret_create.html +1 -1
  159. nautobot/extras/tests/integration/test_computedfields.py +8 -9
  160. nautobot/extras/tests/integration/test_customfields.py +1 -3
  161. nautobot/extras/tests/integration/test_dynamicgroups.py +7 -8
  162. nautobot/extras/tests/integration/test_relationships.py +0 -2
  163. nautobot/extras/tests/test_api.py +63 -0
  164. nautobot/extras/tests/test_changelog.py +24 -2
  165. nautobot/extras/tests/test_filters.py +36 -3
  166. nautobot/extras/tests/test_models.py +38 -2
  167. nautobot/extras/tests/test_utils.py +3 -4
  168. nautobot/extras/tests/test_views.py +22 -83
  169. nautobot/extras/urls.py +0 -14
  170. nautobot/extras/views.py +83 -52
  171. nautobot/ipam/filters.py +26 -0
  172. nautobot/ipam/tables.py +6 -0
  173. nautobot/ipam/templates/ipam/namespace_ip_addresses.html +1 -1
  174. nautobot/ipam/templates/ipam/namespace_prefixes.html +1 -1
  175. nautobot/ipam/templates/ipam/namespace_vrfs.html +1 -1
  176. nautobot/ipam/tests/test_filters.py +26 -1
  177. nautobot/ipam/tests/test_models.py +1 -1
  178. nautobot/ipam/views.py +9 -7
  179. nautobot/load_balancers/__init__.py +0 -0
  180. nautobot/load_balancers/api/__init__.py +1 -0
  181. nautobot/load_balancers/api/serializers.py +75 -0
  182. nautobot/load_balancers/api/urls.py +23 -0
  183. nautobot/load_balancers/api/views.py +61 -0
  184. nautobot/load_balancers/apps.py +17 -0
  185. nautobot/load_balancers/choices.py +167 -0
  186. nautobot/load_balancers/filters.py +225 -0
  187. nautobot/load_balancers/forms.py +532 -0
  188. nautobot/load_balancers/management/commands/__init__.py +0 -0
  189. nautobot/load_balancers/management/commands/generate_load_balancer_models_test_data.py +38 -0
  190. nautobot/load_balancers/migrations/0001_initial.py +465 -0
  191. nautobot/load_balancers/migrations/0002_create_default_statuses_pool_members.py +31 -0
  192. nautobot/load_balancers/migrations/__init__.py +0 -0
  193. nautobot/load_balancers/models.py +423 -0
  194. nautobot/load_balancers/navigation.py +80 -0
  195. nautobot/load_balancers/tables.py +255 -0
  196. nautobot/load_balancers/tests/__init__.py +474 -0
  197. nautobot/load_balancers/tests/test_api.py +353 -0
  198. nautobot/load_balancers/tests/test_filters.py +134 -0
  199. nautobot/load_balancers/tests/test_forms.py +266 -0
  200. nautobot/load_balancers/tests/test_models.py +195 -0
  201. nautobot/load_balancers/tests/test_views.py +229 -0
  202. nautobot/load_balancers/urls.py +17 -0
  203. nautobot/load_balancers/views.py +248 -0
  204. nautobot/project-static/dist/css/github-dark.min.css +10 -0
  205. nautobot/project-static/dist/css/github.min.css +10 -0
  206. nautobot/project-static/dist/css/nautobot.css +1 -11
  207. nautobot/project-static/dist/css/nautobot.css.map +1 -1
  208. nautobot/project-static/dist/js/libraries.js +1 -1
  209. nautobot/project-static/dist/js/libraries.js.map +1 -1
  210. nautobot/project-static/dist/js/nautobot.js +1 -1
  211. nautobot/project-static/dist/js/nautobot.js.map +1 -1
  212. nautobot/project-static/js/forms.js +13 -0
  213. nautobot/project-static/nautobot-icons/bus-globe.svg +3 -0
  214. nautobot/project-static/nautobot-icons/bus-shield-check.svg +3 -0
  215. nautobot/project-static/nautobot-icons/bus-shield.svg +3 -0
  216. nautobot/ui/package-lock.json +87 -4
  217. nautobot/ui/package.json +2 -1
  218. nautobot/ui/src/js/nautobot.js +0 -1
  219. nautobot/ui/src/js/select2.js +53 -2
  220. nautobot/ui/src/scss/nautobot.scss +51 -2
  221. nautobot/ui/webpack.config.js +13 -0
  222. nautobot/users/templates/users/preferences.html +11 -2
  223. nautobot/virtualization/filters.py +6 -1
  224. nautobot/virtualization/tests/test_filters.py +10 -1
  225. nautobot/virtualization/tests/test_models.py +1 -0
  226. nautobot/virtualization/views.py +4 -1
  227. nautobot/vpn/factory.py +25 -15
  228. nautobot/vpn/filters.py +1 -0
  229. nautobot/vpn/forms.py +1 -0
  230. nautobot/vpn/migrations/0001_initial.py +1 -1
  231. nautobot/vpn/models.py +16 -8
  232. nautobot/vpn/tables.py +5 -2
  233. nautobot/vpn/tests/test_api.py +0 -5
  234. nautobot/vpn/tests/test_forms.py +1 -2
  235. nautobot/vpn/tests/test_models.py +57 -7
  236. nautobot/vpn/tests/test_views.py +22 -3
  237. nautobot/vpn/views.py +78 -20
  238. {nautobot-3.0.0a3.dist-info → nautobot-3.0.0rc1.dist-info}/METADATA +4 -4
  239. {nautobot-3.0.0a3.dist-info → nautobot-3.0.0rc1.dist-info}/RECORD +243 -352
  240. nautobot/circuits/templates/circuits/circuit.html +0 -2
  241. nautobot/circuits/templates/circuits/circuit_edit.html +0 -2
  242. nautobot/circuits/templates/circuits/circuit_retrieve.html +0 -2
  243. nautobot/circuits/templates/circuits/circuit_update.html +0 -1
  244. nautobot/circuits/templates/circuits/circuittermination.html +0 -2
  245. nautobot/circuits/templates/circuits/circuittermination_edit.html +0 -2
  246. nautobot/circuits/templates/circuits/circuittermination_retrieve.html +0 -2
  247. nautobot/circuits/templates/circuits/circuittermination_update.html +0 -1
  248. nautobot/circuits/templates/circuits/circuittype.html +0 -2
  249. nautobot/circuits/templates/circuits/circuittype_retrieve.html +0 -2
  250. nautobot/circuits/templates/circuits/inc/circuit_termination.html +0 -85
  251. nautobot/circuits/templates/circuits/provider.html +0 -2
  252. nautobot/circuits/templates/circuits/provider_edit.html +0 -2
  253. nautobot/circuits/templates/circuits/provider_retrieve.html +0 -1
  254. nautobot/circuits/templates/circuits/provider_update.html +0 -1
  255. nautobot/circuits/templates/circuits/providernetwork.html +0 -2
  256. nautobot/circuits/templates/circuits/providernetwork_retrieve.html +0 -2
  257. nautobot/cloud/templates/cloud/cloudaccount_retrieve.html +0 -2
  258. nautobot/cloud/templates/cloud/cloudnetwork_retrieve.html +0 -2
  259. nautobot/cloud/templates/cloud/cloudresourcetype_retrieve.html +0 -2
  260. nautobot/cloud/templates/cloud/cloudservice_retrieve.html +0 -2
  261. nautobot/core/templates/buttons/import.html +0 -9
  262. nautobot/data_validation/templates/data_validation/datacompliance_retrieve.html +0 -1
  263. nautobot/dcim/templates/dcim/cable.html +0 -2
  264. nautobot/dcim/templates/dcim/cable_edit.html +0 -2
  265. nautobot/dcim/templates/dcim/controller/base.html +0 -2
  266. nautobot/dcim/templates/dcim/controller_retrieve.html +0 -2
  267. nautobot/dcim/templates/dcim/controller_wirelessnetworks.html +0 -2
  268. nautobot/dcim/templates/dcim/controllermanageddevicegroup_retrieve.html +0 -2
  269. nautobot/dcim/templates/dcim/device/base.html +0 -2
  270. nautobot/dcim/templates/dcim/device/consoleports.html +0 -2
  271. nautobot/dcim/templates/dcim/device/consoleserverports.html +0 -2
  272. nautobot/dcim/templates/dcim/device/devicebays.html +0 -2
  273. nautobot/dcim/templates/dcim/device/frontports.html +0 -2
  274. nautobot/dcim/templates/dcim/device/interfaces.html +0 -2
  275. nautobot/dcim/templates/dcim/device/inventory.html +0 -2
  276. nautobot/dcim/templates/dcim/device/modulebays.html +0 -2
  277. nautobot/dcim/templates/dcim/device/poweroutlets.html +0 -2
  278. nautobot/dcim/templates/dcim/device/powerports.html +0 -2
  279. nautobot/dcim/templates/dcim/device/rearports.html +0 -2
  280. nautobot/dcim/templates/dcim/device/wireless.html +0 -2
  281. nautobot/dcim/templates/dcim/device_component.html +0 -2
  282. nautobot/dcim/templates/dcim/device_edit.html +0 -2
  283. nautobot/dcim/templates/dcim/devicefamily_retrieve.html +0 -2
  284. nautobot/dcim/templates/dcim/deviceredundancygroup_retrieve.html +0 -2
  285. nautobot/dcim/templates/dcim/devicetype.html +0 -2
  286. nautobot/dcim/templates/dcim/devicetype_edit.html +0 -2
  287. nautobot/dcim/templates/dcim/devicetype_retrieve.html +0 -2
  288. nautobot/dcim/templates/dcim/inc/device_napalm_tabs.html +0 -1
  289. nautobot/dcim/templates/dcim/interfaceredundancygroup_retrieve.html +0 -2
  290. nautobot/dcim/templates/dcim/location.html +0 -2
  291. nautobot/dcim/templates/dcim/location_edit.html +0 -2
  292. nautobot/dcim/templates/dcim/location_retrieve.html +0 -2
  293. nautobot/dcim/templates/dcim/locationtype.html +0 -2
  294. nautobot/dcim/templates/dcim/locationtype_retrieve.html +0 -2
  295. nautobot/dcim/templates/dcim/manufacturer.html +0 -2
  296. nautobot/dcim/templates/dcim/modulebay_retrieve.html +0 -1
  297. nautobot/dcim/templates/dcim/platform.html +0 -2
  298. nautobot/dcim/templates/dcim/powerfeed.html +0 -2
  299. nautobot/dcim/templates/dcim/powerfeed_retrieve.html +0 -2
  300. nautobot/dcim/templates/dcim/powerpanel.html +0 -2
  301. nautobot/dcim/templates/dcim/powerpanel_edit.html +0 -2
  302. nautobot/dcim/templates/dcim/powerpanel_retrieve.html +0 -2
  303. nautobot/dcim/templates/dcim/rack.html +0 -2
  304. nautobot/dcim/templates/dcim/rack_edit.html +0 -2
  305. nautobot/dcim/templates/dcim/rackgroup.html +0 -2
  306. nautobot/dcim/templates/dcim/rackreservation.html +0 -2
  307. nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +0 -2
  308. nautobot/dcim/templates/dcim/softwareversion_retrieve.html +0 -2
  309. nautobot/dcim/templates/dcim/virtualchassis.html +0 -2
  310. nautobot/dcim/templates/dcim/virtualchassis_add.html +0 -2
  311. nautobot/dcim/templates/dcim/virtualchassis_edit.html +0 -2
  312. nautobot/dcim/templates/dcim/virtualchassis_retrieve.html +0 -2
  313. nautobot/dcim/templates/dcim/virtualdevicecontext_retrieve.html +0 -2
  314. nautobot/dcim/ui.py +0 -29
  315. nautobot/extras/templates/extras/computedfield.html +0 -2
  316. nautobot/extras/templates/extras/computedfield_retrieve.html +0 -2
  317. nautobot/extras/templates/extras/configcontext.html +0 -2
  318. nautobot/extras/templates/extras/configcontext_edit.html +0 -2
  319. nautobot/extras/templates/extras/configcontext_retrieve.html +0 -2
  320. nautobot/extras/templates/extras/configcontextschema.html +0 -2
  321. nautobot/extras/templates/extras/configcontextschema_edit.html +0 -2
  322. nautobot/extras/templates/extras/contact_retrieve.html +0 -2
  323. nautobot/extras/templates/extras/customfield.html +0 -2
  324. nautobot/extras/templates/extras/customfield_edit.html +0 -2
  325. nautobot/extras/templates/extras/customfield_retrieve.html +0 -2
  326. nautobot/extras/templates/extras/customlink.html +0 -2
  327. nautobot/extras/templates/extras/dynamicgroup.html +0 -2
  328. nautobot/extras/templates/extras/dynamicgroup_edit.html +0 -2
  329. nautobot/extras/templates/extras/exporttemplate.html +0 -2
  330. nautobot/extras/templates/extras/gitrepository.html +0 -2
  331. nautobot/extras/templates/extras/gitrepository_object_edit.html +0 -2
  332. nautobot/extras/templates/extras/graphqlquery.html +0 -2
  333. nautobot/extras/templates/extras/graphqlquery_list.html +0 -1
  334. nautobot/extras/templates/extras/graphqlquery_retrieve.html +0 -2
  335. nautobot/extras/templates/extras/job_detail.html +0 -2
  336. nautobot/extras/templates/extras/jobbutton_retrieve.html +0 -2
  337. nautobot/extras/templates/extras/jobhook.html +0 -2
  338. nautobot/extras/templates/extras/jobqueue_retrieve.html +0 -2
  339. nautobot/extras/templates/extras/jobresult.html +0 -2
  340. nautobot/extras/templates/extras/metadatatype_retrieve.html +0 -2
  341. nautobot/extras/templates/extras/note.html +0 -2
  342. nautobot/extras/templates/extras/note_retrieve.html +0 -1
  343. nautobot/extras/templates/extras/object_changelog.html +0 -2
  344. nautobot/extras/templates/extras/object_notes.html +0 -2
  345. nautobot/extras/templates/extras/objectchange.html +0 -2
  346. nautobot/extras/templates/extras/objectchange_list.html +0 -3
  347. nautobot/extras/templates/extras/relationship.html +0 -1
  348. nautobot/extras/templates/extras/secret.html +0 -1
  349. nautobot/extras/templates/extras/secret_edit.html +0 -1
  350. nautobot/extras/templates/extras/secretsgroup.html +0 -2
  351. nautobot/extras/templates/extras/secretsgroup_edit.html +0 -2
  352. nautobot/extras/templates/extras/secretsgroup_retrieve.html +0 -2
  353. nautobot/extras/templates/extras/status.html +0 -2
  354. nautobot/extras/templates/extras/tag.html +0 -2
  355. nautobot/extras/templates/extras/tag_edit.html +0 -2
  356. nautobot/extras/templates/extras/tag_retrieve.html +0 -2
  357. nautobot/extras/templates/extras/team_retrieve.html +0 -2
  358. nautobot/ipam/templates/ipam/namespace_retrieve.html +0 -1
  359. nautobot/ipam/templates/ipam/prefix.html +0 -2
  360. nautobot/ipam/templates/ipam/prefix_edit.html +0 -1
  361. nautobot/ipam/templates/ipam/prefix_retrieve.html +0 -2
  362. nautobot/ipam/templates/ipam/rir.html +0 -2
  363. nautobot/ipam/templates/ipam/routetarget.html +0 -1
  364. nautobot/ipam/templates/ipam/service.html +0 -2
  365. nautobot/ipam/templates/ipam/service_edit.html +0 -2
  366. nautobot/ipam/templates/ipam/service_retrieve.html +0 -2
  367. nautobot/ipam/templates/ipam/vlan.html +0 -2
  368. nautobot/ipam/templates/ipam/vlan_edit.html +0 -2
  369. nautobot/ipam/templates/ipam/vlan_retrieve.html +0 -2
  370. nautobot/ipam/templates/ipam/vlangroup.html +0 -2
  371. nautobot/ipam/templates/ipam/vrf.html +0 -1
  372. nautobot/tenancy/templates/tenancy/tenant.html +0 -2
  373. nautobot/tenancy/templates/tenancy/tenant_edit.html +0 -2
  374. nautobot/tenancy/templates/tenancy/tenantgroup.html +0 -2
  375. nautobot/tenancy/templates/tenancy/tenantgroup_retrieve.html +0 -1
  376. nautobot/virtualization/templates/virtualization/clustergroup.html +0 -2
  377. nautobot/virtualization/templates/virtualization/clustertype.html +0 -2
  378. nautobot/virtualization/templates/virtualization/virtualmachine.html +0 -2
  379. nautobot/virtualization/templates/virtualization/virtualmachine_edit.html +0 -2
  380. nautobot/virtualization/templates/virtualization/virtualmachine_retrieve.html +0 -2
  381. nautobot/vpn/templates/vpn/vpnprofile.html +0 -2
  382. nautobot/wireless/templates/wireless/radioprofile_retrieve.html +0 -2
  383. nautobot/wireless/templates/wireless/supporteddatarate_retrieve.html +0 -2
  384. nautobot/wireless/templates/wireless/wirelessnetwork_retrieve.html +0 -2
  385. {nautobot-3.0.0a3.dist-info → nautobot-3.0.0rc1.dist-info}/LICENSE.txt +0 -0
  386. {nautobot-3.0.0a3.dist-info → nautobot-3.0.0rc1.dist-info}/NOTICE +0 -0
  387. {nautobot-3.0.0a3.dist-info → nautobot-3.0.0rc1.dist-info}/WHEEL +0 -0
  388. {nautobot-3.0.0a3.dist-info → nautobot-3.0.0rc1.dist-info}/entry_points.txt +0 -0
@@ -1139,6 +1139,9 @@ This:
1139
1139
  <div class="icon-preview"><img alt="power icon" src="{% static 'nautobot-icons/battery-3.svg' %}">battery-3</div>
1140
1140
  <div class="icon-preview"><img alt="branch icon" src="{% static 'nautobot-icons/branch.svg' %}">branch</div>
1141
1141
  <div class="icon-preview"><img alt="briefcase-2 icon" src="{% static 'nautobot-icons/briefcase-2.svg' %}">briefcase-2</div>
1142
+ <div class="icon-preview"><img alt="bus-globe icon" src="{% static 'nautobot-icons/bus-globe.svg' %}">bus-globe</div>
1143
+ <div class="icon-preview"><img alt="bus-shield icon" src="{% static 'nautobot-icons/bus-shield.svg' %}">bus-shield</div>
1144
+ <div class="icon-preview"><img alt="bus-shield-check icon" src="{% static 'nautobot-icons/bus-shield-check.svg' %}">bus-shield-check</div>
1142
1145
  <div class="icon-preview"><img alt="cable-data icon" src="{% static 'nautobot-icons/cable-data.svg' %}">cable-data</div>
1143
1146
  <div class="icon-preview"><img alt="cable-data-2 icon" src="{% static 'nautobot-icons/cable-data-2.svg' %}">cable-data-2</div>
1144
1147
  <div class="icon-preview"><img alt="cast icon" src="{% static 'nautobot-icons/cast.svg' %}">cast</div>
@@ -0,0 +1,44 @@
1
+ <div class="input-group">
2
+ {% include 'django/forms/widgets/number.html' %}
3
+ {% if widget.choices %}
4
+ <span class="input-group-btn">
5
+ <button type="button" class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown">
6
+ <span class="mdi mdi-chevron-down"></span>
7
+ </button>
8
+ <ul class="dropdown-menu dropdown-menu-end">
9
+ {% for value, label in widget.choices %}
10
+ <li><a href="#" data-name="{{ widget.name }}" data-value="{{ value }}" class="set_value dropdown-item">{{ label }}</a></li>
11
+ {% endfor %}
12
+ </ul>
13
+ </span>
14
+ {% endif %}
15
+ </div>
16
+
17
+ {% if widget.choices %}
18
+ <script type="text/javascript">
19
+ (function() {
20
+ if (window.__nbNumberWithSelectWidgetBound) return;
21
+ window.__nbNumberWithSelectWidgetBound = true;
22
+ function bindNumberWithSelectHandler() {
23
+ document.addEventListener("click", function(e) {
24
+ if (!e.target) return;
25
+ var link = e.target.closest && e.target.closest("a.set_value");
26
+ if (!link) return;
27
+ e.preventDefault();
28
+ var container = link.closest(".input-group");
29
+ var name = link.getAttribute("data-name");
30
+ var value = link.getAttribute("data-value");
31
+ var input = container && name ? container.querySelector('input[name="' + name + '"]') : null;
32
+ if (input) {
33
+ input.value = value;
34
+ }
35
+ });
36
+ }
37
+ if (document.readyState === 'loading') {
38
+ document.addEventListener('DOMContentLoaded', bindNumberWithSelectHandler);
39
+ } else {
40
+ bindNumberWithSelectHandler();
41
+ }
42
+ })();
43
+ </script>
44
+ {% endif %}
@@ -126,23 +126,29 @@ def placeholder(value):
126
126
 
127
127
  @library.filter()
128
128
  @register.filter()
129
- def pre_tag(value):
129
+ def pre_tag(value, format_empty_value=True):
130
130
  """Render a value within `<pre></pre>` tags to enable formatting.
131
131
 
132
132
  Args:
133
133
  value (any): Input value, can be any variable.
134
+ format_empty_value (bool): Whether format empty value or render placeholder.
134
135
 
135
136
  Returns:
136
- (str): Value wrapped in `<pre></pre>` tags.
137
+ (str): Value wrapped in `<pre></pre>` tags or placeholder if None or format_empty_values=False and empty
137
138
 
138
139
  Example:
139
140
  >>> pre_tag("")
140
141
  '<pre></pre>'
141
142
  >>> pre_tag("hello")
142
143
  '<pre>hello</pre>'
144
+ >>> pre_tag("", format_empty_value=False)
145
+ '<span class="text-secondary">&mdash;</span>'
143
146
  """
144
- if value is not None:
147
+ if format_empty_value and value is not None:
148
+ return format_html("<pre>{}</pre>", value)
149
+ elif value:
145
150
  return format_html("<pre>{}</pre>", value)
151
+
146
152
  return HTML_NONE
147
153
 
148
154
 
@@ -395,17 +401,19 @@ def humanize_speed(speed):
395
401
  1544 => "1.544 Mbps"
396
402
  100000 => "100 Mbps"
397
403
  10000000 => "10 Gbps"
404
+ 1000000000 => "1 Tbps"
405
+ 1600000000 => "1.6 Tbps"
406
+ 10000000000 => "10 Tbps"
398
407
  """
399
408
  if not speed:
400
409
  return ""
401
- if speed >= 1000000000 and speed % 1000000000 == 0:
402
- return f"{int(speed / 1000000000)} Tbps"
403
- elif speed >= 1000000 and speed % 1000000 == 0:
404
- return f"{int(speed / 1000000)} Gbps"
405
- elif speed >= 1000 and speed % 1000 == 0:
406
- return f"{int(speed / 1000)} Mbps"
410
+
411
+ if speed >= 1000000000:
412
+ return f"{speed / 1000000000:g} Tbps"
413
+ elif speed >= 1000000:
414
+ return f"{speed / 1000000:g} Gbps"
407
415
  elif speed >= 1000:
408
- return f"{float(speed) / 1000} Mbps"
416
+ return f"{speed / 1000:g} Mbps"
409
417
  else:
410
418
  return f"{speed} Kbps"
411
419
 
@@ -507,7 +515,9 @@ def get_docs_url(model):
507
515
 
508
516
  Example:
509
517
  >>> get_docs_url(location_instance)
510
- "static/docs/models/dcim/location.html"
518
+ "static/docs/user-guide/core-data-model/dcim/location.html"
519
+ >>> get_docs_url(virtual_server_instance)
520
+ "static/docs/user-guide/core-data-model/load-balancers/virtualserver.html"
511
521
  >>> get_docs_url(example_model)
512
522
  "/docs/example-app/models/examplemodel.html"
513
523
  """
@@ -532,7 +542,9 @@ def get_docs_url(model):
532
542
  elif model._meta.app_label == "extras":
533
543
  path = f"docs/user-guide/platform-functionality/{model._meta.model_name}.html"
534
544
  else:
535
- path = f"docs/user-guide/core-data-model/{model._meta.app_label}/{model._meta.model_name}.html"
545
+ path = (
546
+ f"docs/user-guide/core-data-model/{model._meta.app_label.replace('_', '-')}/{model._meta.model_name}.html"
547
+ )
536
548
 
537
549
  # Check to see if documentation exists in any of the static paths.
538
550
  if find(path):
@@ -66,13 +66,14 @@ class ObjectsListMixin:
66
66
  """
67
67
  Click bulk delete from dropdown menu on bottom of the items table list.
68
68
  """
69
- self.browser.execute_script(
70
- "document.querySelector('#bulk-action-buttons button[type=\"submit\"]').scrollIntoView()"
71
- )
69
+ self.scroll_element_into_view(css='#bulk-action-buttons button[type="submit"]')
72
70
  self.browser.find_by_xpath(
73
71
  '//*[@id="bulk-action-buttons"]//button[@type="submit"]/following-sibling::button[1]'
74
72
  ).click()
75
- 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()
76
77
 
77
78
  def click_bulk_delete_all(self):
78
79
  """
@@ -110,7 +111,7 @@ class ObjectsListMixin:
110
111
  objects_table_container = self.browser.find_by_xpath('//*[@id="object_list_form"]')
111
112
  try:
112
113
  objects_table = objects_table_container.find_by_tag("tbody")
113
- return len(objects_table.find_by_tag("tr"))
114
+ return len(objects_table.find_by_xpath(".//tr[not(count(td[@colspan])=1)]"))
114
115
  except ElementDoesNotExist:
115
116
  return 0
116
117
 
@@ -238,7 +239,7 @@ class BulkOperationsMixin:
238
239
  button_text = self.browser.find_by_xpath('//button[@name="_confirm" and @type="submit"]').text
239
240
  self.assertIn(f"Delete these {expected_count}", button_text)
240
241
 
241
- 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
242
243
  self.assertIn(f"The following operation will delete {expected_count}", message_text)
243
244
 
244
245
  def assertIsBulkDeleteJob(self):
@@ -413,7 +414,7 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
413
414
  search_box_class = "select2-search select2-search--dropdown"
414
415
 
415
416
  self.browser.find_by_xpath(f"//select[@id='id_{field_name}']//following-sibling::span").click()
416
- self.browser.execute_script(f"""document.querySelector('#id_{field_name}').scrollIntoView()""")
417
+ self.scroll_element_into_view(css=f"#id_{field_name}")
417
418
  search_box = self.browser.find_by_xpath(f"//*[@class='{search_box_class}']//input", wait_time=5)
418
419
  for _ in search_box.first.type(value, slowly=True):
419
420
  pass
@@ -459,9 +460,7 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
459
460
  def click_button(self, query_selector):
460
461
  self.browser.is_element_present_by_css(query_selector, wait_time=5)
461
462
  # Button might be visible but on the edge and then impossible to click due to vertical/horizontal scrolls
462
- self.browser.execute_script(
463
- f"document.querySelector('{query_selector}').scrollIntoView({{ behavior: 'instant', block: 'start' }});"
464
- )
463
+ self.scroll_element_into_view(css=query_selector)
465
464
  # Scrolling may be asynchronous, wait until it's actually clickable.
466
465
  WebDriverWait(self.browser.driver, 30).until(element_to_be_clickable((By.CSS_SELECTOR, query_selector)))
467
466
  btn = self.browser.find_by_css(query_selector)
@@ -473,9 +472,7 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
473
472
  """
474
473
  self.browser.is_element_present_by_name(input_name, wait_time=5)
475
474
  element = self.browser.find_by_name(input_name)
476
- self.browser.execute_script(
477
- "arguments[0].scrollIntoView({ behavior: 'instant', block: 'start' });", element.first._element
478
- )
475
+ self.scroll_element_into_view(element=element)
479
476
  element.is_visible(wait_time=5)
480
477
  self.browser.execute_script("arguments[0].focus();", element.first._element)
481
478
  self.browser.fill(input_name, input_value)
@@ -486,6 +483,20 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
486
483
  self.login(self.user.username, self.password)
487
484
  self.logged_in = True
488
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
+
489
500
 
490
501
  class BulkOperationsTestCases:
491
502
  """
@@ -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,12 +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
- # TODO: some models (e.g. JobResult) intentionally do NOT display the full `.display` in the detail view,
196
- # but only use the `.name` or `str()`.
197
- # This check will always pass in the case where the Example App is installed, because of the banner
198
- # it adds, but can/should/may? fail otherwise.
199
- 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)))
200
201
 
201
202
  # If any Relationships are defined, they should appear in the response
202
203
  if self.relationships is not None:
@@ -366,18 +367,104 @@ class ViewTestCases:
366
367
 
367
368
  @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
368
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()
369
417
  instance = self._get_queryset().first()
370
- for url_name, required_permissions in self.custom_action_required_permissions.items():
371
- url = reverse(url_name, kwargs={"pk": instance.pk})
372
- self.assertHttpStatus(self.client.get(url), 403)
373
- for permission in required_permissions[:-1]:
374
- self.add_permissions(permission)
375
- self.assertHttpStatus(self.client.get(url), 403)
376
-
377
- self.add_permissions(required_permissions[-1])
378
- self.assertHttpStatus(self.client.get(url), 200)
379
- # delete the permissions here so that repetitive calls to add_permissions do not create duplicate permissions.
380
- 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))
381
468
 
382
469
  class GetObjectChangelogViewTestCase(ModelViewTestCase):
383
470
  """
@@ -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"
@@ -162,8 +162,7 @@ class NautobotTestRunner(DiscoverRunner):
162
162
  db_command = [*command, "--database", alias]
163
163
  call_command(*db_command)
164
164
 
165
- # Calculate membership for the dynamic groups that were generated by the factories/fixtures
166
- call_command("refresh_dynamic_group_member_caches")
165
+ call_command("post_upgrade")
167
166
 
168
167
  if self.parallel > 1:
169
168
  for index in range(self.parallel):