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
@@ -1,21 +1,26 @@
1
+ import inspect
2
+ import sys
1
3
  from unittest import mock
2
4
 
3
5
  from django import forms as django_forms
6
+ from django.apps import apps as django_apps
4
7
  from django.contrib.contenttypes.models import ContentType
5
8
  from django.http import QueryDict
6
- from django.test import TestCase
9
+ from django.test import tag
7
10
  from django.urls import reverse
11
+ from django_filters.filterset import FilterSet
8
12
  from netaddr import IPNetwork
9
13
 
10
14
  from nautobot.core import filters, forms, testing
11
15
  from nautobot.core.utils import requests
16
+ from nautobot.core.utils.filtering import get_filterset_parameter_form_field
12
17
  from nautobot.dcim import filters as dcim_filters, forms as dcim_forms, models as dcim_models
13
18
  from nautobot.dcim.tests import test_views
14
19
  from nautobot.extras import filters as extras_filters, models as extras_models
15
20
  from nautobot.ipam import forms as ipam_forms, models as ipam_models
16
21
 
17
22
 
18
- class ExpandIPAddress(TestCase):
23
+ class ExpandIPAddress(testing.TestCase):
19
24
  """
20
25
  Validate the operation of expand_ipaddress_pattern().
21
26
  """
@@ -198,7 +203,7 @@ class ExpandIPAddress(TestCase):
198
203
  sorted(forms.expand_ipaddress_pattern("1.2.3.[4,,5]/32", 4))
199
204
 
200
205
 
201
- class ExpandAlphanumeric(TestCase):
206
+ class ExpandAlphanumeric(testing.TestCase):
202
207
  """
203
208
  Validate the operation of expand_alphanumeric_pattern().
204
209
  """
@@ -332,7 +337,7 @@ class ExpandAlphanumeric(TestCase):
332
337
  sorted(forms.expand_alphanumeric_pattern("r[a,,b]a"))
333
338
 
334
339
 
335
- class AddFieldToFormClassTest(TestCase):
340
+ class AddFieldToFormClassTest(testing.TestCase):
336
341
  def test_field_added(self):
337
342
  """
338
343
  Test adding of a new field to an existing form.
@@ -361,7 +366,7 @@ class AddFieldToFormClassTest(TestCase):
361
366
  )
362
367
 
363
368
 
364
- class DynamicModelChoiceFieldTest(TestCase):
369
+ class DynamicModelChoiceFieldTest(testing.TestCase):
365
370
  """Tests for DynamicModelChoiceField."""
366
371
 
367
372
  def setUp(self):
@@ -397,7 +402,7 @@ class DynamicModelChoiceFieldTest(TestCase):
397
402
  self.assertEqual(self.field_with_to_field_name.prepare_value(address), address.address)
398
403
 
399
404
 
400
- class DynamicModelMultipleChoiceFieldTest(TestCase):
405
+ class DynamicModelMultipleChoiceFieldTest(testing.TestCase):
401
406
  """Tests for DynamicModelMultipleChoiceField."""
402
407
 
403
408
  def setUp(self):
@@ -434,7 +439,7 @@ class DynamicModelMultipleChoiceFieldTest(TestCase):
434
439
  )
435
440
 
436
441
 
437
- class MultiValueCharFieldTest(TestCase):
442
+ class MultiValueCharFieldTest(testing.TestCase):
438
443
  def setUp(self):
439
444
  self.filter = filters.MultiValueCharFilter()
440
445
  self.field = forms.MultiValueCharField()
@@ -467,7 +472,7 @@ class MultiValueCharFieldTest(TestCase):
467
472
  )
468
473
 
469
474
 
470
- class NumericArrayFieldTest(TestCase):
475
+ class NumericArrayFieldTest(testing.TestCase):
471
476
  def setUp(self):
472
477
  super().setUp()
473
478
  # We need to use a field with required=False so we can test empty/None inputs
@@ -497,7 +502,7 @@ class NumericArrayFieldTest(TestCase):
497
502
  self.field.clean(test)
498
503
 
499
504
 
500
- class AddressFieldMixinTest(TestCase):
505
+ class AddressFieldMixinTest(testing.TestCase):
501
506
  """Test cases for the AddressFieldMixin."""
502
507
 
503
508
  def setUp(self):
@@ -527,7 +532,7 @@ class AddressFieldMixinTest(TestCase):
527
532
  mock_init.assert_called_with(initial=self.initial, instance=self.ip)
528
533
 
529
534
 
530
- class PrefixFieldMixinTest(TestCase):
535
+ class PrefixFieldMixinTest(testing.TestCase):
531
536
  """Test cases for the PrefixFieldMixin."""
532
537
 
533
538
  def setUp(self):
@@ -576,7 +581,7 @@ class JSONFieldTest(testing.TestCase):
576
581
  self.assertEqual('"I am UTF-8! 😀"', forms.JSONField().prepare_value("I am UTF-8! 😀"))
577
582
 
578
583
 
579
- class MultiMatchModelMultipleChoiceFieldTest(TestCase):
584
+ class MultiMatchModelMultipleChoiceFieldTest(testing.TestCase):
580
585
  def test_clean(self):
581
586
  field = forms.MultiMatchModelMultipleChoiceField(
582
587
  queryset=ipam_models.VLANGroup.objects.all(), to_field_name="name"
@@ -604,20 +609,57 @@ class MultiMatchModelMultipleChoiceFieldTest(TestCase):
604
609
  field.clean(value)
605
610
 
606
611
 
607
- class WidgetsTest(TestCase):
612
+ class WidgetsTest(testing.TestCase):
608
613
  def test_api_select_add_query_param_with_utf8(self):
609
614
  widget = forms.APISelect()
610
615
  widget.add_query_param("utf8", "I am UTF-8! 😀")
611
616
  self.assertEqual('["I am UTF-8! 😀"]', widget.attrs["data-query-param-utf8"])
612
617
 
613
618
 
614
- class DynamicFilterFormTest(TestCase):
619
+ class DynamicFilterFormTest(testing.TestCase):
620
+ def test_get_filterset_parameter_form_field_all_filters(self):
621
+ """
622
+ Test every FilterSet to validate that Plural names are correctly mapped in get_filterset_parameter_form_field.
623
+ """
624
+ filterset_classes = set()
625
+ for app_config in django_apps.get_app_configs():
626
+ try:
627
+ filters_mod = sys.modules.get(f"{app_config.name}.filters")
628
+ if not filters_mod:
629
+ continue
630
+ for _name, obj in inspect.getmembers(filters_mod):
631
+ if (
632
+ inspect.isclass(obj) # Check if obj is a class
633
+ and issubclass(obj, FilterSet) # Check if obj is a subclass of FilterSet
634
+ and obj is not FilterSet # Exclude the base FilterSet class itself
635
+ and getattr(getattr(obj, "_meta", None), "model", None)
636
+ is not None # Ensure the FilterSet has a model defined
637
+ ):
638
+ filterset_classes.add(obj)
639
+ except Exception as e:
640
+ # This test might start failing if an app's filters.py gets a design change.
641
+ self.fail(f"Error processing app '{app_config.name}': {e}")
642
+ for filterset_class in filterset_classes:
643
+ filterset = filterset_class()
644
+ model = filterset._meta.model
645
+ for filter_name in filterset.filters.keys():
646
+ try:
647
+ field = get_filterset_parameter_form_field(model, filter_name, filterset=filterset)
648
+ self.assertIsNotNone(field, "Field was unexpectedly None")
649
+ except KeyError as e:
650
+ self.fail(
651
+ f"A filter failed to operate due to mismatched plural name:"
652
+ f" Check MODEL_VERBOSE_NAME_PLURAL_TO_FEATURE_NAME_MAPPING:"
653
+ f" FilterClass: {filterset_class.__name__} name: {filter_name}: {e}"
654
+ )
655
+
615
656
  # TODO(timizuo): investigate why test fails on CI
616
657
  # def test_dynamic_filter_form_with_missing_attr(self):
617
658
  # with self.assertRaises(AttributeError) as err:
618
659
  # DynamicFilterForm()
619
660
  # self.assertEqual("'DynamicFilterForm' object requires `filterset_class` attribute", str(err.exception))
620
661
 
662
+ @tag("example_app")
621
663
  def test_dynamic_filter_form(self):
622
664
  form = forms.DynamicFilterForm(filterset=extras_filters.StatusFilterSet())
623
665
  location_form = forms.DynamicFilterForm(filterset=dcim_filters.LocationFilterSet())
@@ -17,11 +17,12 @@ from nautobot.core.jobs.cleanup import CleanupTypes
17
17
  from nautobot.core.testing import create_job_result_and_run_job, TransactionTestCase
18
18
  from nautobot.core.testing.context import load_event_broker_override_settings
19
19
  from nautobot.dcim.models import Device, DeviceType, Location, LocationType, Manufacturer
20
- from nautobot.extras.choices import JobResultStatusChoices, LogLevelChoices
20
+ from nautobot.extras.choices import DynamicGroupTypeChoices, JobResultStatusChoices, LogLevelChoices
21
21
  from nautobot.extras.factory import JobResultFactory, ObjectChangeFactory
22
22
  from nautobot.extras.models import (
23
23
  Contact,
24
24
  ContactAssociation,
25
+ DynamicGroup,
25
26
  ExportTemplate,
26
27
  FileProxy,
27
28
  JobLogEntry,
@@ -1248,3 +1249,76 @@ class BulkDeleteTestCase(TransactionTestCase):
1248
1249
  saved_view_id=None,
1249
1250
  )
1250
1251
  self._common_no_error_test_assertion(Role, job_result, name__istartswith="Example Status")
1252
+
1253
+
1254
+ class RefreshDynamicGroupCacheJobButtonReceiverTestCase(TransactionTestCase):
1255
+ def setUp(self):
1256
+ super().setUp()
1257
+ self.job_module = "nautobot.core.jobs.groups"
1258
+ self.job_name = "RefreshDynamicGroupCacheJobButtonReceiver"
1259
+
1260
+ def test_successful_cache_refresh(self):
1261
+ LocationType.objects.create(name="DG Test LT 1")
1262
+ LocationType.objects.create(name="DG Test LT 2")
1263
+ LocationType.objects.create(name="DG Test LT 3")
1264
+ dg = DynamicGroup(
1265
+ name="Location Types",
1266
+ content_type=ContentType.objects.get_for_model(LocationType),
1267
+ group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER,
1268
+ filter={"name__isw": ["DG Test"]},
1269
+ )
1270
+ dg.clean()
1271
+ dg.save(update_cached_members=False)
1272
+ self.assertEqual(0, dg.count)
1273
+
1274
+ job_result = create_job_result_and_run_job(
1275
+ self.job_module,
1276
+ self.job_name,
1277
+ object_model_name="extras.dynamicgroup",
1278
+ object_pk=dg.pk,
1279
+ )
1280
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_SUCCESS)
1281
+ self.assertEqual(3, dg.count)
1282
+
1283
+ dg.filter = {"name__iew": ["DG Test"]}
1284
+ dg.clean()
1285
+ dg.save(update_cached_members=False)
1286
+ self.assertEqual(3, dg.count)
1287
+ job_result = create_job_result_and_run_job(
1288
+ self.job_module,
1289
+ self.job_name,
1290
+ object_model_name="extras.dynamicgroup",
1291
+ object_pk=dg.pk,
1292
+ )
1293
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_SUCCESS)
1294
+ self.assertEqual(0, dg.count)
1295
+
1296
+ def test_failure_on_non_dg(self):
1297
+ job_result = create_job_result_and_run_job(
1298
+ self.job_module,
1299
+ self.job_name,
1300
+ object_model_name="extras.status",
1301
+ object_pk=Status.objects.first().pk,
1302
+ )
1303
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
1304
+ log_fail = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_FAILURE)
1305
+ self.assertEqual(log_fail.message, "This job button should only be used with Dynamic Group records.")
1306
+
1307
+ def test_failure_on_static_dg(self):
1308
+ dg = DynamicGroup.objects.create(
1309
+ name="Location Types",
1310
+ content_type=ContentType.objects.get_for_model(LocationType),
1311
+ group_type=DynamicGroupTypeChoices.TYPE_STATIC,
1312
+ )
1313
+ job_result = create_job_result_and_run_job(
1314
+ self.job_module,
1315
+ self.job_name,
1316
+ object_model_name="extras.dynamicgroup",
1317
+ object_pk=dg.pk,
1318
+ )
1319
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
1320
+ log_fail = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_FAILURE)
1321
+ self.assertEqual(
1322
+ log_fail.message,
1323
+ "The members of this Dynamic Group are statically defined and do not need to be recalculated.",
1324
+ )
@@ -13,6 +13,7 @@ from unittest import mock, TestCase
13
13
 
14
14
  from django import __version__ as django_version
15
15
  from django.conf import settings
16
+ from django.test import tag
16
17
 
17
18
  from nautobot import __version__ as nautobot_version
18
19
 
@@ -101,6 +102,7 @@ class NautobotServerTestCase(TestCase):
101
102
 
102
103
  self.assertNotEqual(secret_key_1, secret_key_2)
103
104
 
105
+ @tag("example_app")
104
106
  def test_settings_processing(self):
105
107
  result = subprocess.run(
106
108
  ["nautobot-server", "--config", settings.SETTINGS_PATH, "print_settings"],
@@ -1,9 +1,15 @@
1
+ import os
2
+
3
+ from django.apps import apps
1
4
  from django.test import tag, TestCase
2
5
  from django.urls import resolve
3
6
 
7
+ from nautobot.core.apps import NavMenuTab
4
8
  from nautobot.core.choices import ButtonActionColorChoices, ButtonActionIconChoices
5
9
  from nautobot.core.testing.utils import get_expected_menu_item_name
10
+ from nautobot.core.ui.choices import NavigationIconChoices, NavigationWeightChoices
6
11
  from nautobot.core.utils.lookup import get_route_for_model
12
+ from nautobot.core.utils.module_loading import import_string_optional
7
13
  from nautobot.core.utils.permissions import get_permission_for_model
8
14
  from nautobot.extras.registry import registry
9
15
 
@@ -45,7 +51,8 @@ class NavMenuTestCase(TestCase):
45
51
  except AttributeError:
46
52
  # Not a model view?
47
53
  self.assertIn(
48
- item_details["name"], {"Apps Marketplace", "Installed Apps", "Interface Connections"}
54
+ item_details["name"],
55
+ {"Apps Marketplace", "Installed Apps", "Interface Connections", "Device Constraints"},
49
56
  )
50
57
 
51
58
  for button, button_details in item_details["buttons"].items():
@@ -81,3 +88,71 @@ class NavMenuTestCase(TestCase):
81
88
  else:
82
89
  expected_perms[tab_name] |= group_perms
83
90
  self.assertEqual(expected_perms[tab_name], tab_details["permissions"])
91
+
92
+ def test_nav_menu_tabs_have_icon_and_weight(self):
93
+ """Ensure each NavMenuTab in every navigation.py has an icon and weight set, and any duplicates by name match."""
94
+ tabs_by_name = {}
95
+ for app in apps.get_app_configs():
96
+ if not app.name.startswith("nautobot."):
97
+ continue
98
+ nav_path = f"{app.name}.navigation.menu_items"
99
+ menu_items = import_string_optional(nav_path)
100
+ if menu_items is None:
101
+ continue
102
+ for tab in menu_items:
103
+ if not isinstance(tab, NavMenuTab):
104
+ raise TypeError(f"Expected NavMenuTab instance in {nav_path}, got {type(tab)}")
105
+ tab_name = tab.name
106
+ icon = tab.icon
107
+ weight = tab.weight
108
+ with self.subTest(tab_name=tab_name, nav_path=nav_path):
109
+ self.assertIsNotNone(tab_name, f"Tab in {nav_path} missing 'name'")
110
+ self.assertIsNotNone(icon, f"Tab '{tab_name}' in {nav_path} missing 'icon'")
111
+ self.assertIsNotNone(weight, f"Tab '{tab_name}' in {nav_path} missing 'weight'")
112
+ if tab_name in tabs_by_name:
113
+ prev_icon, prev_weight, prev_path = tabs_by_name[tab_name]
114
+ self.assertEqual(
115
+ icon,
116
+ prev_icon,
117
+ f"Tab '{tab_name}' has inconsistent icons: '{icon}' in {nav_path} vs '{prev_icon}' in {prev_path}",
118
+ )
119
+ self.assertEqual(
120
+ weight,
121
+ prev_weight,
122
+ f"Tab '{tab_name}' has inconsistent weights: '{weight}' in {nav_path} vs '{prev_weight}' in {prev_path}",
123
+ )
124
+ else:
125
+ tabs_by_name[tab_name] = (icon, weight, nav_path)
126
+
127
+ def test_icon_and_weight_class_attributes_match(self):
128
+ """
129
+ Ensure every class attribute in NavigationIconChoices is also in NavigationWeightChoices and vice versa.
130
+ If not, print the missing/extra attributes for easier debugging.
131
+ """
132
+ icon_attrs = {attr for attr in dir(NavigationIconChoices) if attr.isupper()}
133
+ weight_attrs = {attr for attr in dir(NavigationWeightChoices) if attr.isupper()}
134
+
135
+ only_in_icons = sorted(icon_attrs - weight_attrs)
136
+ only_in_weights = sorted(weight_attrs - icon_attrs)
137
+
138
+ if only_in_icons or only_in_weights:
139
+ msg = []
140
+ if only_in_icons:
141
+ msg.append(f"Class attributes only in NavigationIconChoices: {only_in_icons}")
142
+ if only_in_weights:
143
+ msg.append(f"Class attributes only in NavigationWeightChoices: {only_in_weights}")
144
+ self.fail("\n".join(msg))
145
+
146
+ def test_navigation_icons_have_svg(self):
147
+ """Ensure every NavigationIconChoices icon has a corresponding SVG file."""
148
+ missing = []
149
+ svg_dir = os.path.abspath(
150
+ os.path.join(os.path.dirname(__file__), "..", "..", "project-static", "nautobot-icons")
151
+ )
152
+ icon_attrs = [attr for attr in dir(NavigationIconChoices) if attr.isupper() and not attr == "CHOICES"]
153
+ for icon_attr in icon_attrs:
154
+ icon_name = getattr(NavigationIconChoices, icon_attr)
155
+ svg_path = os.path.join(svg_dir, f"{icon_name}.svg")
156
+ if not os.path.isfile(svg_path):
157
+ missing.append(svg_path)
158
+ self.assertFalse(missing, f"Missing SVG files for NavigationIconChoices: {missing}")
@@ -0,0 +1,42 @@
1
+ """
2
+ Test suite for social_django storage patch.
3
+
4
+ This tests that the monkeypatch correctly replaces the vulnerable create_user
5
+ method with the secure version that raises AuthAlreadyAssociated instead of
6
+ silently returning an existing user.
7
+
8
+ Please see nautobot/core/utils/patch_social_django.py for details on the patch.
9
+ """
10
+
11
+ from unittest.mock import MagicMock, patch
12
+
13
+ from nautobot.core.testing import TestCase
14
+
15
+
16
+ class PatchSocialDjangoTestCase(TestCase):
17
+ def test_django_storage_has_patch_at_import_time(self):
18
+ """
19
+ Test that importing DjangoStorage gives us the patched version.
20
+
21
+ This verifies that the patch applied in CoreConfig.ready() persists
22
+ and affects all imports of DjangoStorage throughout the application.
23
+ """
24
+ from django.db.utils import IntegrityError
25
+ from social_core.exceptions import AuthAlreadyAssociated
26
+ from social_django.models import DjangoStorage
27
+
28
+ # Mock user model to trigger IntegrityError
29
+ mock_user_model = MagicMock()
30
+ mock_manager = MagicMock()
31
+ mock_manager.create_user.side_effect = IntegrityError("duplicate key")
32
+ mock_user_model._default_manager = mock_manager
33
+
34
+ # Patch username_field and user_model methods to return our mock user model
35
+ with patch.object(DjangoStorage.user, "username_field", return_value="username"):
36
+ with patch.object(DjangoStorage.user, "user_model", return_value=mock_user_model):
37
+ # Should raise AuthAlreadyAssociated (patched behavior)
38
+ with self.assertRaises(AuthAlreadyAssociated):
39
+ DjangoStorage.user.create_user(username="test", email="test@example.com")
40
+
41
+ # Verify vulnerable get() not called
42
+ mock_manager.get.assert_not_called()
@@ -1,4 +1,4 @@
1
- from django.test import TestCase
1
+ from django.test import tag, TestCase
2
2
 
3
3
  from nautobot.circuits.models import Circuit
4
4
  from nautobot.circuits.tables import CircuitTable
@@ -32,6 +32,7 @@ class TableTestCase(TestCase):
32
32
  )
33
33
  self.assertEqual(list(table_queryset_data), list(sorted_queryset))
34
34
 
35
+ @tag("example_app")
35
36
  def test_tree_model_table_orderable(self):
36
37
  """Assert TreeNode model table are orderable."""
37
38
  location_type = LocationType.objects.get(name="Campus")
@@ -107,6 +108,7 @@ class TableTestCase(TestCase):
107
108
  )
108
109
  self.assertEqual(list(table_queryset_data), list(sorted_queryset))
109
110
 
111
+ @tag("example_app")
110
112
  def test_base_table_apis(self):
111
113
  """
112
114
  Test BaseTable APIs, specifically visible_columns and configurable_columns.
@@ -1,14 +1,16 @@
1
+ from unittest import mock
2
+
1
3
  from constance.test import override_config
2
4
  from django.conf import settings
5
+ from django.contrib.staticfiles.testing import StaticLiveServerTestCase
3
6
  from django.templatetags.static import static
4
- from django.test import override_settings, TestCase
7
+ from django.test import override_settings, tag
5
8
 
6
9
  from nautobot.core.templatetags import helpers
10
+ from nautobot.core.testing import TestCase
7
11
  from nautobot.dcim import models
8
12
  from nautobot.ipam.models import VLAN
9
13
 
10
- from example_app.models import AnotherExampleModel, ExampleModel
11
-
12
14
 
13
15
  class NautobotTemplatetagsHelperTest(TestCase):
14
16
  def test_hyperlinked_object(self):
@@ -132,6 +134,7 @@ class NautobotTemplatetagsHelperTest(TestCase):
132
134
  )
133
135
  self.assertEqual("utf8:\n- 😀😀\n- 😀\n", helpers.render_yaml({"utf8": ["😀😀", "😀"]}, False))
134
136
 
137
+ @tag("example_app")
135
138
  def test_meta(self):
136
139
  location = models.Location.objects.first()
137
140
 
@@ -139,31 +142,42 @@ class NautobotTemplatetagsHelperTest(TestCase):
139
142
  self.assertEqual(helpers.meta(models.Location, "app_label"), "dcim")
140
143
  self.assertEqual(helpers.meta(location, "not_present"), "")
141
144
 
145
+ from example_app.models import ExampleModel
146
+
142
147
  self.assertEqual(helpers.meta(ExampleModel, "app_label"), "example_app")
143
148
 
149
+ @tag("example_app")
144
150
  def test_viewname(self):
145
151
  location = models.Location.objects.first()
146
152
 
147
153
  self.assertEqual(helpers.viewname(location, "edit"), "dcim:location_edit")
148
154
  self.assertEqual(helpers.viewname(models.Location, "test"), "dcim:location_test")
149
155
 
156
+ from example_app.models import ExampleModel
157
+
150
158
  self.assertEqual(helpers.viewname(ExampleModel, "edit"), "plugins:example_app:examplemodel_edit")
151
159
 
160
+ @tag("example_app")
152
161
  def test_validated_viewname(self):
153
162
  location = models.Location.objects.first()
154
163
 
155
164
  self.assertEqual(helpers.validated_viewname(location, "list"), "dcim:location_list")
156
165
  self.assertIsNone(helpers.validated_viewname(models.Location, "notvalid"))
157
166
 
167
+ from example_app.models import ExampleModel
168
+
158
169
  self.assertEqual(helpers.validated_viewname(ExampleModel, "list"), "plugins:example_app:examplemodel_list")
159
170
  self.assertIsNone(helpers.validated_viewname(ExampleModel, "notvalid"))
160
171
 
172
+ @tag("example_app")
161
173
  def test_validated_api_viewname(self):
162
174
  location = models.Location.objects.first()
163
175
 
164
176
  self.assertEqual(helpers.validated_api_viewname(location, "list"), "dcim-api:location-list")
165
177
  self.assertIsNone(helpers.validated_api_viewname(models.Location, "notvalid"))
166
178
 
179
+ from example_app.models import ExampleModel
180
+
167
181
  self.assertEqual(
168
182
  helpers.validated_api_viewname(ExampleModel, "list"), "plugins-api:example_app-api:examplemodel-list"
169
183
  )
@@ -201,16 +215,6 @@ class NautobotTemplatetagsHelperTest(TestCase):
201
215
  self.assertEqual(helpers.percentage(2, 10), 20)
202
216
  self.assertEqual(helpers.percentage(10, 3), 333)
203
217
 
204
- def test_get_docs_url(self):
205
- self.assertTrue(callable(helpers.get_docs_url))
206
- location = models.Location.objects.first()
207
- self.assertEqual(helpers.get_docs_url(location), static("docs/user-guide/core-data-model/dcim/location.html"))
208
- example_model = ExampleModel.objects.create(name="test", number=1)
209
- self.assertEqual(helpers.get_docs_url(example_model), static("example_app/docs/models/examplemodel.html"))
210
- # AnotherExampleModel does not have documentation.
211
- another_model = AnotherExampleModel.objects.create(name="test", number=1)
212
- self.assertIsNone(helpers.get_docs_url(another_model))
213
-
214
218
  def test_has_perms(self):
215
219
  self.assertTrue(callable(helpers.has_perms))
216
220
  # TODO add unit tests for has_perms
@@ -265,6 +269,7 @@ class NautobotTemplatetagsHelperTest(TestCase):
265
269
  # Assert when obj is None
266
270
  self.assertEqual(helpers.hyperlinked_object_with_color(obj=None), '<span class="text-secondary">&mdash;</span>')
267
271
 
272
+ @tag("example_app")
268
273
  @override_settings(BANNER_TOP="¡Hola, mundo!")
269
274
  @override_config(example_app__SAMPLE_VARIABLE="Testing")
270
275
  def test_settings_or_config(self):
@@ -338,3 +343,38 @@ class NautobotTemplatetagsHelperTest(TestCase):
338
343
  "-85 dBm",
339
344
  )
340
345
  self.assertEqual(helpers.dbm(None), helpers.placeholder(None))
346
+
347
+
348
+ @tag("test")
349
+ class NautobotStaticDocsTestCase(StaticLiveServerTestCase):
350
+ @tag("example_app")
351
+ def test_get_docs_url(self):
352
+ self.assertTrue(callable(helpers.get_docs_url))
353
+ location_type = models.LocationType.objects.create(name="Some Location Type")
354
+ self.assertEqual(
355
+ helpers.get_docs_url(location_type), static("docs/user-guide/core-data-model/dcim/locationtype.html")
356
+ )
357
+
358
+ from example_app.models import AnotherExampleModel, ExampleModel
359
+
360
+ example_model = ExampleModel.objects.create(name="test", number=1)
361
+ self.assertEqual(helpers.get_docs_url(example_model), "/docs/example-app/models/examplemodel.html")
362
+ # AnotherExampleModel does not have documentation.
363
+ another_model = AnotherExampleModel.objects.create(name="test", number=1)
364
+ self.assertIsNone(helpers.get_docs_url(another_model))
365
+
366
+ @tag("example_app")
367
+ @mock.patch("nautobot.core.templatetags.helpers.find", return_value=False)
368
+ @mock.patch("nautobot.core.templatetags.helpers.resources.files", side_effect=ModuleNotFoundError)
369
+ def test_get_docs_url_module_not_found_and_no_static_file(self, mock_files, mock_find):
370
+ # Force `resources.files()` to raise ModuleNotFoundError to simulate a plugin
371
+ # that is listed in settings.PLUGINS but doesn't actually exist on disk.
372
+ # This ensures the `except ModuleNotFoundError` branch is covered.
373
+ from example_app.models import ExampleModel
374
+
375
+ example_model = ExampleModel.objects.create(name="test", number=1)
376
+ result = helpers.get_docs_url(example_model)
377
+ self.assertIsNone(result)
378
+
379
+ mock_files.assert_called_once()
380
+ mock_find.assert_called_once()
@@ -65,7 +65,7 @@ class NautobotTemplatetagsUIComponentsTest(TestCase):
65
65
  {
66
66
  "list_url": "home",
67
67
  "title": "New Home",
68
- "view_action": "list",
68
+ "detail": True,
69
69
  "breadcrumbs": Breadcrumbs(),
70
70
  }
71
71
  )
@@ -98,7 +98,7 @@ class NautobotTemplatetagsUIComponentsTest(TestCase):
98
98
  {
99
99
  "list_url": "home",
100
100
  "title": "New Home",
101
- "view_action": "list",
101
+ "detail": True,
102
102
  "breadcrumbs": Breadcrumbs(),
103
103
  }
104
104
  )
@@ -126,7 +126,7 @@ class NautobotTemplatetagsUIComponentsTest(TestCase):
126
126
  {
127
127
  "list_url": "home",
128
128
  "title": "New Home",
129
- "view_action": "list",
129
+ "detail": True,
130
130
  "breadcrumbs": Breadcrumbs(),
131
131
  }
132
132
  )
@@ -152,7 +152,7 @@ class NautobotTemplatetagsUIComponentsTest(TestCase):
152
152
  {
153
153
  "list_url": "home",
154
154
  "title": "New Home",
155
- "view_action": "list",
155
+ "detail": True,
156
156
  "breadcrumbs": Breadcrumbs(),
157
157
  }
158
158
  )
@@ -13,8 +13,21 @@ class TestInvalidateMaxTreeDepthSignal(TestCase):
13
13
  # Ensure that the max_depth hasn't already been cached
14
14
  Location.objects.__dict__.pop("max_depth", None)
15
15
  location = Location.objects.first()
16
- with self.assertNumQueries(1):
16
+
17
+ with CaptureQueriesContext(connection) as ctx:
17
18
  location.save()
19
+ captured_tree_cte_queries = [
20
+ query["sql"] for query in ctx.captured_queries if "WITH RECURSIVE" in query["sql"]
21
+ ]
22
+ allowed_number_of_tree_queries = 0 # We don't expect any tree queries to be run
23
+ _query_separator = "\n" + ("-" * 10) + "\n" + "NEXT QUERY" + "\n" + ("-" * 10)
24
+ self.assertEqual(
25
+ len(captured_tree_cte_queries),
26
+ allowed_number_of_tree_queries,
27
+ f"The CTE tree was calculated a different number of times ({len(captured_tree_cte_queries)})"
28
+ f" than allowed ({allowed_number_of_tree_queries})."
29
+ f" The following queries were used:\n{_query_separator.join(captured_tree_cte_queries)}",
30
+ )
18
31
 
19
32
 
20
33
  class QuerySetAncestorTests(TestCase):
@@ -446,7 +446,7 @@ class ObjectDetailContentExtraTabsTest(TestCase):
446
446
  self.factory = RequestFactory()
447
447
  self.request = self.factory.get("/")
448
448
  self.request.user = self.user
449
- self.default_tabs_id = ["main", "advanced", "contacts", "dynamic_groups", "object_metadata"]
449
+ self.default_tabs_id = ["main", "advanced", "contacts", "dynamic_groups", "object_metadata", "data_compliance"]
450
450
 
451
451
  def test_default_extra_tabs_exist(self):
452
452
  """