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

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

Potentially problematic release.


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

Files changed (420) hide show
  1. nautobot/apps/choices.py +0 -2
  2. nautobot/apps/filters.py +7 -9
  3. nautobot/apps/models.py +2 -2
  4. nautobot/apps/ui.py +9 -1
  5. nautobot/circuits/filters.py +3 -2
  6. nautobot/circuits/navigation.py +3 -2
  7. nautobot/circuits/templates/circuits/circuit.html +1 -1
  8. nautobot/circuits/templates/circuits/circuit_create.html +3 -3
  9. nautobot/circuits/templates/circuits/circuittermination.html +1 -1
  10. nautobot/circuits/templates/circuits/circuittermination_create.html +9 -24
  11. nautobot/circuits/templates/circuits/circuittype.html +1 -1
  12. nautobot/circuits/templates/circuits/inc/circuit_termination_cable_fragment.html +6 -6
  13. nautobot/circuits/templates/circuits/inc/speed_widget.html +12 -12
  14. nautobot/circuits/templates/circuits/providernetwork.html +1 -1
  15. nautobot/circuits/tests/integration/test_circuit.py +10 -13
  16. nautobot/cloud/filters.py +1 -1
  17. nautobot/cloud/navigation.py +3 -2
  18. nautobot/core/api/schema.py +1 -1
  19. nautobot/core/api/serializers.py +6 -1
  20. nautobot/core/api/urls.py +1 -0
  21. nautobot/core/api/views.py +8 -0
  22. nautobot/core/apps/__init__.py +11 -10
  23. nautobot/core/celery/__init__.py +3 -5
  24. nautobot/core/checks.py +46 -0
  25. nautobot/core/cli/bootstrap_v3_to_v5.py +70 -1
  26. nautobot/core/cli/migrate_deprecated_templates.py +200 -0
  27. nautobot/core/constants.py +3 -0
  28. nautobot/core/context_processors.py +9 -1
  29. nautobot/core/forms/forms.py +1 -1
  30. nautobot/core/jobs/__init__.py +6 -3
  31. nautobot/core/jobs/groups.py +31 -1
  32. nautobot/core/management/commands/generate_test_data.py +28 -9
  33. nautobot/core/models/generics.py +9 -1
  34. nautobot/core/models/tree_queries.py +10 -5
  35. nautobot/core/settings.py +18 -12
  36. nautobot/core/settings.yaml +13 -7
  37. nautobot/core/signals.py +12 -1
  38. nautobot/core/tables.py +13 -6
  39. nautobot/core/templates/40x.html +1 -1
  40. nautobot/core/templates/500.html +2 -2
  41. nautobot/core/templates/admin/config/config.html +12 -12
  42. nautobot/core/templates/admin/index.html +3 -3
  43. nautobot/core/templates/buttons/export.html +1 -1
  44. nautobot/core/templates/components/button/dropdown.html +5 -3
  45. nautobot/core/templates/components/panel/body_wrapper_generic_table.html +1 -1
  46. nautobot/core/templates/components/panel/panel.html +3 -3
  47. nautobot/core/templates/components/tab/content_wrapper.html +2 -3
  48. nautobot/core/templates/components/tab/label_wrapper_distinct_view.html +1 -1
  49. nautobot/core/templates/echarts/echarts.html +1 -1
  50. nautobot/core/templates/generic/object_bulk_add_component.html +2 -1
  51. nautobot/core/templates/generic/object_bulk_create.html +4 -3
  52. nautobot/core/templates/generic/object_bulk_destroy.html +3 -3
  53. nautobot/core/templates/generic/object_bulk_remove.html +2 -2
  54. nautobot/core/templates/generic/object_bulk_update.html +5 -4
  55. nautobot/core/templates/generic/object_create.html +5 -4
  56. nautobot/core/templates/generic/object_import.html +2 -1
  57. nautobot/core/templates/generic/object_list.html +12 -4
  58. nautobot/core/templates/generic/object_notes.html +5 -3
  59. nautobot/core/templates/generic/object_retrieve.html +2 -3
  60. nautobot/core/templates/graphene/graphiql.html +7 -7
  61. nautobot/core/templates/home.html +1 -1
  62. nautobot/core/templates/import_success.html +2 -1
  63. nautobot/core/templates/inc/computed_fields/panel_data.html +1 -1
  64. nautobot/core/templates/inc/created_updated.html +7 -3
  65. nautobot/core/templates/inc/custom_fields/panel_data.html +1 -1
  66. nautobot/core/templates/inc/form_static_field.html +6 -0
  67. nautobot/core/templates/inc/header.html +1 -1
  68. nautobot/core/templates/inc/image_attachments.html +2 -1
  69. nautobot/core/templates/inc/nav_menu.html +2 -1
  70. nautobot/core/templates/inc/search_panel.html +4 -4
  71. nautobot/core/templates/login.html +4 -2
  72. nautobot/core/templates/nautobot_config.py.j2 +6 -5
  73. nautobot/core/templates/redoc_ui.html +7 -0
  74. nautobot/core/templates/search.html +1 -1
  75. nautobot/core/templates/swagger_ui.html +17 -3
  76. nautobot/core/templates/system_jobs/import_objects.html +1 -2
  77. nautobot/core/templates/utilities/confirmation_form.html +2 -2
  78. nautobot/core/templates/utilities/obj_table.html +10 -2
  79. nautobot/core/templates/utilities/render_field.html +7 -7
  80. nautobot/core/templates/utilities/render_jinja2.html +2 -2
  81. nautobot/core/templates/utilities/templatetags/filter_form_drawer.html +4 -4
  82. nautobot/core/templates/utilities/theme_preview.html +16 -3
  83. nautobot/core/templates/widgets/selectwithdisabled_option.html +3 -1
  84. nautobot/core/templatetags/helpers.py +52 -6
  85. nautobot/core/testing/api.py +68 -9
  86. nautobot/core/testing/filters.py +0 -23
  87. nautobot/core/testing/integration.py +23 -10
  88. nautobot/core/testing/mixins.py +2 -0
  89. nautobot/core/testing/views.py +4 -0
  90. nautobot/core/tests/integration/test_app_home.py +34 -30
  91. nautobot/core/tests/integration/test_app_navbar.py +3 -0
  92. nautobot/core/tests/nautobot_config_without_example_apps.py +4 -0
  93. nautobot/core/tests/runner.py +9 -1
  94. nautobot/core/tests/test_api.py +5 -3
  95. nautobot/core/tests/test_breadcrumbs.py +6 -7
  96. nautobot/core/tests/test_checks.py +28 -0
  97. nautobot/core/tests/test_cli.py +40 -0
  98. nautobot/core/tests/test_config.py +2 -1
  99. nautobot/core/tests/test_forms.py +55 -13
  100. nautobot/core/tests/test_jobs.py +75 -1
  101. nautobot/core/tests/test_nautobot_server.py +2 -0
  102. nautobot/core/tests/test_navigations.py +76 -1
  103. nautobot/core/tests/test_patch_social_django.py +42 -0
  104. nautobot/core/tests/test_tables.py +3 -1
  105. nautobot/core/tests/test_templatetags_helpers.py +53 -13
  106. nautobot/core/tests/test_templatetags_ui_framework.py +4 -4
  107. nautobot/core/tests/test_tree_queries.py +14 -1
  108. nautobot/core/tests/test_ui.py +1 -1
  109. nautobot/core/tests/test_utils.py +31 -4
  110. nautobot/core/tests/test_views.py +159 -31
  111. nautobot/core/ui/breadcrumbs.py +2 -12
  112. nautobot/core/ui/choices.py +142 -10
  113. nautobot/core/ui/constants.py +76 -12
  114. nautobot/core/ui/object_detail.py +92 -12
  115. nautobot/core/urls.py +12 -1
  116. nautobot/core/utils/cache.py +2 -1
  117. nautobot/core/utils/filtering.py +17 -17
  118. nautobot/core/utils/lookup.py +3 -8
  119. nautobot/core/utils/module_loading.py +21 -0
  120. nautobot/core/utils/patch_social_django.py +128 -0
  121. nautobot/core/views/__init__.py +38 -1
  122. nautobot/core/views/generic.py +3 -3
  123. nautobot/core/views/mixins.py +15 -3
  124. nautobot/core/views/renderers.py +2 -0
  125. nautobot/core/views/viewsets.py +2 -1
  126. nautobot/data_validation/apps.py +1 -5
  127. nautobot/data_validation/custom_validators.py +4 -4
  128. nautobot/data_validation/filters.py +1 -1
  129. nautobot/data_validation/forms.py +40 -0
  130. nautobot/data_validation/migrations/0001_initial.py +0 -7
  131. nautobot/data_validation/migrations/0002_data_migration_from_app.py +0 -12
  132. nautobot/data_validation/models.py +16 -7
  133. nautobot/data_validation/navigation.py +8 -1
  134. nautobot/data_validation/tables.py +12 -5
  135. nautobot/data_validation/templates/data_validation/datacompliance_tab.html +1 -0
  136. nautobot/data_validation/templates/data_validation/device_constraints.html +61 -0
  137. nautobot/data_validation/tests/__init__.py +2 -2
  138. nautobot/data_validation/tests/migrations/test_migrations.py +83 -3
  139. nautobot/data_validation/tests/test_data_compliance_rules.py +12 -7
  140. nautobot/data_validation/tests/test_filters.py +8 -6
  141. nautobot/data_validation/tests/test_models.py +15 -0
  142. nautobot/data_validation/tests/test_views.py +190 -32
  143. nautobot/data_validation/urls.py +2 -5
  144. nautobot/data_validation/views.py +73 -40
  145. nautobot/dcim/api/serializers.py +0 -13
  146. nautobot/dcim/apps.py +4 -0
  147. nautobot/dcim/choices.py +16 -0
  148. nautobot/dcim/custom_validators.py +84 -0
  149. nautobot/dcim/filter_mixins.py +353 -4
  150. nautobot/dcim/{filters/__init__.py → filters.py} +2 -35
  151. nautobot/dcim/forms.py +1 -1
  152. nautobot/dcim/migrations/0078_remove_device_location_tenant_name_uniqueness.py +16 -0
  153. nautobot/dcim/migrations/0079_device_name_data_migration.py +59 -0
  154. nautobot/dcim/models/device_components.py +81 -68
  155. nautobot/dcim/models/devices.py +13 -16
  156. nautobot/dcim/navigation.py +7 -6
  157. nautobot/dcim/tables/devices.py +3 -0
  158. nautobot/dcim/tables/template_code.py +14 -14
  159. nautobot/dcim/templates/dcim/cable.html +2 -61
  160. nautobot/dcim/templates/dcim/cable_connect.html +28 -112
  161. nautobot/dcim/templates/dcim/cable_edit.html +2 -5
  162. nautobot/dcim/templates/dcim/cable_retrieve.html +61 -0
  163. nautobot/dcim/templates/dcim/cable_trace.html +1 -3
  164. nautobot/dcim/templates/dcim/cable_update.html +5 -0
  165. nautobot/dcim/templates/dcim/consoleport.html +6 -5
  166. nautobot/dcim/templates/dcim/consoleserverport.html +6 -5
  167. nautobot/dcim/templates/dcim/device/config.html +2 -2
  168. nautobot/dcim/templates/dcim/device/consoleports.html +1 -1
  169. nautobot/dcim/templates/dcim/device/consoleserverports.html +1 -1
  170. nautobot/dcim/templates/dcim/device/devicebays.html +1 -1
  171. nautobot/dcim/templates/dcim/device/frontports.html +1 -1
  172. nautobot/dcim/templates/dcim/device/interfaces.html +1 -1
  173. nautobot/dcim/templates/dcim/device/inventory.html +1 -1
  174. nautobot/dcim/templates/dcim/device/lldp_neighbors.html +1 -1
  175. nautobot/dcim/templates/dcim/device/modulebays.html +1 -1
  176. nautobot/dcim/templates/dcim/device/poweroutlets.html +1 -1
  177. nautobot/dcim/templates/dcim/device/powerports.html +1 -1
  178. nautobot/dcim/templates/dcim/device/rearports.html +1 -1
  179. nautobot/dcim/templates/dcim/device/status.html +8 -8
  180. nautobot/dcim/templates/dcim/device/wireless.html +1 -1
  181. nautobot/dcim/templates/dcim/device.html +1 -1
  182. nautobot/dcim/templates/dcim/device_component_add.html +2 -2
  183. nautobot/dcim/templates/dcim/device_create.html +5 -3
  184. nautobot/dcim/templates/dcim/device_interface_delete.html +1 -1
  185. nautobot/dcim/templates/dcim/device_list.html +73 -10
  186. nautobot/dcim/templates/dcim/devicebay_populate.html +2 -2
  187. nautobot/dcim/templates/dcim/devicetype.html +1 -1
  188. nautobot/dcim/templates/dcim/devicetype_component_add.html +2 -2
  189. nautobot/dcim/templates/dcim/footer_convert_to_contact_or_team_record.html +14 -0
  190. nautobot/dcim/templates/dcim/frontport.html +9 -8
  191. nautobot/dcim/templates/dcim/inc/edit_form_softwareversion_js.html +2 -2
  192. nautobot/dcim/templates/dcim/interface.html +26 -6
  193. nautobot/dcim/templates/dcim/interface_bulk_delete.html +1 -1
  194. nautobot/dcim/templates/dcim/inventoryitem_add.html +3 -1
  195. nautobot/dcim/templates/dcim/inventoryitem_bulk_delete.html +1 -1
  196. nautobot/dcim/templates/dcim/inventoryitem_edit.html +3 -1
  197. nautobot/dcim/templates/dcim/location_retrieve.html +1 -242
  198. nautobot/dcim/templates/dcim/module/base.html +49 -9
  199. nautobot/dcim/templates/dcim/module_list.html +57 -8
  200. nautobot/dcim/templates/dcim/modulefamily_retrieve.html +1 -1
  201. nautobot/dcim/templates/dcim/moduletype_retrieve.html +49 -9
  202. nautobot/dcim/templates/dcim/platform_create.html +1 -1
  203. nautobot/dcim/templates/dcim/powerfeed.html +1 -1
  204. nautobot/dcim/templates/dcim/powerpanel.html +1 -1
  205. nautobot/dcim/templates/dcim/powerport.html +5 -4
  206. nautobot/dcim/templates/dcim/rack_elevation_list.html +16 -4
  207. nautobot/dcim/templates/dcim/rack_retrieve.html +33 -15
  208. nautobot/dcim/templates/dcim/rearport.html +7 -6
  209. nautobot/dcim/templates/dcim/virtualchassis.html +1 -1
  210. nautobot/dcim/templates/dcim/virtualchassis_add_member.html +16 -14
  211. nautobot/dcim/templates/dcim/virtualchassis_update.html +14 -6
  212. nautobot/dcim/tests/integration/test_controller.py +1 -0
  213. nautobot/dcim/tests/test_api.py +8 -0
  214. nautobot/dcim/tests/test_custom_validators.py +229 -0
  215. nautobot/dcim/tests/test_filters.py +12 -6
  216. nautobot/dcim/tests/test_models.py +63 -4
  217. nautobot/dcim/tests/test_views.py +63 -22
  218. nautobot/dcim/urls.py +64 -21
  219. nautobot/dcim/utils.py +3 -3
  220. nautobot/dcim/views.py +547 -273
  221. nautobot/extras/api/views.py +9 -1
  222. nautobot/extras/choices.py +2 -13
  223. nautobot/extras/{filters/mixins.py → filter_mixins.py} +1 -1
  224. nautobot/extras/{filters/customfields.py → filter_mixins_customfields.py} +42 -6
  225. nautobot/extras/{filters/__init__.py → filters.py} +14 -46
  226. nautobot/extras/forms/forms.py +5 -13
  227. nautobot/extras/forms/mixins.py +0 -41
  228. nautobot/extras/management/__init__.py +9 -0
  229. nautobot/extras/migrations/0127_approval_workflow_models.py +6 -6
  230. nautobot/extras/migrations/0129_jobresult_debug_log_count_jobresult_error_log_count_and_more.py +37 -0
  231. nautobot/extras/migrations/0130_jobresult_generate_log_entry_counts.py +42 -0
  232. nautobot/extras/models/__init__.py +1 -2
  233. nautobot/extras/models/approvals.py +22 -13
  234. nautobot/extras/models/contacts.py +2 -0
  235. nautobot/extras/models/groups.py +44 -5
  236. nautobot/extras/models/jobs.py +59 -1
  237. nautobot/extras/models/mixins.py +28 -0
  238. nautobot/extras/models/models.py +13 -0
  239. nautobot/extras/models/secrets.py +1 -0
  240. nautobot/extras/models/statuses.py +0 -15
  241. nautobot/extras/navigation.py +13 -9
  242. nautobot/extras/plugins/__init__.py +33 -55
  243. nautobot/extras/plugins/tables.py +3 -3
  244. nautobot/extras/plugins/urls.py +2 -21
  245. nautobot/extras/plugins/utils.py +1 -33
  246. nautobot/extras/plugins/views.py +0 -4
  247. nautobot/extras/signals.py +20 -19
  248. nautobot/extras/tables.py +52 -68
  249. nautobot/extras/templates/extras/approval_dashboard.html +7 -5
  250. nautobot/extras/templates/extras/approvalworkflowdefinition_update.html +4 -2
  251. nautobot/extras/templates/extras/approvalworkflowstage_retrieve.html +20 -12
  252. nautobot/extras/templates/extras/computedfield.html +1 -1
  253. nautobot/extras/templates/extras/configcontext.html +1 -1
  254. nautobot/extras/templates/extras/configcontextschema_validation.html +2 -2
  255. nautobot/extras/templates/extras/customfield.html +1 -1
  256. nautobot/extras/templates/extras/dynamicgroup_retrieve.html +11 -5
  257. nautobot/extras/templates/extras/dynamicgroup_update.html +1 -1
  258. nautobot/extras/templates/extras/gitrepository_result.html +0 -2
  259. nautobot/extras/templates/extras/graphqlquery_retrieve.html +1 -96
  260. nautobot/extras/templates/extras/inc/approval_buttons_column.html +20 -6
  261. nautobot/extras/templates/extras/inc/bulk_edit_overridable_field.html +8 -7
  262. nautobot/extras/templates/extras/inc/configcontext_format.html +10 -3
  263. nautobot/extras/templates/extras/inc/graphqlquery_execute.html +71 -0
  264. nautobot/extras/templates/extras/inc/job_tiles.html +15 -3
  265. nautobot/extras/templates/extras/inc/json_format.html +10 -3
  266. nautobot/extras/templates/extras/inc/overridable_field.html +13 -12
  267. nautobot/extras/templates/extras/job.html +29 -12
  268. nautobot/extras/templates/extras/job_bulk_edit.html +18 -0
  269. nautobot/extras/templates/extras/job_edit.html +52 -46
  270. nautobot/extras/templates/extras/job_list.html +29 -25
  271. nautobot/extras/templates/extras/marketplace.html +5 -9
  272. nautobot/extras/templates/extras/object_configcontext.html +1 -1
  273. nautobot/extras/templates/extras/object_dynamicgroups.html +2 -2
  274. nautobot/extras/templates/extras/objectchange_retrieve.html +19 -37
  275. nautobot/extras/templates/extras/plugin_detail.html +26 -21
  276. nautobot/extras/templates/extras/plugins_list.html +16 -26
  277. nautobot/extras/templates/extras/role_retrieve.html +64 -0
  278. nautobot/extras/templates/extras/scheduledjob.html +4 -2
  279. nautobot/extras/templates/extras/secretsgroup.html +1 -1
  280. nautobot/extras/templates/extras/tag.html +1 -1
  281. nautobot/extras/templatetags/custom_links.py +12 -12
  282. nautobot/extras/templatetags/job_buttons.py +14 -12
  283. nautobot/extras/test_jobs/invalid_import.py +9 -0
  284. nautobot/extras/test_jobs/log_counts_by_level.py +23 -0
  285. nautobot/extras/test_jobs/missing_import.py +11 -0
  286. nautobot/extras/tests/integration/test_configcontextschema.py +27 -26
  287. nautobot/extras/tests/integration/test_customfields.py +8 -7
  288. nautobot/extras/tests/integration/test_dynamicgroups.py +5 -1
  289. nautobot/extras/tests/integration/test_plugin_banner.py +3 -0
  290. nautobot/extras/tests/integration/test_plugins.py +18 -6
  291. nautobot/extras/tests/test_api.py +27 -18
  292. nautobot/extras/tests/test_approvals.py +38 -38
  293. nautobot/extras/tests/test_changelog.py +35 -3
  294. nautobot/extras/tests/test_customfields.py +22 -13
  295. nautobot/extras/tests/test_customfields_filters.py +479 -0
  296. nautobot/extras/tests/test_dynamicgroups.py +39 -1
  297. nautobot/extras/tests/test_filters.py +21 -19
  298. nautobot/extras/tests/test_forms.py +18 -21
  299. nautobot/extras/tests/test_jobs.py +25 -4
  300. nautobot/extras/tests/test_migrations.py +1 -0
  301. nautobot/extras/tests/test_models.py +13 -31
  302. nautobot/extras/tests/test_plugins.py +36 -10
  303. nautobot/extras/tests/test_views.py +31 -30
  304. nautobot/extras/views.py +81 -19
  305. nautobot/ipam/factory.py +7 -0
  306. nautobot/ipam/filter_mixins.py +38 -0
  307. nautobot/ipam/filters.py +27 -38
  308. nautobot/ipam/formfields.py +1 -1
  309. nautobot/ipam/forms.py +6 -3
  310. nautobot/ipam/migrations/0030_ipam__namespaces.py +13 -0
  311. nautobot/ipam/migrations/0031_ipam___data_migrations.py +4 -1
  312. nautobot/ipam/migrations/0054_namespace_tenant.py +25 -0
  313. nautobot/ipam/models.py +29 -2
  314. nautobot/ipam/navigation.py +3 -2
  315. nautobot/ipam/signals.py +71 -0
  316. nautobot/ipam/tables.py +13 -6
  317. nautobot/ipam/templates/ipam/inc/toggle_available.html +10 -10
  318. nautobot/ipam/templates/ipam/inc/vlangroup_header.html +1 -0
  319. nautobot/ipam/templates/ipam/ipaddress.html +14 -0
  320. nautobot/ipam/templates/ipam/ipaddress_merge.html +3 -3
  321. nautobot/ipam/templates/ipam/ipaddresstointerface_retrieve.html +1 -0
  322. nautobot/ipam/templates/ipam/namespace_update.html +15 -0
  323. nautobot/ipam/templates/ipam/prefix_delete.html +1 -1
  324. nautobot/ipam/templates/ipam/prefix_list.html +14 -13
  325. nautobot/ipam/templates/ipam/service.html +1 -1
  326. nautobot/ipam/templates/ipam/vlan.html +1 -1
  327. nautobot/ipam/templates/ipam/vlan_interfaces.html +1 -1
  328. nautobot/ipam/templates/ipam/vlan_vminterfaces.html +1 -1
  329. nautobot/ipam/tests/migration/test_migrations.py +89 -0
  330. nautobot/ipam/tests/test_api.py +13 -6
  331. nautobot/ipam/tests/test_filters.py +10 -0
  332. nautobot/ipam/tests/test_forms.py +1 -1
  333. nautobot/ipam/tests/test_models.py +43 -1
  334. nautobot/ipam/tests/test_tables.py +1 -2
  335. nautobot/ipam/tests/test_utils.py +1 -1
  336. nautobot/ipam/tests/test_views.py +13 -14
  337. nautobot/ipam/ui.py +0 -17
  338. nautobot/ipam/utils/migrations.py +16 -2
  339. nautobot/ipam/utils/testing.py +9 -3
  340. nautobot/ipam/views.py +46 -6
  341. nautobot/project-static/dist/css/nautobot.css +1 -1
  342. nautobot/project-static/dist/css/nautobot.css.map +1 -1
  343. nautobot/project-static/dist/js/nautobot.js +1 -1
  344. nautobot/project-static/dist/js/nautobot.js.map +1 -1
  345. nautobot/project-static/js/cabletrace.js +1 -1
  346. nautobot/project-static/js/interface_filtering.js +20 -16
  347. nautobot/project-static/nautobot-icons/battery-3.svg +3 -0
  348. nautobot/project-static/nautobot-icons/cloud.svg +1 -1
  349. nautobot/project-static/nautobot-icons/control-panel.svg +1 -1
  350. nautobot/project-static/nautobot-icons/device-lifecycle.svg +1 -1
  351. nautobot/project-static/nautobot-icons/elements.svg +1 -1
  352. nautobot/project-static/nautobot-icons/extensibility.svg +3 -0
  353. nautobot/project-static/nautobot-icons/hammer.svg +1 -1
  354. nautobot/project-static/nautobot-icons/organization.svg +3 -0
  355. nautobot/project-static/nautobot-icons/secrets.svg +1 -1
  356. nautobot/project-static/nautobot-icons/security.svg +3 -0
  357. nautobot/project-static/nautobot-icons/server.svg +1 -1
  358. nautobot/project-static/nautobot-icons/star-filled.svg +1 -1
  359. nautobot/project-static/nautobot-icons/star.svg +1 -1
  360. nautobot/tenancy/api/serializers.py +1 -0
  361. nautobot/tenancy/api/views.py +2 -1
  362. nautobot/tenancy/{filters/__init__.py → filters.py} +2 -10
  363. nautobot/tenancy/navigation.py +3 -1
  364. nautobot/tenancy/tests/test_filters.py +0 -2
  365. nautobot/tenancy/views.py +2 -1
  366. nautobot/ui/src/js/collapse.js +3 -3
  367. nautobot/ui/src/js/nautobot.js +16 -0
  368. nautobot/ui/src/scss/colors.scss +1 -1
  369. nautobot/ui/src/scss/nautobot.scss +61 -28
  370. nautobot/users/templates/users/profile.html +45 -12
  371. nautobot/users/templates/users/sessionkey_delete.html +1 -1
  372. nautobot/users/tests/test_api.py +4 -0
  373. nautobot/users/views.py +4 -2
  374. nautobot/virtualization/models.py +1 -68
  375. nautobot/virtualization/navigation.py +3 -2
  376. nautobot/virtualization/templates/virtualization/virtual_machine_vminterface_delete.html +1 -1
  377. nautobot/virtualization/templates/virtualization/virtualmachine.html +1 -1
  378. nautobot/virtualization/templates/virtualization/virtualmachine_list.html +2 -2
  379. nautobot/virtualization/templates/virtualization/virtualmachine_update.html +3 -1
  380. nautobot/virtualization/tests/test_api.py +3 -0
  381. nautobot/virtualization/tests/test_models.py +44 -4
  382. nautobot/vpn/__init__.py +0 -0
  383. nautobot/vpn/api/serializers.py +113 -0
  384. nautobot/vpn/api/urls.py +19 -0
  385. nautobot/vpn/api/views.py +70 -0
  386. nautobot/vpn/apps.py +8 -0
  387. nautobot/vpn/choices.py +171 -0
  388. nautobot/vpn/factory.py +209 -0
  389. nautobot/vpn/filters.py +233 -0
  390. nautobot/vpn/forms.py +486 -0
  391. nautobot/vpn/homepage.py +19 -0
  392. nautobot/vpn/migrations/0001_initial.py +541 -0
  393. nautobot/vpn/migrations/0002_populate_defaults.py +199 -0
  394. nautobot/vpn/migrations/__init__.py +0 -0
  395. nautobot/vpn/models.py +527 -0
  396. nautobot/vpn/navigation.py +98 -0
  397. nautobot/vpn/tables.py +380 -0
  398. nautobot/vpn/templates/vpn/vpnprofile.html +2 -0
  399. nautobot/vpn/templates/vpn/vpnprofile_create.html +150 -0
  400. nautobot/vpn/tests/__init__.py +0 -0
  401. nautobot/vpn/tests/test_api.py +341 -0
  402. nautobot/vpn/tests/test_filters.py +139 -0
  403. nautobot/vpn/tests/test_forms.py +294 -0
  404. nautobot/vpn/tests/test_models.py +97 -0
  405. nautobot/vpn/tests/test_views.py +281 -0
  406. nautobot/vpn/urls.py +16 -0
  407. nautobot/vpn/views.py +437 -0
  408. nautobot/wireless/navigation.py +3 -2
  409. nautobot/wireless/tests/integration/test_radio_profile.py +1 -5
  410. nautobot/wireless/tests/test_api.py +1 -1
  411. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/METADATA +14 -14
  412. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/RECORD +417 -366
  413. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/entry_points.txt +1 -0
  414. nautobot/data_validation/template_content.py +0 -42
  415. nautobot/dcim/filters/mixins.py +0 -354
  416. nautobot/ipam/templates/ipam/inc/prefix_header_extra_content_table.html +0 -4
  417. /nautobot/tenancy/{filters/mixins.py → filter_mixins.py} +0 -0
  418. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/LICENSE.txt +0 -0
  419. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/NOTICE +0 -0
  420. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/WHEEL +0 -0
@@ -10,16 +10,16 @@ from nautobot.extras.models import CustomLink
10
10
 
11
11
  register = template.Library()
12
12
 
13
- LINK_BUTTON = '<a href="{}"{} class="btn btn-sm btn-{}">{}</a>\n'
13
+ LINK_BUTTON = '<a href="{}"{} class="btn btn-{}">{}</a>\n'
14
14
  GROUP_BUTTON = (
15
- '<div class="btn-group">\n'
16
- '<button type="button" class="btn btn-sm btn-{} dropdown-toggle" data-toggle="dropdown">\n'
17
- '{} <span class="caret"></span>\n'
15
+ '<div class="dropdown d-inline-flex align-middle">\n'
16
+ '<button type="button" class="btn btn-{} dropdown-toggle" data-bs-toggle="dropdown">\n'
17
+ '{} <span class="mdi mdi-chevron-down" aria-hidden="true"></span>\n'
18
18
  "</button>\n"
19
- '<ul class="dropdown-menu pull-right">\n'
19
+ '<ul class="dropdown-menu float-end">\n'
20
20
  "{}</ul></div>\n"
21
21
  )
22
- GROUP_LINK = '<li><a href="{}"{}>{}</a></li>\n'
22
+ GROUP_LINK = '<li><a class="dropdown-item" href="{}"{}>{}</a></li>\n'
23
23
 
24
24
 
25
25
  @register.simple_tag(takes_context=True)
@@ -58,12 +58,12 @@ def custom_links(context, obj):
58
58
  link_rendered = render_jinja2(cl.target_url, link_context)
59
59
  link_target = ' target="_blank"' if cl.new_window else ""
60
60
  template_code += format_html(
61
- LINK_BUTTON, link_rendered, link_target, cl.button_class, text_rendered
61
+ LINK_BUTTON, link_rendered, link_target, cl.button_class_css_class, text_rendered
62
62
  )
63
63
  except Exception as e:
64
64
  template_code += format_html(
65
- '<a class="btn btn-sm btn-default" disabled="disabled" title="{}">'
66
- '<i class="mdi mdi-alert"></i> {}</a>\n',
65
+ '<a aria-disabled="true" class="btn btn-secondary disabled" title="{}">'
66
+ '<span class="mdi mdi-alert"></span> {}</a>\n',
67
67
  e,
68
68
  cl.name,
69
69
  )
@@ -81,13 +81,13 @@ def custom_links(context, obj):
81
81
  links_rendered += format_html(GROUP_LINK, link_rendered, link_target, text_rendered)
82
82
  except Exception as e:
83
83
  links_rendered += format_html(
84
- '<li><a disabled="disabled" title="{}"><span class="text-secondary">'
85
- '<i class="mdi mdi-alert"></i> {}</span></a></li>',
84
+ '<li><a aria-disabled="true" class="disabled dropdown-item" title="{}"><span class="text-secondary">'
85
+ '<span class="mdi mdi-alert"></span> {}</span></a></li>',
86
86
  e,
87
87
  cl.name,
88
88
  )
89
89
 
90
90
  if links_rendered:
91
- template_code += format_html(GROUP_BUTTON, links[0].button_class, group, links_rendered)
91
+ template_code += format_html(GROUP_BUTTON, links[0].button_class_css_class, group, links_rendered)
92
92
 
93
93
  return template_code
@@ -13,11 +13,11 @@ from nautobot.extras.models import Job, JobButton, JobQueue
13
13
  register = template.Library()
14
14
 
15
15
  GROUP_DROPDOWN = """
16
- <div class="btn-group">
17
- <button type="button" class="btn btn-sm btn-{group_button_class} dropdown-toggle" data-toggle="dropdown">
18
- {group_name} <span class="caret"></span>
16
+ <div class="dropdown d-inline-flex align-middle">
17
+ <button type="button" class="btn btn-{group_button_class} dropdown-toggle" data-bs-toggle="dropdown">
18
+ {group_name} <span class="mdi mdi-chevron-down" aria-hidden="true"></span>
19
19
  </button>
20
- <ul class="dropdown-menu pull-right">
20
+ <ul class="dropdown-menu dropdown-menu-end">
21
21
  {grouped_buttons}
22
22
  </ul>
23
23
  </div>
@@ -33,7 +33,7 @@ HIDDEN_INPUTS = """
33
33
  """
34
34
 
35
35
  NO_CONFIRM_BUTTON = """
36
- <button type="submit" form="form_id_{button_id}" class="btn btn-sm btn-{button_class}" {disabled}>{button_text}</button>
36
+ <button type="submit" form="form_id_{button_id}" class="btn btn-{button_class} {menu_item}" {disabled}>{button_text}</button>
37
37
  """
38
38
 
39
39
  NO_CONFIRM_FORM = """
@@ -43,7 +43,7 @@ NO_CONFIRM_FORM = """
43
43
  """
44
44
 
45
45
  CONFIRM_BUTTON = """
46
- <button type="button" class="btn btn-sm btn-{button_class}" data-toggle="modal" data-target="#confirm_modal_id_{button_id}" {disabled}>
46
+ <button type="button" class="btn btn-{button_class} {menu_item}" data-bs-toggle="modal" data-bs-target="#confirm_modal_id_{button_id}" {disabled}>
47
47
  {button_text}
48
48
  </button>
49
49
  """
@@ -53,8 +53,8 @@ CONFIRM_MODAL = """
53
53
  <div class="modal-dialog" role="document">
54
54
  <div class="modal-content">
55
55
  <div class="modal-header">
56
- <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
57
56
  <h4 class="modal-title" id="confirm_modal_label_{button_id}">Confirmation</h4>
57
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
58
58
  </div>
59
59
  <form id="form_id_{button_id}" action="{button_url}" method="post" class="form">
60
60
  <div class="modal-body">
@@ -62,7 +62,7 @@ CONFIRM_MODAL = """
62
62
  Run Job <strong>'{job}'</strong> with object <strong>'{object}'</strong>?
63
63
  </div>
64
64
  <div class="modal-footer">
65
- <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
65
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
66
66
  <button type="submit" class="btn btn-primary">Confirm</button>
67
67
  </div>
68
68
  </form>
@@ -94,8 +94,9 @@ def _render_job_button_for_obj(job_button, obj, context, content_type):
94
94
  except Exception as exc:
95
95
  return (
96
96
  format_html(
97
- '<a class="btn btn-sm btn-{}" disabled="disabled" title="{}"><i class="mdi mdi-alert"></i> {}</a>\n',
98
- "default" if not job_button.group_name else "link",
97
+ '<a aria-disabled="true" class="btn btn-{} disabled" title="{}">'
98
+ '<span class="mdi mdi-alert"></span> {}</a>\n',
99
+ "secondary" if not job_button.group_name else "link dropdown-item",
99
100
  exc,
100
101
  job_button.name,
101
102
  ),
@@ -123,12 +124,13 @@ def _render_job_button_for_obj(job_button, obj, context, content_type):
123
124
  template_args = {
124
125
  "button_id": job_button.pk,
125
126
  "button_text": text_rendered,
126
- "button_class": job_button.button_class if not job_button.group_name else "link",
127
+ "button_class": job_button.button_class_css_class if not job_button.group_name else "link",
127
128
  "button_url": reverse("extras:job_run", kwargs={"pk": job_button.job.pk}),
128
129
  "object": obj,
129
130
  "job": job_button.job,
130
131
  "hidden_inputs": hidden_inputs,
131
132
  "disabled": "" if (has_run_perm and job_button.job.installed and job_button.job.enabled) else "disabled",
133
+ "menu_item": "dropdown-item" if job_button.group_name else "",
132
134
  }
133
135
 
134
136
  if job_button.confirmation:
@@ -170,7 +172,7 @@ def job_buttons(context, obj):
170
172
 
171
173
  # Add grouped buttons to template
172
174
  for group_name, buttons in group_names.items():
173
- group_button_class = buttons[0].button_class
175
+ group_button_class = buttons[0].button_class_css_class
174
176
 
175
177
  buttons_rendered = SAFE_EMPTY_STR
176
178
 
@@ -0,0 +1,9 @@
1
+ from nautobot.core.celery import register_jobs
2
+ from nautobot.extras.jobs import Job, no_such_attribute # noqa: F401 # pylint: disable=no-name-in-module
3
+
4
+
5
+ class MyJob(Job):
6
+ pass
7
+
8
+
9
+ register_jobs(MyJob)
@@ -0,0 +1,23 @@
1
+ from nautobot.core.celery import register_jobs
2
+ from nautobot.extras.jobs import get_task_logger, Job
3
+
4
+ logger = get_task_logger(__name__)
5
+
6
+
7
+ class TestLogCountsByLevel(Job):
8
+ class Meta:
9
+ description = "Test counts of log entries by level."
10
+
11
+ def run(self): # pylint: disable=arguments-differ
12
+ # intentionally omit debug
13
+ logger.info("This is an info log")
14
+ logger.info("This is an info log")
15
+ logger.warning("This is a warning log")
16
+ logger.warning("This is a warning log")
17
+ logger.error("This is an error log")
18
+ logger.critical("This is a critical log")
19
+ logger.failure("This is a failure log")
20
+ # success log will be added by post_run
21
+
22
+
23
+ register_jobs(TestLogCountsByLevel)
@@ -0,0 +1,11 @@
1
+ from no_such_module import no_such_attribute # noqa: F401 # pylint: disable=import-error
2
+
3
+ from nautobot.core.celery import register_jobs
4
+ from nautobot.extras.jobs import Job
5
+
6
+
7
+ class MyJob(Job):
8
+ pass
9
+
10
+
11
+ register_jobs(MyJob)
@@ -1,5 +1,4 @@
1
1
  from django.contrib.contenttypes.models import ContentType
2
- from django.test import tag
3
2
 
4
3
  from nautobot.core.testing.integration import ObjectDetailsMixin, ObjectsListMixin, SeleniumTestCase
5
4
  from nautobot.dcim.models import Device, DeviceType, Location, LocationType, Manufacturer
@@ -16,7 +15,6 @@ class ConfigContextSchemaTestCase(SeleniumTestCase, ObjectDetailsMixin, ObjectsL
16
15
  super().setUp()
17
16
  self.login_as_superuser()
18
17
 
19
- @tag("fix_in_v3")
20
18
  def test_create_valid_config_context_schema(self):
21
19
  """
22
20
  Given a clean slate, navigate to and fill out the form for a valid schema object
@@ -34,13 +32,12 @@ class ConfigContextSchemaTestCase(SeleniumTestCase, ObjectDetailsMixin, ObjectsL
34
32
  self.fill_input("name", "Integration Schema 1")
35
33
  self.fill_input("description", "Description")
36
34
  self.fill_input("data_schema", '{"type": "object", "properties": {"a": {"type": "string"}}}')
37
- self.browser.find_by_text("Create").click()
35
+ self.browser.find_by_xpath("//button[normalize-space()='Create']").click()
38
36
 
39
37
  # Verify form redirect
40
38
  self.assertTrue(self.browser.is_text_present("Created config context schema Integration Schema 1"))
41
39
  self.assertTrue(self.browser.is_text_present("Edit"))
42
40
 
43
- @tag("fix_in_v3")
44
41
  def test_create_invalid_config_context_schema(self):
45
42
  """
46
43
  Given a clean slate, navigate to and fill out the form for an invalid schema object
@@ -61,14 +58,13 @@ class ConfigContextSchemaTestCase(SeleniumTestCase, ObjectDetailsMixin, ObjectsL
61
58
  self.fill_input("name", "Integration Schema 2")
62
59
  self.fill_input("description", "Description")
63
60
  self.fill_input("data_schema", '{"type": "object", "properties": {"a": {"type": "not a valid type"}}}')
64
- self.browser.find_by_text("Create").click()
61
+ self.browser.find_by_xpath("//button[normalize-space()='Create']").click()
65
62
 
66
63
  # Verify validation error raised to user within form
67
64
  self.assertTrue(self.browser.is_text_present("'not a valid type' is not valid under any of the given schemas"))
68
65
  self.assertTrue(self.browser.is_text_present("Add a new config context schema"))
69
66
  self.assertEqual(self.browser.find_by_name("name").first.value, "Integration Schema 2")
70
67
 
71
- @tag("fix_in_v3")
72
68
  def test_validation_tab(self):
73
69
  """
74
70
  Given a config context schema that is assigned to a config context, and device, and a VM with valid context data
@@ -137,60 +133,65 @@ class ConfigContextSchemaTestCase(SeleniumTestCase, ObjectDetailsMixin, ObjectsL
137
133
 
138
134
  # Assert Validation states
139
135
  self.assertEqual(
140
- len(self.browser.find_by_xpath("//div[@class[contains(., 'panel')]]//tbody/tr")), 3
136
+ len(self.browser.find_by_xpath("//div[@class[contains(., 'card')]]//tbody/tr")), 3
141
137
  ) # 3 rows (config context, device, virtual machine)
142
- for row in self.browser.find_by_xpath("//div[@class[contains(., 'panel')]]//tbody/tr"):
138
+ for row in self.browser.find_by_xpath("//div[@class[contains(., 'card')]]//tbody/tr"):
143
139
  self.assertEqual(
144
140
  row.find_by_tag("td")[-2].html,
145
- '<span class="text-success"><i class="mdi mdi-check-bold" title="" data-original-title="Yes"></i></span>',
141
+ '<span class="text-success"><i class="mdi mdi-check-bold" title="Yes"></i></span>',
146
142
  )
147
143
 
148
144
  # Edit the schema
149
- self.browser.links.find_by_partial_text("Edit").click()
145
+ self.switch_tab("Config Context")
146
+ self.click_button("#edit-button")
150
147
  # Change property "a" to be type string
151
148
  self.fill_input(
152
149
  "data_schema",
153
150
  '{"type": "object", "properties": {"a": {"type": "string"}, "b": {"type": "integer"}, "c": {"type": "integer"}}, "additionalProperties": false}',
154
151
  )
155
- self.browser.find_by_text("Update").click()
152
+ self.browser.find_by_xpath("//button[normalize-space()='Update']").click()
156
153
 
157
154
  # Navigate to ConfigContextSchema Validation tab
158
- self.browser.links.find_by_text("Validation").click()
155
+ self.switch_tab("Validation")
159
156
 
160
157
  # Assert Validation states
161
158
  self.assertEqual(
162
- len(self.browser.find_by_xpath("//div[@class[contains(., 'panel')]]//tbody/tr")), 3
159
+ len(self.browser.find_by_xpath("//div[@class[contains(., 'card')]]//tbody/tr")), 3
163
160
  ) # 3 rows (config context, device, virtual machine)
164
- for row in self.browser.find_by_xpath("//div[@class[contains(., 'panel')]]//tbody/tr"):
161
+ for row in self.browser.find_by_xpath("//div[@class[contains(., 'card')]]//tbody/tr"):
165
162
  self.assertEqual(
166
163
  row.find_by_tag("td")[-2].html,
167
- '<span class="text-danger"><i class="mdi mdi-close-thick" title="" data-original-title="No"></i></span><span class="text-danger">123 is not of type \'string\'</span>',
164
+ '<span class="text-danger"><i class="mdi mdi-close-thick" title="No"></i></span><span class="text-danger">123 is not of type \'string\'</span>',
168
165
  )
169
166
 
170
167
  # Edit the device local context data and redirect back to the validation tab
171
- self.browser.find_by_xpath("//div[@class[contains(., 'panel')]]//tbody/tr")[1].find_by_tag("td")[
172
- -1
173
- ].find_by_tag("a").click()
168
+ self.browser.find_by_xpath("//div[@class[contains(., 'card')]]//tbody/tr")[1].find_by_tag("td")[-1].find_by_tag(
169
+ "button"
170
+ ).click()
171
+ menu = self.browser.find_by_xpath("//ul[contains(@class, 'dropdown-menu') and contains(@class, 'show')]")
172
+ menu.is_visible(wait_time=5)
173
+ menu.find_by_tag("a").click()
174
+
174
175
  # Update the property "a" to be a string
175
176
  self.fill_input("local_config_context_data", '{"a": "foo", "b": 456, "c": 777}')
176
- self.browser.find_by_text("Update").click()
177
+ self.browser.find_by_xpath("//button[normalize-space()='Update']").click()
177
178
 
178
179
  # Assert Validation states
179
180
  self.assertEqual(
180
- len(self.browser.find_by_xpath("//div[@class[contains(., 'panel')]]//tbody/tr")), 3
181
+ len(self.browser.find_by_xpath("//div[@class[contains(., 'card')]]//tbody/tr")), 3
181
182
  ) # 3 rows (config context, device, virtual machine)
182
183
  # Config context still fails
183
184
  self.assertEqual(
184
- self.browser.find_by_xpath("//div[@class[contains(., 'panel')]]//tbody/tr")[0].find_by_tag("td")[-2].html,
185
- '<span class="text-danger"><i class="mdi mdi-close-thick" title="" data-original-title="No"></i></span><span class="text-danger">123 is not of type \'string\'</span>',
185
+ self.browser.find_by_xpath("//div[@class[contains(., 'card')]]//tbody/tr")[0].find_by_tag("td")[-2].html,
186
+ '<span class="text-danger"><i class="mdi mdi-close-thick" title="No"></i></span><span class="text-danger">123 is not of type \'string\'</span>',
186
187
  )
187
188
  # Device now passes
188
189
  self.assertEqual(
189
- self.browser.find_by_xpath("//div[@class[contains(., 'panel')]]//tbody/tr")[1].find_by_tag("td")[-2].html,
190
- '<span class="text-success"><i class="mdi mdi-check-bold" title="" data-original-title="Yes"></i></span>',
190
+ self.browser.find_by_xpath("//div[@class[contains(., 'card')]]//tbody/tr")[1].find_by_tag("td")[-2].html,
191
+ '<span class="text-success"><i class="mdi mdi-check-bold" title="Yes"></i></span>',
191
192
  )
192
193
  # Virtual machine still fails
193
194
  self.assertEqual(
194
- self.browser.find_by_xpath("//div[@class[contains(., 'panel')]]//tbody/tr")[2].find_by_tag("td")[-2].html,
195
- '<span class="text-danger"><i class="mdi mdi-close-thick" title="" data-original-title="No"></i></span><span class="text-danger">123 is not of type \'string\'</span>',
195
+ self.browser.find_by_xpath("//div[@class[contains(., 'card')]]//tbody/tr")[2].find_by_tag("td")[-2].html,
196
+ '<span class="text-danger"><i class="mdi mdi-close-thick" title="No"></i></span><span class="text-danger">123 is not of type \'string\'</span>',
196
197
  )
@@ -1,5 +1,4 @@
1
1
  from django.contrib.contenttypes.models import ContentType
2
- from django.test import tag
3
2
  from django.urls import reverse
4
3
  from selenium.webdriver.common.keys import Keys
5
4
 
@@ -111,7 +110,11 @@ class CustomFieldTestCase(SeleniumTestCase, ObjectDetailsMixin):
111
110
  self.assertEqual(len(table.find_by_css(".formset_row-custom_field_choices")), 5)
112
111
 
113
112
  # And 6 after clicking "Add another..."
114
- self.browser.find_by_css(".add-row").click()
113
+ self.click_button(".add-row")
114
+ # Before proceeding with further assertions and interactions, wait for the 6th row to appear in the DOM.
115
+ self.browser.is_element_present_by_css(
116
+ "#custom-field-choices .formset_row-custom_field_choices:nth-of-type(6)", wait_time=5
117
+ )
115
118
  rows = table.find_by_css(".formset_row-custom_field_choices")
116
119
  self.assertEqual(len(rows), 6)
117
120
  self.fill_input("custom_field_choices-5-value", "choice3")
@@ -157,7 +160,6 @@ class CustomFieldTestCase(SeleniumTestCase, ObjectDetailsMixin):
157
160
  self.assertTrue(self.browser.is_text_present("Modified custom field"))
158
161
  self.assertTrue(self.browser.is_text_present("new_choice"))
159
162
 
160
- @tag("fix_in_v3")
161
163
  def test_update_type_select_create_delete_choices(self):
162
164
  """
163
165
  Test edit existing field, deleting first choice, adding a new row and saving that as a new choice.
@@ -172,13 +174,13 @@ class CustomFieldTestCase(SeleniumTestCase, ObjectDetailsMixin):
172
174
 
173
175
  # Gather the rows, delete the first one, add a new one.
174
176
  table = self.browser.find_by_id("custom-field-choices")
175
- self.browser.find_by_css(".add-row").click() # Add a new row
177
+ self.click_button(".add-row") # Add a new row
176
178
  rows = table.find_by_css(".formset_row-custom_field_choices")
177
179
  rows.first.find_by_css(".delete-row").click() # Delete first row
178
180
 
179
181
  # Fill the new row, save it, assert correctness.
180
182
  self.fill_input("custom_field_choices-5-value", "new_choice") # Fill the last row
181
- self.browser.find_by_text("Update").click()
183
+ self.browser.find_by_xpath("//button[normalize-space()='Update']").click()
182
184
  self.assertEqual(self.browser.url, detail_url)
183
185
  self.assertTrue(self.browser.is_text_present("Modified custom field"))
184
186
  self.assertTrue(self.browser.is_text_present("new_choice"))
@@ -252,7 +254,6 @@ class CustomFieldTestCase(SeleniumTestCase, ObjectDetailsMixin):
252
254
  # Confirm the JSON data is visible
253
255
  self.assertTrue(self.browser.is_text_present("Test JSON Value"))
254
256
 
255
- @tag("fix_in_v3")
256
257
  def test_json_type_with_invalid_json(self):
257
258
  """
258
259
  This test creates a custom field with a type of "json".
@@ -274,7 +275,7 @@ class CustomFieldTestCase(SeleniumTestCase, ObjectDetailsMixin):
274
275
  active_web_element = self.browser.driver.switch_to.active_element
275
276
  # Type invalid JSON data into the form
276
277
  active_web_element.send_keys('{test_json_key: "Test Invalid JSON Value"}')
277
- self.browser.find_by_xpath(".//button[contains(text(), 'Update')]").click()
278
+ self.browser.find_by_xpath("//button[normalize-space()='Update']").click()
278
279
  self.assertTrue(self.browser.is_text_present("Enter a valid JSON"))
279
280
 
280
281
  def test_saving_object_after_its_custom_field_deleted(self):
@@ -69,8 +69,12 @@ class DynamicGroupTestCase(SeleniumTestCase):
69
69
 
70
70
  # And just a cursory check to make sure that the filter worked.
71
71
  group = DynamicGroup.objects.get(name=name)
72
- self.assertEqual(group.count, Device.objects.filter(status__name="Active").count())
73
72
  self.assertEqual(group.filter, {"status": ["Active"]})
73
+ # Because we don't auto-refresh the members on UI create/update any more:
74
+ # TODO: a more complete integration test could click the "Refresh Members" JobButton, wait until the job completes,
75
+ # and so forth, rather than doing so programmatically here:
76
+ group.update_cached_members()
77
+ self.assertEqual(group.count, Device.objects.filter(status__name="Active").count())
74
78
 
75
79
  # Verify dynamic group shows up on device detail tab
76
80
  self.browser.visit(
@@ -1,6 +1,9 @@
1
+ from django.test import tag
2
+
1
3
  from nautobot.core.testing.integration import SeleniumTestCase
2
4
 
3
5
 
6
+ @tag("example_app")
4
7
  class PluginBannerTestCase(SeleniumTestCase):
5
8
  """Integration test for rendering of plugin-injected banner content."""
6
9
 
@@ -3,7 +3,7 @@ import os
3
3
  import tempfile
4
4
 
5
5
  from django.contrib.contenttypes.models import ContentType
6
- from django.test import override_settings
6
+ from django.test import override_settings, tag
7
7
  from django.urls import reverse
8
8
 
9
9
  from nautobot.circuits.models import (
@@ -18,9 +18,8 @@ from nautobot.extras.choices import WebhookHttpMethodChoices
18
18
  from nautobot.extras.context_managers import web_request_context
19
19
  from nautobot.extras.models import Status, Webhook
20
20
 
21
- from example_app.models import ExampleModel
22
-
23
21
 
22
+ @tag("example_app")
24
23
  class AppWebhookTest(SeleniumTestCase):
25
24
  """
26
25
  This test case proves that Apps can use the webhook functions when making changes on a model.
@@ -28,6 +27,9 @@ class AppWebhookTest(SeleniumTestCase):
28
27
 
29
28
  def setUp(self):
30
29
  super().setUp()
30
+
31
+ from example_app.models import ExampleModel
32
+
31
33
  tempdir = tempfile.gettempdir()
32
34
  for f in os.listdir(tempdir):
33
35
  if f.startswith("test_app_webhook_"):
@@ -61,6 +63,8 @@ class AppWebhookTest(SeleniumTestCase):
61
63
  """
62
64
  Test that webhooks are correctly triggered by an App model create.
63
65
  """
66
+ from example_app.models import ExampleModel
67
+
64
68
  self.update_headers("test_app_webhook_create")
65
69
  # Make change to model
66
70
  with web_request_context(self.user):
@@ -73,6 +77,8 @@ class AppWebhookTest(SeleniumTestCase):
73
77
  """
74
78
  Test that webhooks are correctly triggered by an App model update.
75
79
  """
80
+ from example_app.models import ExampleModel
81
+
76
82
  self.update_headers("test_app_webhook_update")
77
83
  obj = ExampleModel.objects.create(name="foo", number=100)
78
84
 
@@ -88,6 +94,8 @@ class AppWebhookTest(SeleniumTestCase):
88
94
  """
89
95
  Test that webhooks are correctly triggered by an App model delete.
90
96
  """
97
+ from example_app.models import ExampleModel
98
+
91
99
  self.update_headers(os.path.join(tempfile.gettempdir(), "test_app_webhook_delete"))
92
100
  obj = ExampleModel.objects.create(name="foo", number=100)
93
101
 
@@ -102,6 +110,8 @@ class AppWebhookTest(SeleniumTestCase):
102
110
  """
103
111
  Verify that webhook body_template is correctly used.
104
112
  """
113
+ from example_app.models import ExampleModel
114
+
105
115
  self.update_headers("test_app_webhook_with_body")
106
116
 
107
117
  self.webhook.body_template = '{"message": "{{ event }}"}'
@@ -117,6 +127,7 @@ class AppWebhookTest(SeleniumTestCase):
117
127
  os.remove(os.path.join(tempfile.gettempdir(), "test_app_webhook_with_body"))
118
128
 
119
129
 
130
+ @tag("example_app")
120
131
  class AppDocumentationTest(SeleniumTestCase):
121
132
  """
122
133
  Integration tests for ensuring App provided docs are supported.
@@ -129,16 +140,16 @@ class AppDocumentationTest(SeleniumTestCase):
129
140
  def test_object_edit_help_provided(self):
130
141
  """The ExampleModel object provides model documentation, this test ensures the help link is rendered."""
131
142
  self.browser.visit(f"{self.live_server_url}{reverse('plugins:example_app:examplemodel_add')}")
132
-
133
- self.assertTrue(self.browser.links.find_by_partial_href("example_app/docs/models/examplemodel.html"))
143
+ self.assertTrue(self.browser.links.find_by_partial_href("docs/example-app/models/examplemodel.html"))
134
144
 
135
145
  def test_object_edit_help_not_provided(self):
136
146
  """The AnotherExampleModel object doesn't provide model documentation, this test ensures no help link is provided."""
137
147
  self.browser.visit(f"{self.live_server_url}{reverse('plugins:example_app:anotherexamplemodel_add')}")
138
148
 
139
- self.assertFalse(self.browser.links.find_by_partial_href("example_app/docs/models/anotherexamplemodel.html"))
149
+ self.assertFalse(self.browser.links.find_by_partial_href("/docs/example-app/models/anotherexamplemodel.html"))
140
150
 
141
151
 
152
+ @tag("example_app")
142
153
  class AppReturnUrlTestCase(SeleniumTestCase):
143
154
  """
144
155
  Integration tests for reversing App return urls.
@@ -159,6 +170,7 @@ class AppReturnUrlTestCase(SeleniumTestCase):
159
170
  self.assertEqual(element["href"], f"{self.live_server_url}{reverse('plugins:example_app:examplemodel_list')}")
160
171
 
161
172
 
173
+ @tag("example_app")
162
174
  class AppTabsTestCase(SeleniumTestCase, ObjectDetailsMixin):
163
175
  """
164
176
  Integration tests for extra object detail UI tabs.