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
@@ -17,6 +17,7 @@ from rest_framework import serializers, status
17
17
  from rest_framework.relations import ManyRelatedField
18
18
  from rest_framework.test import APITransactionTestCase as _APITransactionTestCase
19
19
 
20
+ from nautobot.core import constants
20
21
  from nautobot.core.api.utils import get_serializer_for_model
21
22
  from nautobot.core.models import fields as core_fields
22
23
  from nautobot.core.models.tree_queries import TreeModel
@@ -294,8 +295,9 @@ class APIViewTestCases:
294
295
  m2m_fields = self.get_m2m_fields()
295
296
  self.add_permissions(f"{self.model._meta.app_label}.view_{self.model._meta.model_name}")
296
297
  list_url = f"{self._get_list_url()}?depth=0"
298
+ # With exclude_m2m query parameter set to False
297
299
  with CaptureQueriesContext(connections[DEFAULT_DB_ALIAS]) as cqc:
298
- response = self.client.get(list_url, **self.header)
300
+ response = self.client.get(list_url + "&exclude_m2m=false", **self.header)
299
301
  base_num_queries = len(cqc)
300
302
 
301
303
  self.assertHttpStatus(response, status.HTTP_200_OK)
@@ -340,9 +342,9 @@ class APIViewTestCases:
340
342
  app_label, model_name = object_type.split(".")
341
343
  ContentType.objects.get(app_label=app_label, model=model_name)
342
344
 
343
- list_url += "&exclude_m2m=true"
345
+ # With exclude_m2m query parameter set to True
344
346
  with CaptureQueriesContext(connections[DEFAULT_DB_ALIAS]) as cqc:
345
- response = self.client.get(list_url, **self.header)
347
+ response = self.client.get(list_url + "&exclude_m2m=true", **self.header)
346
348
 
347
349
  self.assertHttpStatus(response, status.HTTP_200_OK)
348
350
  self.assertIsInstance(response.data, dict)
@@ -387,8 +389,9 @@ class APIViewTestCases:
387
389
  m2m_fields = self.get_m2m_fields()
388
390
  self.add_permissions(f"{self.model._meta.app_label}.view_{self.model._meta.model_name}")
389
391
  list_url = f"{self._get_list_url()}?depth=1"
392
+ # With exclude_m2m query parameter set to False
390
393
  with CaptureQueriesContext(connections[DEFAULT_DB_ALIAS]) as cqc:
391
- response = self.client.get(list_url, **self.header)
394
+ response = self.client.get(list_url + "&exclude_m2m=false", **self.header)
392
395
  base_num_queries = len(cqc)
393
396
 
394
397
  self.assertHttpStatus(response, status.HTTP_200_OK)
@@ -420,9 +423,9 @@ class APIViewTestCases:
420
423
  self.assertTrue(is_uuid(response_data[field]["id"]))
421
424
  self.assertGreater(len(response_data[field].keys()), 3, response_data[field])
422
425
 
423
- list_url += "&exclude_m2m=true"
426
+ # With exclude_m2m query parameter set to True
424
427
  with CaptureQueriesContext(connections[DEFAULT_DB_ALIAS]) as cqc:
425
- response = self.client.get(list_url, **self.header)
428
+ response = self.client.get(list_url + "&exclude_m2m=true", **self.header)
426
429
 
427
430
  self.assertHttpStatus(response, status.HTTP_200_OK)
428
431
  self.assertIsInstance(response.data, dict)
@@ -458,6 +461,54 @@ class APIViewTestCases:
458
461
  self.assertNotIn(field, response_data)
459
462
  # TODO: we should assert that all other fields are still present, but there's a few corner cases...
460
463
 
464
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
465
+ def test_list_objects_exclude_m2m(self):
466
+ """
467
+ GET a list of objects with or without the "exclude_m2m" parameter.
468
+
469
+ With exclude_m2m query parameter set to True, we should see no many-to-many fields.
470
+ With exclude_m2m query parameter set to False, we should see all many-to-many fields.
471
+ With exclude_m2m query parameter not set, we should only see the default many-to-many fields.
472
+ """
473
+ m2m_fields = self.get_m2m_fields()
474
+ if not m2m_fields:
475
+ self.skipTest("No many-to-many fields to test")
476
+ self.add_permissions(f"{self.model._meta.app_label}.view_{self.model._meta.model_name}")
477
+ list_url = f"{self._get_list_url()}"
478
+
479
+ # With exclude_m2m query parameter not set
480
+ response = self.client.get(list_url, **self.header)
481
+ self.assertHttpStatus(response, status.HTTP_200_OK)
482
+ self.assertIsInstance(response.data, dict)
483
+ self.assertIn("results", response.data)
484
+
485
+ for response_data in response.data["results"]:
486
+ for field in m2m_fields:
487
+ if field in constants.DEFAULT_M2M_FIELDS:
488
+ self.assertIn(field, response_data)
489
+ self.assertIsInstance(response_data[field], list)
490
+ else:
491
+ self.assertNotIn(field, response_data)
492
+
493
+ # With exclude_m2m query parameter set to True
494
+ response = self.client.get(list_url + "?exclude_m2m=true", **self.header)
495
+ self.assertHttpStatus(response, status.HTTP_200_OK)
496
+ self.assertIsInstance(response.data, dict)
497
+ self.assertIn("results", response.data)
498
+ for response_data in response.data["results"]:
499
+ for field in m2m_fields:
500
+ self.assertNotIn(field, response_data)
501
+
502
+ # With exclude_m2m query parameter set to False
503
+ response = self.client.get(list_url + "?exclude_m2m=false", **self.header)
504
+ self.assertHttpStatus(response, status.HTTP_200_OK)
505
+ self.assertIsInstance(response.data, dict)
506
+ self.assertIn("results", response.data)
507
+ for response_data in response.data["results"]:
508
+ for field in m2m_fields:
509
+ self.assertIn(field, response_data)
510
+ self.assertIsInstance(response_data[field], list)
511
+
461
512
  @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
462
513
  def test_list_objects_without_permission(self):
463
514
  """
@@ -1076,12 +1127,20 @@ class APIViewTestCases:
1076
1127
 
1077
1128
  self.assertIn("actions", data)
1078
1129
 
1079
- # Grab any field that has choices defined (fields with enums)
1130
+ # Grab any field that has choices defined (fields with enums including child fields with enums)
1080
1131
  field_choices = {}
1081
1132
  if "POST" in data["actions"]:
1082
- field_choices = {k for k, v in data["actions"]["POST"].items() if "choices" in v}
1133
+ field_choices = {
1134
+ k
1135
+ for k, v in data["actions"]["POST"].items()
1136
+ if "choices" in v or ("child" in v and "choices" in v["child"])
1137
+ }
1083
1138
  elif "PUT" in data["actions"]:
1084
- field_choices = {k for k, v in data["actions"]["PUT"].items() if "choices" in v}
1139
+ field_choices = {
1140
+ k
1141
+ for k, v in data["actions"]["PUT"].items()
1142
+ if "choices" in v or ("child" in v and "choices" in v["child"])
1143
+ }
1085
1144
  else:
1086
1145
  self.fail(f"Neither PUT nor POST are available actions in: {data['actions']}")
1087
1146
 
@@ -21,7 +21,6 @@ from nautobot.core.filters import (
21
21
  )
22
22
  from nautobot.core.models.generics import PrimaryModel
23
23
  from nautobot.core.testing import views
24
- from nautobot.core.utils.deprecation import class_deprecated_in_favor_of
25
24
  from nautobot.extras.models import Contact, ContactAssociation, Role, Status, Tag, Team
26
25
  from nautobot.tenancy import models
27
26
 
@@ -435,28 +434,6 @@ class FilterTestCases:
435
434
  ),
436
435
  )
437
436
 
438
- # Test cases should just explicitly include `name` as a generic_filter_tests entry
439
- @class_deprecated_in_favor_of(FilterTestCase) # pylint: disable=undefined-variable
440
- class NameOnlyFilterTestCase(FilterTestCase):
441
- """Add simple tests for filtering by name."""
442
-
443
- def test_filters_generic(self):
444
- if not any(test[0] == "name" for test in self.generic_filter_tests):
445
- self.generic_filter_tests = (["name"], *self.generic_filter_tests)
446
- super().test_filters_generic()
447
-
448
- # Test cases should just explicitly include `name` and `slug` as generic_filter_tests entries
449
- @class_deprecated_in_favor_of(FilterTestCase) # pylint: disable=undefined-variable
450
- class NameSlugFilterTestCase(FilterTestCase):
451
- """Add simple tests for filtering by name and by slug."""
452
-
453
- def test_filters_generic(self):
454
- if not any(test[0] == "slug" for test in self.generic_filter_tests):
455
- self.generic_filter_tests = (["slug"], *self.generic_filter_tests)
456
- if not any(test[0] == "name" for test in self.generic_filter_tests):
457
- self.generic_filter_tests = (["name"], *self.generic_filter_tests)
458
- super().test_filters_generic()
459
-
460
437
  class TenancyFilterTestCaseMixin(views.TestCase):
461
438
  """Add test cases for tenant and tenant-group filters."""
462
439
 
@@ -7,7 +7,9 @@ from django.db.models import Model
7
7
  from django.test import override_settings, tag
8
8
  from django.urls import reverse
9
9
  from django.utils.functional import classproperty
10
+ from selenium.webdriver.common.by import By
10
11
  from selenium.webdriver.common.keys import Keys
12
+ from selenium.webdriver.support.expected_conditions import element_to_be_clickable
11
13
  from selenium.webdriver.support.wait import WebDriverWait
12
14
  from splinter.browser import Browser
13
15
  from splinter.exceptions import ElementDoesNotExist
@@ -94,7 +96,7 @@ class ObjectsListMixin:
94
96
  """
95
97
  Click add item button on top of the items table list.
96
98
  """
97
- self.click_button("#add_button")
99
+ self.click_button("#add-button")
98
100
 
99
101
  def click_table_link(self, row=1, column=2):
100
102
  """By default, tries to click column next to checkbox to go to the details page."""
@@ -129,7 +131,7 @@ class ObjectDetailsMixin:
129
131
  By default, it's not using the exact match, because on the UI we're often adding
130
132
  additional tags, relationships or units.
131
133
  """
132
- panel_xpath = f'//*[@id="main"]//div[@class="card-header"][contains(normalize-space(), "{panel_label}")]/following-sibling::table'
134
+ panel_xpath = f'//*[@id="main"]//div[contains(@class, "card-header") and contains(normalize-space(), "{panel_label}")]/following-sibling::div[contains(@class, "collapse")]/table'
133
135
  value = self.browser.find_by_xpath(f'{panel_xpath}//td[text()="{field_label}"]/following-sibling::td[1]').text
134
136
 
135
137
  if exact_match:
@@ -144,7 +146,7 @@ class ObjectDetailsMixin:
144
146
  TODO: remove after all panels will be moved to UI Components Framework or new Bootstrap 5 templates.
145
147
  """
146
148
  panel_xpath = f'//*[@id="main"]//div[@class="card-header"][contains(normalize-space(), "{panel_label}")]'
147
- expand_button_xpath = f"{panel_xpath}/button[normalize-space()='Expand All']"
149
+ expand_button_xpath = f"{panel_xpath}/button[normalize-space()='Expand All Groups']"
148
150
  expand_button = self.browser.find_by_xpath(expand_button_xpath)
149
151
  if not expand_button.is_empty():
150
152
  expand_button.click()
@@ -374,10 +376,12 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
374
376
  sidenav_button = self.browser.find_by_xpath(f"{section_xpath}/button", wait_time=5)
375
377
  if not sidenav_button["aria-expanded"] == "true":
376
378
  sidenav_button.click()
377
- child_menu_xpath = f"{section_xpath}/div[@class='nb-sidenav-flyout']//a[@class='nb-sidenav-link' and normalize-space()='{child_menu_name}']"
379
+ child_menu_xpath = f"{section_xpath}/div[@class='nb-sidenav-flyout']//a[contains(@class, 'nb-sidenav-link') and normalize-space()='{child_menu_name}']"
378
380
  child_menu = self.browser.find_by_xpath(child_menu_xpath, wait_time=5)
381
+ old_url = self.browser.url
379
382
  child_menu.click()
380
383
 
384
+ WebDriverWait(self.browser, 30).until(lambda driver: driver.url != old_url)
381
385
  # Wait for body element to appear
382
386
  self.assertTrue(self.browser.is_element_present_by_tag("body", wait_time=5), "Page failed to load")
383
387
 
@@ -389,7 +393,7 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
389
393
  add_button.click()
390
394
 
391
395
  # Wait for body element to appear
392
- self.assertTrue(self.browser.is_element_present_by_tag("body", wait_time=5), "Page failed to load")
396
+ self.assertTrue(self.browser.is_element_present_by_name("_create", wait_time=5), "Page failed to load")
393
397
 
394
398
  def click_edit_form_create_button(self):
395
399
  """
@@ -399,7 +403,7 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
399
403
  add_button.click()
400
404
 
401
405
  # Wait for body element to appear
402
- self.assertTrue(self.browser.is_element_present_by_tag("body", wait_time=5), "Page failed to load")
406
+ self.assertTrue(self.browser.is_element_present_by_css(".alert-success", wait_time=5), "Page failed to load")
403
407
 
404
408
  def _fill_select2_field(self, field_name, value, search_box_class=None):
405
409
  """
@@ -453,17 +457,26 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
453
457
  search_box.first.type(Keys.ENTER)
454
458
 
455
459
  def click_button(self, query_selector):
456
- btn = self.browser.find_by_css(query_selector, wait_time=5)
460
+ self.browser.is_element_present_by_css(query_selector, wait_time=5)
457
461
  # Button might be visible but on the edge and then impossible to click due to vertical/horizontal scrolls
458
- self.browser.execute_script(f"document.querySelector('{query_selector}').scrollIntoView(true)")
462
+ self.browser.execute_script(
463
+ f"document.querySelector('{query_selector}').scrollIntoView({{ behavior: 'instant', block: 'start' }});"
464
+ )
465
+ # Scrolling may be asynchronous, wait until it's actually clickable.
466
+ WebDriverWait(self.browser.driver, 30).until(element_to_be_clickable((By.CSS_SELECTOR, query_selector)))
467
+ btn = self.browser.find_by_css(query_selector)
459
468
  btn.click()
460
469
 
461
470
  def fill_input(self, input_name, input_value):
462
471
  """
463
472
  Helper function to fill an input field. Solves issue with element could not be scrolled into view for some pages.
464
473
  """
465
- element = self.browser.find_by_name(input_name, wait_time=5)
466
- self.browser.execute_script("arguments[0].scrollIntoView(true);", element.first._element)
474
+ self.browser.is_element_present_by_name(input_name, wait_time=5)
475
+ 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
+ )
479
+ element.is_visible(wait_time=5)
467
480
  self.browser.execute_script("arguments[0].focus();", element.first._element)
468
481
  self.browser.fill(input_name, input_value)
469
482
 
@@ -19,6 +19,7 @@ from nautobot.core.testing import utils
19
19
  from nautobot.core.utils import permissions
20
20
  from nautobot.extras import management, models as extras_models
21
21
  from nautobot.extras.choices import JobResultStatusChoices
22
+ from nautobot.ipam.models import default_namespace_pk
22
23
  from nautobot.users import models as users_models
23
24
 
24
25
  # Use the proper swappable User model
@@ -81,6 +82,7 @@ class NautobotTestCaseMixin:
81
82
  """
82
83
  super().tearDown()
83
84
  cache.clear()
85
+ default_namespace_pk.set(None)
84
86
 
85
87
  def prepare_instance(self, instance):
86
88
  """
@@ -192,6 +192,10 @@ class ViewTestCases:
192
192
  with CaptureQueriesContext(connection) as capture_queries_context:
193
193
  response = self.client.get(instance.get_absolute_url())
194
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.
195
199
  self.assertBodyContains(response, escape(getattr(instance, "display", str(instance))))
196
200
 
197
201
  # If any Relationships are defined, they should appear in the response
@@ -1,44 +1,48 @@
1
+ from django.test import tag
2
+
1
3
  from nautobot.circuits.models import Circuit, Provider
2
4
  from nautobot.core.testing.integration import SeleniumTestCase
3
5
  from nautobot.dcim.models import Location, PowerFeed, PowerPanel
4
6
  from nautobot.tenancy.models import Tenant
5
7
 
6
- from example_app.models import ExampleModel
7
-
8
8
 
9
+ @tag("example_app")
9
10
  class AppHomeTestCase(SeleniumTestCase):
10
11
  """Integration test the Example App homepage extensions."""
11
12
 
12
- layout = {
13
- "Organization": {
14
- "Locations": {"model": Location, "permission": "dcim.view_location"},
15
- "Example Models": {"model": ExampleModel, "permission": "example_app.view_examplemodel"},
16
- "Tenants": {"model": Tenant, "permission": "tenancy.view_tenant"},
17
- },
18
- "Example App Standard Panel": {
19
- "Example App Custom Item": {"permission": "example_app.view_examplemodel"},
20
- },
21
- "Power": {
22
- "Power Feeds": {"model": PowerFeed, "permission": "dcim.view_powerfeed"},
23
- "Power Panel": {"model": PowerPanel, "permission": "dcim.view_powerpanel"},
24
- },
25
- "Circuits": {
26
- "Providers": {"model": Provider, "permission": "circuits.view_provider"},
27
- "Circuits": {"model": Circuit, "permission": "circuits.view_circuit"},
28
- },
29
- }
30
-
31
- custom_panel_examplemodel = {
32
- "name": "Example App Custom Panel",
33
- "items": [
34
- "Example 1",
35
- "Example 2",
36
- "Example 3",
37
- ],
38
- }
39
-
40
13
  def setUp(self):
41
14
  super().setUp()
15
+
16
+ from example_app.models import ExampleModel
17
+
18
+ self.layout = {
19
+ "Organization": {
20
+ "Locations": {"model": Location, "permission": "dcim.view_location"},
21
+ "Example Models": {"model": ExampleModel, "permission": "example_app.view_examplemodel"},
22
+ "Tenants": {"model": Tenant, "permission": "tenancy.view_tenant"},
23
+ },
24
+ "Example App Standard Panel": {
25
+ "Example App Custom Item": {"permission": "example_app.view_examplemodel"},
26
+ },
27
+ "Power": {
28
+ "Power Feeds": {"model": PowerFeed, "permission": "dcim.view_powerfeed"},
29
+ "Power Panel": {"model": PowerPanel, "permission": "dcim.view_powerpanel"},
30
+ },
31
+ "Circuits": {
32
+ "Providers": {"model": Provider, "permission": "circuits.view_provider"},
33
+ "Circuits": {"model": Circuit, "permission": "circuits.view_circuit"},
34
+ },
35
+ }
36
+
37
+ self.custom_panel_examplemodel = {
38
+ "name": "Example App Custom Panel",
39
+ "items": [
40
+ "Example 1",
41
+ "Example 2",
42
+ "Example 3",
43
+ ],
44
+ }
45
+
42
46
  self.login(self.user.username, self.password)
43
47
 
44
48
  def tearDown(self):
@@ -1,6 +1,9 @@
1
+ from django.test import tag
2
+
1
3
  from nautobot.core.testing.integration import SeleniumTestCase
2
4
 
3
5
 
6
+ @tag("example_app")
4
7
  class AppNavBarTestCase(SeleniumTestCase):
5
8
  """Integration test the navigation menu."""
6
9
 
@@ -0,0 +1,4 @@
1
+ from nautobot.core.tests.nautobot_config import * # noqa: F403
2
+
3
+ # Do not enable example apps
4
+ PLUGINS = []
@@ -39,11 +39,12 @@ class NautobotParallelTestSuite(ParallelTestSuite):
39
39
 
40
40
  class NautobotTestRunner(DiscoverRunner):
41
41
  """
42
- Custom test runner that excludes (slow) integration and migration tests by default.
42
+ Custom test runner that excludes (slow) integration and migration tests by default among others.
43
43
 
44
44
  This test runner is aware of our use of the "integration" tag and only runs integration tests if
45
45
  explicitly passed in with `nautobot-server test --tag integration`.
46
46
  Similarly, it only runs migration tests if explicitly called with `--tag migration_test`.
47
+ Similarly, it only runs tests that require the example app(s) if those are present in settings.PLUGINS.
47
48
 
48
49
  By Nautobot convention, integration tests must be tagged with "integration". The base
49
50
  `nautobot.core.testing.integration.SeleniumTestCase` has this tag, therefore any test cases
@@ -58,6 +59,10 @@ class NautobotTestRunner(DiscoverRunner):
58
59
  parallel_test_suite = NautobotParallelTestSuite
59
60
 
60
61
  exclude_tags = ["integration", "migration_test"]
62
+ if "example_app" not in settings.PLUGINS:
63
+ exclude_tags.append("example_app")
64
+ if "example_app_with_view_override" not in settings.PLUGINS:
65
+ exclude_tags.append("example_app_with_view_override")
61
66
 
62
67
  @classmethod
63
68
  def add_arguments(cls, parser):
@@ -157,6 +162,9 @@ class NautobotTestRunner(DiscoverRunner):
157
162
  db_command = [*command, "--database", alias]
158
163
  call_command(*db_command)
159
164
 
165
+ # Calculate membership for the dynamic groups that were generated by the factories/fixtures
166
+ call_command("refresh_dynamic_group_member_caches")
167
+
160
168
  if self.parallel > 1:
161
169
  for index in range(self.parallel):
162
170
  with time_keeper.timed(f" Cloning '{alias}'"):
@@ -533,10 +533,12 @@ class ModelViewSetMixinTest(testing.APITestCase):
533
533
  self.user.is_superuser = True
534
534
  self.user.save()
535
535
 
536
- # Default behavior - m2m fields included
536
+ # With exclude_m2m query parameter set to False
537
537
  view = self.SimpleIPAddressViewSet()
538
538
  view.action_map = {"get": "list"}
539
- request = APIRequestFactory().get(reverse("ipam-api:ipaddress-list"), headers=self.header)
539
+ request = APIRequestFactory().get(
540
+ reverse("ipam-api:ipaddress-list"), headers=self.header, data={"exclude_m2m": False}
541
+ )
540
542
  force_authenticate(request, user=self.user)
541
543
  request = view.initialize_request(request)
542
544
  view.setup(request)
@@ -561,7 +563,7 @@ class ModelViewSetMixinTest(testing.APITestCase):
561
563
  list(instance.vm_interfaces.all())
562
564
  list(instance.tags.all())
563
565
 
564
- # With exclude_m2m query parameter
566
+ # With exclude_m2m query parameter set to True
565
567
  view = self.SimpleIPAddressViewSet()
566
568
  view.action_map = {"get": "list"}
567
569
  request = APIRequestFactory().get(
@@ -296,8 +296,8 @@ class BreadcrumbsTestCase(TestCase):
296
296
  breadcrumbs = Breadcrumbs()
297
297
 
298
298
  # Should have defaults for list and details
299
- self.assertEqual(len(breadcrumbs.items["list"]), 2)
300
- self.assertEqual(len(breadcrumbs.items["detail"]), 3)
299
+ self.assertEqual(len(breadcrumbs.items["list"]), 0)
300
+ self.assertEqual(len(breadcrumbs.items["detail"]), 2)
301
301
 
302
302
  # Verify adding items
303
303
  new_item = BaseBreadcrumbItem()
@@ -306,7 +306,7 @@ class BreadcrumbsTestCase(TestCase):
306
306
  self.assertEqual(len(breadcrumbs.items["list"]), 1)
307
307
  self.assertEqual(breadcrumbs.items["list"][0], new_item)
308
308
 
309
- self.assertEqual(len(breadcrumbs.items["detail"]), 2)
309
+ self.assertEqual(len(breadcrumbs.items["detail"]), 1)
310
310
  self.assertEqual(breadcrumbs.items["detail"][0], new_item)
311
311
 
312
312
  self.assertEqual(len(breadcrumbs.items["custom_action"]), 1)
@@ -325,7 +325,7 @@ class BreadcrumbsTestCase(TestCase):
325
325
 
326
326
  # Other defaults should still exist
327
327
  self.assertIn("detail", breadcrumbs.items)
328
- self.assertEqual(len(breadcrumbs.items["detail"]), 3)
328
+ self.assertEqual(len(breadcrumbs.items["detail"]), 2)
329
329
 
330
330
  def test_get_items_from_action_static_method(self):
331
331
  """Test the _get_items_from_action static method."""
@@ -355,7 +355,6 @@ class BreadcrumbsTestCase(TestCase):
355
355
  breadcrumbs = Breadcrumbs()
356
356
  expected_items = [
357
357
  ("/dcim/location-types/", "Location Types"),
358
- (f"/dcim/location-types/{self.location_type.pk}/", str(self.location_type)),
359
358
  ]
360
359
 
361
360
  # Test with an action that doesn't exist but detail=True
@@ -366,13 +365,13 @@ class BreadcrumbsTestCase(TestCase):
366
365
  items = breadcrumbs.get_breadcrumbs_items(context)
367
366
 
368
367
  # Should get 2 items from detail fallback
369
- self.assertEqual(len(items), 2)
368
+ self.assertEqual(len(items), 1)
370
369
  self.assertEqual(items, expected_items)
371
370
 
372
371
  def test_render_method(self):
373
372
  """Test the render method."""
374
373
  breadcrumbs = Breadcrumbs()
375
- context = Context({"view_action": "list", "model": Device})
374
+ context = Context({"detail": True, "model": Device})
376
375
 
377
376
  html = breadcrumbs.render(context)
378
377
 
@@ -1,6 +1,7 @@
1
1
  from django.test import override_settings, TestCase
2
2
 
3
3
  from nautobot.core import checks
4
+ from nautobot.dcim.choices import DeviceUniquenessChoices
4
5
 
5
6
 
6
7
  class CheckCoreSettingsTest(TestCase):
@@ -42,3 +43,30 @@ class CheckCoreSettingsTest(TestCase):
42
43
  def test_check_maintenance_mode(self):
43
44
  """Error if MAINTENANCE_MODE is set and yet SESSION_ENGINE is still storing sessions in the db."""
44
45
  self.assertEqual(checks.check_maintenance_mode(None), [checks.E005])
46
+
47
+ @override_settings(
48
+ DEVICE_NAME_AS_NATURAL_KEY=True,
49
+ )
50
+ def test_check_deprecated_device_name_as_natural_key(self):
51
+ """Warn if DEVICE_NAME_AS_NATURAL_KEY is defined in settings."""
52
+ self.assertEqual(
53
+ checks.check_deprecated_device_name_as_natural_key(None),
54
+ [checks.W006],
55
+ )
56
+
57
+ @override_settings(
58
+ DEVICE_UNIQUENESS="invalid_value",
59
+ )
60
+ def test_check_invalid_device_uniqueness_value(self):
61
+ """Warn if DEVICE_UNIQUENESS is set to an invalid value."""
62
+ self.assertEqual(
63
+ checks.check_valid_value_for_device_uniqueness(None),
64
+ [checks.W007],
65
+ )
66
+
67
+ @override_settings(
68
+ DEVICE_UNIQUENESS=DeviceUniquenessChoices.NAME,
69
+ )
70
+ def test_check_valid_device_uniqueness_value(self):
71
+ """No warning if DEVICE_UNIQUENESS is set to a valid value."""
72
+ self.assertEqual(checks.check_valid_value_for_device_uniqueness(None), [])
@@ -0,0 +1,40 @@
1
+ from nautobot.core.cli import migrate_deprecated_templates
2
+ from nautobot.core.testing import TestCase
3
+
4
+
5
+ class TestMigrateTemplates(TestCase):
6
+ def test_template_replacements(self):
7
+ """Verify that all old templates are replaced by a single new template."""
8
+ audit_dict = {}
9
+ for new_template, old_templates in migrate_deprecated_templates.TEMPLATE_REPLACEMENTS.items():
10
+ for old_template in old_templates:
11
+ self.assertNotIn(old_template, audit_dict)
12
+ audit_dict[old_template] = new_template
13
+
14
+ def test_replace_template_references_no_change(self):
15
+ content = """
16
+ {% extends "base.html" %}
17
+ {% block content %}
18
+ <h1>Hello, World!</h1>
19
+ {% endblock %}
20
+ """
21
+ replaced_content, was_updated = migrate_deprecated_templates.replace_template_references(content)
22
+ self.assertFalse(was_updated)
23
+ self.assertEqual(replaced_content, content)
24
+
25
+ def test_replace_template_references(self):
26
+ original_content = """
27
+ {% extends "generic/object_bulk_import.html" %}
28
+ {% block content %}
29
+ <h1>Hello, World!</h1>
30
+ {% endblock %}
31
+ """
32
+ new_content = """
33
+ {% extends "generic/object_bulk_create.html" %}
34
+ {% block content %}
35
+ <h1>Hello, World!</h1>
36
+ {% endblock %}
37
+ """
38
+ replaced_content, was_updated = migrate_deprecated_templates.replace_template_references(original_content)
39
+ self.assertTrue(was_updated)
40
+ self.assertEqual(replaced_content, new_content)
@@ -1,7 +1,7 @@
1
1
  """Test cases for nautobot.core.config module."""
2
2
 
3
3
  from constance.test import override_config
4
- from django.test import override_settings, TestCase
4
+ from django.test import override_settings, tag, TestCase
5
5
 
6
6
  from nautobot.apps import config as app_config
7
7
  from nautobot.core.utils import config
@@ -37,6 +37,7 @@ class GetSettingsOrConfigTestCase(TestCase):
37
37
  self.assertRaises(AttributeError, config.get_settings_or_config, "FAKE_SETTING")
38
38
 
39
39
 
40
+ @tag("example_app")
40
41
  class GetAppSettingsOrConfigTestCase(TestCase):
41
42
  """Test the get_app_settings_or_config() helper function."""
42
43