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
@@ -135,6 +135,61 @@ def _fix_nav_tabs_items(html: str, stats: dict, file_path=None) -> str:
135
135
  return pattern.sub(ul_replacer, html)
136
136
 
137
137
 
138
+ def _fix_dropdown_lis(html: str, stats: dict, file_path=None) -> str:
139
+ """Adds 'dropdown-item' class to all <li><a>...</a></li> tags in the given HTML string."""
140
+
141
+ def a_replacer(match):
142
+ a_tag = match.group(0)
143
+ # If Django template logic is found, notify user and skip auto-fix
144
+ if re.search(r"{%.*%}", a_tag):
145
+ if "manual_nav_template_lines" not in stats:
146
+ stats["manual_nav_template_lines"] = []
147
+ # Get line number and character position of li_tag
148
+ html_lines = html.splitlines()
149
+ for i, line in enumerate(html_lines):
150
+ if a_tag in line:
151
+ # Append line number, character position and file path for easier identification
152
+ stats["manual_nav_template_lines"].append(
153
+ f"{file_path}:{i + 1}:{line.index(a_tag)} - Please review manually '{a_tag}'"
154
+ )
155
+ break
156
+ else:
157
+ stats["manual_nav_template_lines"].append(f"{file_path} - Please review manually '{a_tag}'")
158
+ return a_tag
159
+
160
+ # Add dropdown-item to <a>
161
+ class_attr_match = re.search(r"""class=(["'])(.*?)\1""", a_tag)
162
+ if class_attr_match:
163
+ classes = class_attr_match.group(2).split()
164
+ if not any("dropdown-item" in _class for _class in classes):
165
+ classes.append("dropdown-item")
166
+ stats["dropdown_items"] += 1
167
+ new_class_attr = f'class="{" ".join(classes)}"'
168
+ a_tag = re.sub(r"""class=(["'])(.*?)\1""", new_class_attr, a_tag, count=1)
169
+ else:
170
+ a_tag = re.sub(r"<a(\s|>)", r'<a class="dropdown-item"\1', a_tag, count=1)
171
+ stats["dropdown_items"] += 1
172
+
173
+ return a_tag
174
+
175
+ return re.sub("<a[^>]*>.*?</a>", a_replacer, html, flags=re.DOTALL)
176
+
177
+
178
+ def _fix_dropdown_items(html: str, stats: dict, file_path=None) -> str:
179
+ """Ensures that all <li> elements within <ul class="dropdown-menu"> have class="dropdown-item" as appropriate."""
180
+
181
+ def ul_replacer(ul_match):
182
+ ul_tag = ul_match.group(0)
183
+ ul_tag_new = _fix_dropdown_lis(ul_tag, stats, file_path=file_path)
184
+ return ul_tag_new
185
+
186
+ pattern = re.compile(
187
+ r"""<ul\s+class=(["'])(?:[^"']*\s)?dropdown-menu(?:\s[^"']*)?\1[^>]*>.*?</ul>""",
188
+ re.DOTALL | re.IGNORECASE,
189
+ )
190
+ return pattern.sub(ul_replacer, html)
191
+
192
+
138
193
  def _fix_extra_nav_tabs_block(html_string: str, stats: dict, file_path: str) -> str:
139
194
  """
140
195
  Finds {% block extra_nav_tabs %} blocks and adds nav-item/nav-link to <li> tags inside using regex.
@@ -505,6 +560,7 @@ def convert_bootstrap_classes(html_input: str, file_path: str) -> tuple[str, dic
505
560
  "extra_breadcrumbs": 0,
506
561
  "breadcrumb_items": 0,
507
562
  "nav_items": 0,
563
+ "dropdown_items": 0,
508
564
  "panel_classes": 0,
509
565
  "manual_nav_template_lines": [],
510
566
  }
@@ -521,6 +577,7 @@ def convert_bootstrap_classes(html_input: str, file_path: str) -> tuple[str, dic
521
577
  "checkbox-inline": "form-check-input",
522
578
  "close": "btn-close",
523
579
  "control-label": "col-form-label",
580
+ "dropdown-menu-right": "dropdown-menu-end",
524
581
  "form-control-static": "form-control-plaintext",
525
582
  "form-group": "mb-10 d-flex justify-content-center",
526
583
  "help-block": "form-text",
@@ -582,6 +639,7 @@ def convert_bootstrap_classes(html_input: str, file_path: str) -> tuple[str, dic
582
639
  current_html = _convert_caret_in_span_to_mdi(current_html, stats)
583
640
  current_html = _convert_hover_copy_buttons(current_html, stats)
584
641
  current_html = _fix_nav_tabs_items(current_html, stats, file_path=file_path)
642
+ current_html = _fix_dropdown_items(current_html, stats, file_path=file_path)
585
643
 
586
644
  return current_html, stats
587
645
 
@@ -598,7 +656,15 @@ def fix_html_files_in_directory(directory: str, resize=False) -> None:
598
656
 
599
657
  totals = {
600
658
  k: 0
601
- for k in ["replacements", "extra_breadcrumbs", "breadcrumb_items", "nav_items", "panel_classes", "resizing_xs"]
659
+ for k in [
660
+ "replacements",
661
+ "extra_breadcrumbs",
662
+ "breadcrumb_items",
663
+ "nav_items",
664
+ "dropdown_items",
665
+ "panel_classes",
666
+ "resizing_xs",
667
+ ]
602
668
  }
603
669
  # Breakpoints that are not xs do not count as failures in djlint, so we keep a separate counter
604
670
  resizing_other = 0
@@ -663,6 +729,8 @@ def fix_html_files_in_directory(directory: str, resize=False) -> None:
663
729
  print(f"{stats['breadcrumb_items']} breadcrumb-items, ", end="")
664
730
  if stats["nav_items"]:
665
731
  print(f"{stats['nav_items']} nav-items, ", end="")
732
+ if stats["dropdown_items"]:
733
+ print(f"{stats['dropdown_items']} dropdown-items, ", end="")
666
734
  if stats["panel_classes"]:
667
735
  print(f"{stats['panel_classes']} panel replacements, ", end="")
668
736
  print()
@@ -683,6 +751,7 @@ def fix_html_files_in_directory(directory: str, resize=False) -> None:
683
751
  print(f"- Extra-breadcrumb fixes: {totals['extra_breadcrumbs']}")
684
752
  print(f"- <li> in <ol.breadcrumb>: {totals['breadcrumb_items']}")
685
753
  print(f"- <li> in <ul.nav-tabs>: {totals['nav_items']}")
754
+ print(f"- <a> in <ul.dropdown-menu>: {totals['dropdown_items']}")
686
755
  print(f"- Panel class replacements: {totals['panel_classes']}")
687
756
  print(f"- Resizing breakpoint xs: {totals['resizing_xs']}")
688
757
  print("-------------------------------------")
@@ -0,0 +1,200 @@
1
+ import argparse
2
+ import os
3
+ import re
4
+
5
+ TEMPLATE_REPLACEMENTS = {
6
+ # Format: new_template: [old_template1, old_template2, ...]
7
+ "circuits/circuit_create.html": ["circuits/circuit_edit.html"],
8
+ "circuits/circuittermination_create.html": ["circuits/circuittermination_edit.html"],
9
+ "circuits/provider_create.html": ["circuits/provider_edit.html"],
10
+ "circuits/provider_retrieve.html": ["circuits/provider.html"],
11
+ "dcim/cable_retrieve.html": ["dcim/cable.html"],
12
+ "dcim/cable_update.html": ["dcim/cable_edit.html"],
13
+ "dcim/device_create.html": ["dcim/device_edit.html"],
14
+ "dcim/devicetype_update.html": ["dcim/devicetype_edit.html"],
15
+ "dcim/location_retrieve.html": ["dcim/location.html"],
16
+ "dcim/location_update.html": ["dcim/location_edit.html"],
17
+ "dcim/rack_retrieve.html": ["dcim/rack.html"],
18
+ "dcim/rack_update.html": ["dcim/rack_edit.html"],
19
+ "dcim/rackreservation_retrieve.html": ["dcim/rackreservation.html"],
20
+ "dcim/virtualchassis_create.html": ["dcim/virtualchassis_add.html"],
21
+ "extras/configcontext_update.html": ["extras/configcontext_edit.html"],
22
+ "extras/configcontextschema_retrieve.html": ["extras/configcontextschema.html"],
23
+ "extras/configcontextschema_update.html": ["extras/configcontextschema_edit.html"],
24
+ "extras/customfield_update.html": ["extras/customfield_edit.html"],
25
+ "extras/dynamicgroup_retrieve.html": ["extras/dynamicgroup.html"],
26
+ "extras/dynamicgroup_update.html": ["extras/dynamicgroup_edit.html"],
27
+ "extras/gitrepository_retrieve.html": ["extras/gitrepository.html"],
28
+ "extras/gitrepository_update.html": ["extras/gitrepository_object_edit.html"],
29
+ "extras/graphqlquery_retrieve.html": ["extras/graphqlquery.html"],
30
+ "extras/jobresult_retrieve.html": ["extras/jobresult.html"],
31
+ "extras/note_retrieve.html": ["extras/note.html"],
32
+ "extras/objectchange_retrieve.html": ["extras/objectchange.html"],
33
+ "extras/secretsgroup_update.html": ["extras/secretsgroup_edit.html"],
34
+ "extras/tag_update.html": ["extras/tag_edit.html"],
35
+ "generic/object_bulk_create.html": ["generic/object_bulk_import.html"],
36
+ "generic/object_bulk_destroy.html": ["generic/object_bulk_delete.html"],
37
+ "generic/object_bulk_update.html": ["generic/object_bulk_edit.html"],
38
+ "generic/object_changelog.html": ["extras/object_changelog.html"],
39
+ "generic/object_create.html": ["dcim/powerpanel_edit.html", "generic/object_edit.html", "ipam/service_edit.html"],
40
+ "generic/object_destroy.html": ["generic/object_delete.html"],
41
+ "generic/object_notes.html": ["extras/object_notes.html"],
42
+ "generic/object_retrieve.html": [
43
+ "circuits/circuit.html",
44
+ "circuits/circuit_retrieve.html",
45
+ "circuits/circuittermination.html",
46
+ "circuits/circuittermination_retrieve.html",
47
+ "circuits/circuittype.html",
48
+ "circuits/circuittype_retrieve.html",
49
+ "circuits/providernetwork.html",
50
+ "circuits/providernetwork_retrieve.html",
51
+ "cloud/cloudaccount_retrieve.html",
52
+ "cloud/cloudnetwork_retrieve.html",
53
+ "cloud/cloudresourcetype_retrieve.html",
54
+ "cloud/cloudservice_retrieve.html",
55
+ "dcim/controller/base.html",
56
+ "dcim/controller_retrieve.html",
57
+ "dcim/controller_wirelessnetworks.html",
58
+ "dcim/controllermanageddevicegroup_retrieve.html",
59
+ "dcim/device/base.html",
60
+ "dcim/device/consoleports.html",
61
+ "dcim/device/consoleserverports.html",
62
+ "dcim/device/devicebays.html",
63
+ "dcim/device/frontports.html",
64
+ "dcim/device/interfaces.html",
65
+ "dcim/device/inventory.html",
66
+ "dcim/device/modulebays.html",
67
+ "dcim/device/poweroutlets.html",
68
+ "dcim/device/powerports.html",
69
+ "dcim/device/rearports.html",
70
+ "dcim/device/wireless.html",
71
+ "dcim/devicefamily_retrieve.html",
72
+ "dcim/deviceredundancygroup_retrieve.html",
73
+ "dcim/devicetype.html",
74
+ "dcim/devicetype_retrieve.html",
75
+ "dcim/interfaceredundancygroup_retrieve.html",
76
+ "dcim/locationtype.html",
77
+ "dcim/locationtype_retrieve.html",
78
+ "dcim/manufacturer.html",
79
+ "dcim/platform.html",
80
+ "dcim/powerfeed.html",
81
+ "dcim/powerfeed_retrieve.html",
82
+ "dcim/powerpanel.html",
83
+ "dcim/powerpanel_retrieve.html",
84
+ "dcim/rackgroup.html",
85
+ "dcim/softwareimagefile_retrieve.html",
86
+ "dcim/softwareversion_retrieve.html",
87
+ "dcim/virtualchassis.html",
88
+ "dcim/virtualchassis_retrieve.html",
89
+ "dcim/virtualdevicecontext_retrieve.html",
90
+ "extras/computedfield.html",
91
+ "extras/computedfield_retrieve.html",
92
+ "extras/configcontext.html",
93
+ "extras/configcontext_retrieve.html",
94
+ "extras/contact_retrieve.html",
95
+ "extras/customfield.html",
96
+ "extras/customfield_retrieve.html",
97
+ "extras/customlink.html",
98
+ "extras/exporttemplate.html",
99
+ "extras/job_detail.html",
100
+ "extras/jobbutton_retrieve.html",
101
+ "extras/jobhook.html",
102
+ "extras/jobqueue_retrieve.html",
103
+ "extras/metadatatype_retrieve.html",
104
+ "extras/secretsgroup.html",
105
+ "extras/secretsgroup_retrieve.html",
106
+ "extras/status.html",
107
+ "extras/tag.html",
108
+ "extras/tag_retrieve.html",
109
+ "extras/team_retrieve.html",
110
+ "generic/object_detail.html",
111
+ "ipam/rir.html",
112
+ "ipam/service.html",
113
+ "ipam/service_retrieve.html",
114
+ "ipam/vlan.html",
115
+ "ipam/vlan_retrieve.html",
116
+ "ipam/vlangroup.html",
117
+ "tenancy/tenant.html",
118
+ "virtualization/clustergroup.html",
119
+ "virtualization/clustertype.html",
120
+ "virtualization/virtualmachine.html",
121
+ "virtualization/virtualmachine_retrieve.html",
122
+ "wireless/radioprofile_retrieve.html",
123
+ "wireless/supporteddatarate_retrieve.html",
124
+ "wireless/wirelessnetwork_retrieve.html",
125
+ ],
126
+ "ipam/prefix_retrieve.html": ["ipam/prefix.html"],
127
+ "ipam/vlan_update.html": ["ipam/vlan_edit.html"],
128
+ "tenancy/tenant_create.html": ["tenancy/tenant_edit.html"],
129
+ "tenancy/tenantgroup_retrieve.html": ["tenancy/tenantgroup.html"],
130
+ "virtualchassis_update.html": ["dcim/virtualchassis_edit.html"],
131
+ "virtualization/virtualmachine_update.html": ["virtualization/virtualmachine_edit.html"],
132
+ }
133
+
134
+
135
+ def replace_template_references(content: str) -> tuple[str, bool]:
136
+ """
137
+ Replaces references to deprecated templates with new ones.
138
+
139
+ Args:
140
+ content: The content of the file to replace references in.
141
+
142
+ Returns:
143
+ A tuple containing the updated content and a boolean indicating if any changes were made.
144
+ """
145
+ for new_template, old_templates in TEMPLATE_REPLACEMENTS.items():
146
+ for old_template in old_templates:
147
+ pattern = rf"(\{{%\s*extends\s*['\"]){re.escape(old_template)}(['\"]\s*%\}})"
148
+ new_content, count = re.subn(pattern, rf"\1{new_template}\2", content)
149
+ if count > 0:
150
+ # A django template can only have one extends statement, so we can return as soon as we find a match.
151
+ return new_content, True
152
+
153
+ return content, False
154
+
155
+
156
+ def replace_deprecated_templates(path: str, dry_run: bool = False):
157
+ """
158
+ Recursively finds all .html files in the given directory,
159
+ and replaces references to deprecated templates with new ones.
160
+ """
161
+
162
+ if os.path.isfile(path):
163
+ only_filename = os.path.basename(path)
164
+ path = os.path.dirname(path)
165
+ else:
166
+ only_filename = None
167
+
168
+ for root, _, files in os.walk(path):
169
+ for filename in files:
170
+ if only_filename and only_filename != filename:
171
+ continue
172
+ if filename.endswith((".html")):
173
+ file_path = os.path.join(root, filename)
174
+ with open(file_path, "r", encoding="utf-8") as f:
175
+ original_content = f.read()
176
+
177
+ content = original_content
178
+
179
+ fixed_content, was_updated = replace_template_references(content)
180
+
181
+ if was_updated:
182
+ if dry_run:
183
+ print(f"Detected deprecated template reference in {file_path}")
184
+ continue
185
+ with open(file_path, "w", encoding="utf-8") as f:
186
+ f.write(fixed_content)
187
+ print(f"Updated: {file_path}")
188
+
189
+
190
+ def main():
191
+ parser = argparse.ArgumentParser(description="Replace deprecated templates with new ones")
192
+ parser.add_argument("path", type=str, help="Path to directory in which to recursively fix all .html files.")
193
+ parser.add_argument("--dry-run", action="store_true", help="Do not make any changes to the files.")
194
+ args = parser.parse_args()
195
+
196
+ replace_deprecated_templates(args.path, args.dry_run)
197
+
198
+
199
+ if __name__ == "__main__":
200
+ main()
@@ -210,3 +210,6 @@ GLOBAL_SEARCH_EXCLUDE_LIST = [
210
210
  "vrfprefixassignment",
211
211
  "webhook",
212
212
  ]
213
+
214
+ # M2M fields that are included by default in API responses if exclude_m2m is not provided
215
+ DEFAULT_M2M_FIELDS = ["tags", "content_types", "object_types"]
@@ -1,3 +1,5 @@
1
+ from urllib.parse import urlparse
2
+
1
3
  from django.conf import settings as django_settings
2
4
  from django.urls import NoReverseMatch, reverse
3
5
 
@@ -78,6 +80,12 @@ def nav_menu(request):
78
80
  pass
79
81
 
80
82
  nav_menu_object = {"tabs": {}}
83
+
84
+ if htmx_current_url := request.headers.get("HX-Current-URL"):
85
+ current_url = urlparse(htmx_current_url).path
86
+ else:
87
+ current_url = request.path
88
+
81
89
  for tab_name, tab_details in registry["nav_menu"]["tabs"].items():
82
90
  if not tab_details["permissions"] or has_one_or_more_perms(request.user, tab_details["permissions"]):
83
91
  nav_menu_object["tabs"][tab_name] = {"groups": {}, "icon": tab_details["icon"]}
@@ -93,7 +101,7 @@ def nav_menu(request):
93
101
  if has_identified_active_link:
94
102
  is_active = False
95
103
  else:
96
- is_active = item_link in [request.path, related_list_view_link]
104
+ is_active = item_link in [current_url, related_list_view_link]
97
105
  if is_active:
98
106
  has_identified_active_link = True
99
107
 
@@ -377,7 +377,7 @@ class DynamicFilterForm(BootstrapMixin, forms.Form):
377
377
 
378
378
  def _get_lookup_field_choices(self):
379
379
  """Get choices for lookup_fields i.e filterset parameters without a lookup expr"""
380
- from nautobot.extras.filters.mixins import RelationshipFilter # Avoid circular import
380
+ from nautobot.extras.filter_mixins import RelationshipFilter # Avoid circular import
381
381
 
382
382
  filterset_without_lookup = (
383
383
  (
@@ -22,7 +22,7 @@ from nautobot.core.celery import app, register_jobs
22
22
  from nautobot.core.exceptions import AbortTransaction
23
23
  from nautobot.core.jobs.bulk_actions import BulkDeleteObjects, BulkEditObjects
24
24
  from nautobot.core.jobs.cleanup import LogsCleanup
25
- from nautobot.core.jobs.groups import RefreshDynamicGroupCaches
25
+ from nautobot.core.jobs.groups import RefreshDynamicGroupCacheJobButtonReceiver, RefreshDynamicGroupCaches
26
26
  from nautobot.core.utils.lookup import get_filterset_for_model
27
27
  from nautobot.core.utils.requests import get_filterable_params_from_filter_params
28
28
  from nautobot.data_validation import models
@@ -440,8 +440,10 @@ def clean_compliance_rules_results_for_instance(instance, excluded_pks):
440
440
  class RunRegisteredDataComplianceRules(Job):
441
441
  """Run the validate function on all registered DataComplianceRule classes and, optionally, the built-in data validation rules."""
442
442
 
443
- name = "Run Registered Data Compliance Rules"
444
- description = "Runs selected Data Compliance rule classes."
443
+ class Meta:
444
+ name = "Run Registered Data Compliance Rules"
445
+ description = "Runs selected Data Compliance rule classes."
446
+ has_sensitive_variables = False
445
447
 
446
448
  selected_data_compliance_rules = MultiChoiceVar(
447
449
  choices=get_data_compliance_choices,
@@ -536,6 +538,7 @@ jobs = [
536
538
  ImportObjects,
537
539
  LogsCleanup,
538
540
  RefreshDynamicGroupCaches,
541
+ RefreshDynamicGroupCacheJobButtonReceiver,
539
542
  RunRegisteredDataComplianceRules,
540
543
  ]
541
544
  register_jobs(*jobs)
@@ -1,5 +1,5 @@
1
1
  from nautobot.extras.choices import DynamicGroupTypeChoices
2
- from nautobot.extras.jobs import Job, ObjectVar
2
+ from nautobot.extras.jobs import Job, JobButtonReceiver, ObjectVar
3
3
  from nautobot.extras.models import DynamicGroup
4
4
 
5
5
  name = "System Jobs"
@@ -31,8 +31,38 @@ class RefreshDynamicGroupCaches(Job):
31
31
  if single_group is not None:
32
32
  groups = groups.filter(pk=single_group.pk)
33
33
 
34
+ if not groups.exists():
35
+ self.logger.info("No relevant dynamic groups were specified, nothing to do.")
36
+ return
37
+
38
+ self.logger.info("Re-calculating and re-caching group members. This may take some time.")
34
39
  for group in groups:
35
40
  group.update_cached_members()
36
41
  self.logger.info("Cache refreshed successfully, now with %d members", group.count, extra={"object": group})
37
42
 
38
43
  self.logger.info("Cache(s) refreshed")
44
+
45
+
46
+ class RefreshDynamicGroupCacheJobButtonReceiver(JobButtonReceiver):
47
+ """
48
+ System Job Button Receiver to re-calculate and re-cache the members of a given Dynamic Group.
49
+ """
50
+
51
+ class Meta:
52
+ name = "Refresh Dynamic Group Cache (Job Button Receiver)"
53
+ description = "Re-calculate and re-cache the membership list of a given Dynamic Group."
54
+
55
+ def receive_job_button(self, obj):
56
+ if not isinstance(obj, DynamicGroup):
57
+ self.fail("This job button should only be used with Dynamic Group records.")
58
+ elif obj.group_type == DynamicGroupTypeChoices.TYPE_STATIC:
59
+ self.fail(
60
+ "The members of this Dynamic Group are statically defined and do not need to be recalculated.",
61
+ extra={"object": obj},
62
+ )
63
+ else:
64
+ self.logger.info(
65
+ "Re-calculating and re-caching group members. This may take some time.", extra={"object": obj}
66
+ )
67
+ obj.update_cached_members()
68
+ self.logger.success("Cache refreshed successfully, now with %d members", obj.count, extra={"object": obj})
@@ -133,6 +133,14 @@ class Command(BaseCommand):
133
133
  )
134
134
  from nautobot.tenancy.factory import TenantFactory, TenantGroupFactory
135
135
  from nautobot.users.factory import UserFactory
136
+ from nautobot.vpn.factory import (
137
+ VPNFactory,
138
+ VPNPhase1PolicyFactory,
139
+ VPNPhase2PolicyFactory,
140
+ VPNProfileFactory,
141
+ VPNTunnelEndpointFactory,
142
+ VPNTunnelFactory,
143
+ )
136
144
  from nautobot.wireless.factory import (
137
145
  ControllerManagedDeviceGroupWithMembersFactory,
138
146
  RadioProfileFactory,
@@ -171,14 +179,12 @@ class Command(BaseCommand):
171
179
  )
172
180
  # ...and some tags that apply to a random subset of content-types
173
181
  _create_batch(TagFactory, 15, description="on some content-types")
174
- _create_batch(UserFactory, 5)
182
+ _create_batch(UserFactory, 10)
175
183
  _create_batch(SavedViewFactory, 20)
176
184
  _create_batch(ContactFactory, 20)
177
185
  _create_batch(TeamFactory, 20)
178
- _create_batch(TenantGroupFactory, 10, description="without parents", has_parent=False)
179
- _create_batch(TenantGroupFactory, 10, description="with parents", has_parent=True)
180
- _create_batch(TenantFactory, 10, description="without a parent group", has_tenant_group=False)
181
- _create_batch(TenantFactory, 10, description="with a parent group", has_tenant_group=True)
186
+ _create_batch(TenantGroupFactory, 30)
187
+ _create_batch(TenantFactory, 30)
182
188
  _create_batch(LocationTypeFactory, 7) # only 7 unique LocationTypes are hard-coded presently
183
189
  # First 7 locations must be created in specific order so subsequent objects have valid parents to reference
184
190
  _create_batch(LocationFactory, 7, description="as structure", has_parent=True)
@@ -192,7 +198,7 @@ class Command(BaseCommand):
192
198
  _create_batch(VRFFactory, 20)
193
199
  _create_batch(VLANGroupFactory, 20)
194
200
  _create_batch(VLANFactory, 20)
195
- for i in range(30):
201
+ for i in range(50):
196
202
  _create_batch(
197
203
  PrefixFactory,
198
204
  1,
@@ -207,7 +213,15 @@ class Command(BaseCommand):
207
213
  prefix=f"2001:db8:0:{i}::/64",
208
214
  type=PrefixTypeChoices.TYPE_CONTAINER,
209
215
  )
210
- _create_batch(NamespaceFactory, 5, description="without any Prefixes or IPAddresses")
216
+ _create_batch(
217
+ NamespaceFactory, 5, description="with a Tenant and without any Prefixes or IPAddresses", has_tenant=True
218
+ )
219
+ _create_batch(
220
+ NamespaceFactory,
221
+ 5,
222
+ description="without a Tenant and without any Prefixes or IPAddresses",
223
+ has_tenant=False,
224
+ )
211
225
  _create_batch(DeviceFamilyFactory, 20)
212
226
  _create_batch(ManufacturerFactory, 8) # First 8 hard-coded Manufacturers
213
227
  _create_batch(PlatformFactory, 20, description="with Manufacturers", has_manufacturer=True)
@@ -222,7 +236,7 @@ class Command(BaseCommand):
222
236
  _create_batch(ConsoleServerPortTemplateFactory, 30)
223
237
  _create_batch(RearPortTemplateFactory, 30)
224
238
  _create_batch(FrontPortTemplateFactory, 30)
225
- _create_batch(InterfaceTemplateFactory, 30)
239
+ _create_batch(InterfaceTemplateFactory, 50)
226
240
  _create_batch(PowerPortTemplateFactory, 30)
227
241
  _create_batch(PowerOutletTemplateFactory, 30)
228
242
  _create_batch(ModuleBayTemplateFactory, 60, description="without module families", has_module_family=False)
@@ -324,11 +338,16 @@ class Command(BaseCommand):
324
338
  _create_batch(WirelessNetworksWithMembersFactory, 5, description="with members")
325
339
  # make sure we have some supported data rates that have null relationships to make filter tests happy
326
340
  _create_batch(SupportedDataRateFactory, 10, description="without any associated objects")
341
+ _create_batch(VPNPhase1PolicyFactory, 10)
342
+ _create_batch(VPNPhase2PolicyFactory, 10)
343
+ _create_batch(VPNProfileFactory, 20)
344
+ _create_batch(VPNFactory, 10)
345
+ _create_batch(VPNTunnelEndpointFactory, 20)
346
+ _create_batch(VPNTunnelFactory, 10)
327
347
  _create_batch(JobQueueFactory, 10)
328
348
  # make sure we have some tenants that have null relationships to make filter tests happy
329
349
  _create_batch(TenantFactory, 10, description="without any associated objects")
330
350
  # TODO: nautobot.tenancy.tests.test_filters currently calls the following additional factories:
331
- # _create_batch(UserFactory, 10)
332
351
  # _create_batch(RackFactory, 10)
333
352
  # _create_batch(RackReservationFactory, 10)
334
353
  # _create_batch(ClusterTypeFactory, 10)
@@ -4,13 +4,20 @@ from nautobot.core.models import BaseModel
4
4
  from nautobot.core.models.fields import TagsField
5
5
  from nautobot.extras.models.change_logging import ChangeLoggedModel
6
6
  from nautobot.extras.models.customfields import CustomFieldModel
7
- from nautobot.extras.models.mixins import ContactMixin, DynamicGroupsModelMixin, NotesMixin, SavedViewMixin
7
+ from nautobot.extras.models.mixins import (
8
+ ContactMixin,
9
+ DataComplianceModelMixin,
10
+ DynamicGroupsModelMixin,
11
+ NotesMixin,
12
+ SavedViewMixin,
13
+ )
8
14
  from nautobot.extras.models.relationships import RelationshipModel
9
15
 
10
16
  logger = logging.getLogger(__name__)
11
17
 
12
18
 
13
19
  class OrganizationalModel(
20
+ DataComplianceModelMixin,
14
21
  ChangeLoggedModel,
15
22
  ContactMixin,
16
23
  CustomFieldModel,
@@ -35,6 +42,7 @@ class OrganizationalModel(
35
42
 
36
43
 
37
44
  class PrimaryModel(
45
+ DataComplianceModelMixin,
38
46
  ChangeLoggedModel,
39
47
  ContactMixin,
40
48
  CustomFieldModel,
@@ -1,3 +1,5 @@
1
+ import uuid
2
+
1
3
  from django.core.cache import cache
2
4
  from django.db.models import Case, When
3
5
  from django.db.models.signals import post_delete, post_save
@@ -21,15 +23,18 @@ class TreeQuerySet(TreeQuerySet_, querysets.RestrictedQuerySet):
21
23
  Dynamically computes ancestors either through the tree or through the `parent` foreign key depending on whether
22
24
  tree fields are present on `of`.
23
25
  """
26
+
27
+ # If `of` is a UUID, i.e. pk, retrieve the corresponding model instance with tree fields disabled.
28
+ if isinstance(of, uuid.UUID):
29
+ of = self.model.objects.without_tree_fields().get(pk=of)
30
+
24
31
  # If `of` has `tree_depth` defined, i.e. if it was retrieved from the database on a queryset where tree fields
25
32
  # were enabled (see `TreeQuerySet.with_tree_fields` and `TreeQuerySet.without_tree_fields`), use the default
26
33
  # implementation from `tree_queries.query.TreeQuerySet`.
27
- # Furthermore, if `of` doesn't have a parent field we also have to defer to the tree-based implementation which
28
- # will then annotate the tree fields and proceed as usual.
29
- if hasattr(of, "tree_depth") or not hasattr(of, "parent"):
34
+ if hasattr(of, "tree_depth"):
30
35
  return super().ancestors(of, include_self=include_self)
36
+
31
37
  # In the other case, traverse the `parent` foreign key until the root.
32
- model_class = of._meta.concrete_model
33
38
  ancestor_pks = []
34
39
  if include_self:
35
40
  ancestor_pks.append(of.pk)
@@ -40,7 +45,7 @@ class TreeQuerySet(TreeQuerySet_, querysets.RestrictedQuerySet):
40
45
  # Reference:
41
46
  # https://stackoverflow.com/questions/4916851/django-get-a-queryset-from-array-of-ids-in-specific-order
42
47
  preserve_order = Case(*[When(pk=pk, then=position) for position, pk in enumerate(ancestor_pks)])
43
- return model_class.objects.without_tree_fields().filter(pk__in=ancestor_pks).order_by(preserve_order)
48
+ return self.model.objects.without_tree_fields().filter(pk__in=ancestor_pks).order_by(preserve_order)
44
49
 
45
50
  def max_tree_depth(self):
46
51
  r"""
nautobot/core/settings.py CHANGED
@@ -95,11 +95,12 @@ if "NAUTOBOT_DEPLOYMENT_ID" in os.environ and os.environ["NAUTOBOT_DEPLOYMENT_ID
95
95
  DEPLOYMENT_ID = os.environ["NAUTOBOT_DEPLOYMENT_ID"]
96
96
 
97
97
  # Device names are not guaranteed globally-unique by Nautobot but in practice they often are.
98
- # Set this to True to use the device name alone as the natural key for Device objects.
99
- # Set this to False to use the sequence (name, tenant, location) as the natural key instead.
100
- #
101
- if "NAUTOBOT_DEVICE_NAME_AS_NATURAL_KEY" in os.environ and os.environ["NAUTOBOT_DEVICE_NAME_AS_NATURAL_KEY"] != "":
102
- DEVICE_NAME_AS_NATURAL_KEY = is_truthy(os.environ["NAUTOBOT_DEVICE_NAME_AS_NATURAL_KEY"])
98
+ # Select how Devices are uniquely identified:
99
+ # - 'location_tenant_name': combination of Location + Tenant + Name
100
+ # - 'name': Device name must be globally unique
101
+ # - 'none': No enforced uniqueness (rely on other validation rules or custom validators)
102
+ if "NAUTOBOT_DEVICE_UNIQUENESS" in os.environ and os.environ["NAUTOBOT_DEVICE_UNIQUENESS"] != "":
103
+ DEVICE_UNIQUENESS = os.environ["NAUTOBOT_DEVICE_UNIQUENESS"]
103
104
 
104
105
  # Event Brokers
105
106
  EVENT_BROKERS = {}
@@ -416,6 +417,7 @@ SPECTACULAR_SETTINGS = {
416
417
  # result in this error:
417
418
  # enum naming encountered a non-optimally resolvable collision for fields named "mode".
418
419
  "InterfaceModeChoices": "nautobot.dcim.choices.InterfaceModeChoices",
420
+ "VPNPhase2PolicyChoices": "nautobot.vpn.choices.DhGroupChoices",
419
421
  "WirelessNetworkModeChoices": "nautobot.wireless.choices.WirelessNetworkModeChoices",
420
422
  },
421
423
  # Create separate schema components for PATCH requests (fields generally are not `required` on PATCH)
@@ -569,6 +571,7 @@ INSTALLED_APPS = [
569
571
  "nautobot.tenancy",
570
572
  "nautobot.users",
571
573
  "nautobot.virtualization",
574
+ "nautobot.vpn",
572
575
  "nautobot.wireless",
573
576
  "drf_spectacular",
574
577
  "drf_spectacular_sidecar",
@@ -759,12 +762,15 @@ CONSTANCE_CONFIG = {
759
762
  help_text="Number of days to retain object changelog history.\nSet this to 0 to retain changes indefinitely.",
760
763
  field_type=int,
761
764
  ),
762
- "DEVICE_NAME_AS_NATURAL_KEY": ConstanceConfigItem(
763
- default=False,
764
- help_text="Device names are not guaranteed globally-unique by Nautobot but in practice they often are. "
765
- "Set this to True to use the device name alone as the natural key for Device objects. "
766
- "Set this to False to use the sequence (name, tenant, location) as the natural key instead.",
767
- field_type=bool,
765
+ "DEVICE_UNIQUENESS": ConstanceConfigItem(
766
+ default="location_tenant_name",
767
+ help_text=(
768
+ "Select how Devices are uniquely identified:\n"
769
+ "- 'location_tenant_name': combination of Location + Tenant + Name\n"
770
+ "- 'name': Device name must be globally unique\n"
771
+ "- 'none': No enforced uniqueness (rely on other validation rules or custom validators)"
772
+ ),
773
+ field_type=str,
768
774
  ),
769
775
  "DEPLOYMENT_ID": ConstanceConfigItem(
770
776
  default="",
@@ -869,7 +875,7 @@ CONSTANCE_CONFIG_FIELDSETS = {
869
875
  "Change Logging": ["CHANGELOG_RETENTION"],
870
876
  "Device Connectivity": ["NETWORK_DRIVERS", "PREFER_IPV4"],
871
877
  "Installation Metrics": ["DEPLOYMENT_ID"],
872
- "Natural Keys": ["DEVICE_NAME_AS_NATURAL_KEY", "LOCATION_NAME_AS_NATURAL_KEY"],
878
+ "Natural Keys": ["DEVICE_UNIQUENESS", "LOCATION_NAME_AS_NATURAL_KEY"],
873
879
  "Pagination": ["PAGINATE_COUNT", "MAX_PAGE_SIZE", "PER_PAGE_DEFAULTS"],
874
880
  "Performance": ["JOB_CREATE_FILE_MAX_SIZE"],
875
881
  "Rack Elevation Rendering": [