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
@@ -3,6 +3,7 @@ Model test cases
3
3
  """
4
4
 
5
5
  import re
6
+ from unittest import TestCase
6
7
 
7
8
  from django.contrib.contenttypes.models import ContentType
8
9
  from django.core.validators import ValidationError
@@ -376,3 +377,17 @@ class UniqueValidationRuleModelTestCase(ValidationRuleModelTestCases.ValidationR
376
377
  )
377
378
 
378
379
  rule.clean()
380
+
381
+
382
+ class ValidationRuleModelMixinTestCase(TestCase):
383
+ """
384
+ Validate ValidationRuleModelMixin is working as intended.
385
+ """
386
+
387
+ def test_is_data_compliance_model(self):
388
+ """Validate is_data_compliance_model is set correctly on models using ValidationRuleModelMixin."""
389
+ # These models should have is_data_compliance_model = False
390
+ self.assertFalse(MinMaxValidationRule.is_data_compliance_model)
391
+ self.assertFalse(RegularExpressionValidationRule.is_data_compliance_model)
392
+ self.assertFalse(RequiredValidationRule.is_data_compliance_model)
393
+ self.assertFalse(UniqueValidationRule.is_data_compliance_model)
@@ -1,10 +1,13 @@
1
1
  """Unit tests for data_validation views."""
2
2
 
3
- from unittest.mock import MagicMock, patch
4
-
3
+ from constance import config
4
+ from django.contrib.auth import get_user_model
5
5
  from django.contrib.contenttypes.models import ContentType
6
- from django.http.request import QueryDict
6
+ from django.core.cache import caches
7
+ from django.test import override_settings
8
+ from django.urls import reverse
7
9
 
10
+ from nautobot.core import settings
8
11
  from nautobot.core.testing import TestCase, ViewTestCases
9
12
  from nautobot.data_validation.models import (
10
13
  DataCompliance,
@@ -13,13 +16,17 @@ from nautobot.data_validation.models import (
13
16
  RequiredValidationRule,
14
17
  UniqueValidationRule,
15
18
  )
16
- from nautobot.data_validation.tables import DataComplianceTableTab
17
19
  from nautobot.data_validation.tests import ValidationRuleTestCaseMixin
18
- from nautobot.data_validation.tests.test_data_compliance_rules import TestFailedDataComplianceRule
19
- from nautobot.data_validation.views import DataComplianceObjectView
20
+ from nautobot.data_validation.tests.test_data_compliance_rules import (
21
+ TestFailedDataComplianceRule,
22
+ TestFailedDataComplianceRuleAlt,
23
+ )
24
+ from nautobot.dcim.choices import DeviceUniquenessChoices
20
25
  from nautobot.dcim.models import Device, Location, LocationType, PowerFeed
21
26
  from nautobot.extras.models import Status
22
27
 
28
+ User = get_user_model()
29
+
23
30
 
24
31
  class RegularExpressionValidationRuleTestCase(ValidationRuleTestCaseMixin, ViewTestCases.PrimaryObjectViewTestCase):
25
32
  """View test cases for the RegularExpressionValidationRule model."""
@@ -247,31 +254,182 @@ class DataComplianceObjectTestCase(TestCase):
247
254
  """Test cases for DataComplianceObjectView."""
248
255
 
249
256
  def setUp(self):
250
- location_type = LocationType(name="Region")
251
- location_type.validated_save()
252
- s = Location(
253
- name="Test Location 1",
254
- location_type=LocationType.objects.get_by_natural_key("Region"),
255
- status=Status.objects.get_by_natural_key("Active"),
256
- )
257
- s.save()
258
- t = TestFailedDataComplianceRule(s)
257
+ self.device = Device.objects.first()
258
+
259
+ t = TestFailedDataComplianceRuleAlt(self.device)
259
260
  t.clean()
261
+ self.user = User.objects.create_user(username="testuser", is_superuser=True)
262
+
263
+ def test_data_compliance_action(self):
264
+ self.add_permissions("data_validation.view_datacompliance")
265
+ self.client.force_login(self.user)
266
+ url = reverse("dcim:device_data-compliance", kwargs={"pk": self.device.pk})
267
+ response = self.client.get(url)
268
+ self.assertEqual(response.status_code, 200)
269
+ self.assertIn("active_tab", response.context)
270
+ self.assertEqual(response.context["active_tab"], "data_compliance")
271
+ self.assertBodyContains(response, "The tenant is wrong")
272
+ self.assertBodyContains(response, "The name is wrong")
273
+ self.assertBodyContains(response, "The status is wrong")
274
+
275
+
276
+ class DeviceConstraintsViewTest(TestCase):
277
+ """Tests for the DeviceConstraintsView."""
260
278
 
261
- def test_get_extra_context(self):
262
- view = DataComplianceObjectView()
263
- location = Location.objects.first()
264
- mock_request = MagicMock()
265
- mock_request.GET = QueryDict("tab=data_validation:1")
266
- result = view.get_extra_context(mock_request, location)
267
- self.assertEqual(result["active_tab"], "data_validation:1")
268
- self.assertIsInstance(result["table"], DataComplianceTableTab)
269
-
270
- @patch("nautobot.core.views.generic.ObjectView.dispatch")
271
- def test_dispatch(self, mocked_dispatch):
272
- view = DataComplianceObjectView()
273
- mock_request = MagicMock()
274
- kwargs = {"model": "dcim.location", "other_arg": "other_arg", "another_arg": "another_arg"}
275
- view.dispatch(mock_request, **kwargs)
276
- mocked_dispatch.assert_called()
277
- mocked_dispatch.assert_called_with(mock_request, other_arg="other_arg", another_arg="another_arg")
279
+ def setUp(self):
280
+ self.initial_setting = config.DEVICE_UNIQUENESS
281
+ self.url = reverse("data_validation:device-constraints")
282
+ self.device_ct = ContentType.objects.get_for_model(Device)
283
+
284
+ def tearDown(self):
285
+ """Reset Constance config and clear cache."""
286
+ config.DEVICE_UNIQUENESS = self.initial_setting
287
+ cache = caches[settings.CONSTANCE_DATABASE_CACHE_BACKEND]
288
+ cache.clear()
289
+
290
+ @override_settings(
291
+ DEVICE_UNIQUENESS=DeviceUniquenessChoices.NONE,
292
+ )
293
+ def test_page_reflects_correct_settings_for_non_staff(self):
294
+ """Non-staff view still shows the actual configuration, but in disabled form."""
295
+ user = get_user_model().objects.create_user(username="testuser")
296
+ self.client.force_login(user)
297
+ response = self.client.get(self.url)
298
+
299
+ self.assertEqual(response.status_code, 200)
300
+
301
+ # Ensure fields are disabled but reflect correct config values
302
+ self.assertContains(response, f'value="{DeviceUniquenessChoices.NONE}" selected')
303
+ self.assertContains(response, "disabled")
304
+ self.assertContains(response, "You do not have permission to modify these settings.")
305
+
306
+ # DEVICE_NAME_REQUIRED should NOT be checked, because RequiredValidationRule not exist
307
+ device_ct = ContentType.objects.get_for_model(Device)
308
+ self.assertFalse(RequiredValidationRule.objects.filter(content_type=device_ct, field="name").exists())
309
+ self.assertNotContains(response, 'name="DEVICE_NAME_REQUIRED" checked')
310
+
311
+ # Footer buttons should NOT be rendered
312
+ self.assertNotContains(response, '<button type="submit"')
313
+ self.assertNotContains(response, "-->Update")
314
+ self.assertNotContains(response, "-->Cancel")
315
+
316
+ @override_settings(
317
+ DEVICE_UNIQUENESS=DeviceUniquenessChoices.NAME,
318
+ )
319
+ def test_page_reflects_correct_settings_for_staff(self):
320
+ """Page should reflect the true values of DEVICE_UNIQUENESS and DEVICE_NAME_REQUIRED."""
321
+ # Create RequiredValidationRule to check proper value of DEVICE_NAME_REQUIRED
322
+ device_ct = ContentType.objects.get_for_model(Device)
323
+ RequiredValidationRule.objects.create(
324
+ name="Required Name rule",
325
+ content_type=device_ct,
326
+ field="name",
327
+ )
328
+ user = get_user_model().objects.create_user(username="testuser", is_staff=True)
329
+ self.client.force_login(user)
330
+ response = self.client.get(self.url)
331
+
332
+ self.assertEqual(response.status_code, 200)
333
+ self.assertTemplateUsed(response, "data_validation/device_constraints.html")
334
+
335
+ self.assertIn("form", response.context)
336
+ self.assertContains(response, "Device Constraints")
337
+
338
+ # Check that the correct DEVICE_UNIQUENESS option is selected
339
+ self.assertContains(response, f'value="{DeviceUniquenessChoices.NAME}" selected')
340
+
341
+ self.assertNotContains(response, "disabled")
342
+ self.assertNotContains(response, "You do not have permission to modify these settings.")
343
+
344
+ # Check that DEVICE_NAME_REQUIRED checkbox is checked when RequiredValidationRule exist
345
+ self.assertTrue(RequiredValidationRule.objects.filter(content_type=device_ct, field="name").exists())
346
+ self.assertContains(response, 'id="id_DEVICE_NAME_REQUIRED" checked')
347
+
348
+ # Footer buttons should be rendered
349
+ self.assertContains(response, '<button type="submit"')
350
+ self.assertContains(response, "-->Update")
351
+ self.assertContains(response, "-->Cancel")
352
+
353
+ def test_post_as_non_admin_denied(self):
354
+ """POST by non-admin should be denied."""
355
+ user = get_user_model().objects.create_user(username="normaluser")
356
+ self.client.force_login(user)
357
+
358
+ response = self.client.post(
359
+ self.url,
360
+ data={
361
+ "DEVICE_UNIQUENESS": DeviceUniquenessChoices.LOCATION_TENANT_NAME,
362
+ "DEVICE_NAME_REQUIRED": True,
363
+ },
364
+ follow=True,
365
+ )
366
+ self.assertEqual(response.status_code, 403)
367
+
368
+ # No rule should be created
369
+ self.assertFalse(RequiredValidationRule.objects.filter(content_type=self.device_ct, field="name").exists())
370
+ self.assertEqual(config.DEVICE_UNIQUENESS, self.initial_setting)
371
+
372
+ def test_post_updates_device_uniqueness_and_creates_required_rule(self):
373
+ """POST with DEVICE_NAME_REQUIRED=True should create a RequiredValidationRule."""
374
+ user = get_user_model().objects.create_user(username="testuser", is_staff=True)
375
+ self.client.force_login(user)
376
+ response = self.client.post(
377
+ self.url,
378
+ {
379
+ "DEVICE_UNIQUENESS": DeviceUniquenessChoices.NAME,
380
+ "DEVICE_NAME_REQUIRED": True,
381
+ },
382
+ follow=True,
383
+ )
384
+
385
+ self.assertRedirects(response, self.url)
386
+ self.assertEqual(config.DEVICE_UNIQUENESS, "name")
387
+
388
+ rule_exists = RequiredValidationRule.objects.filter(
389
+ content_type=self.device_ct,
390
+ field="name",
391
+ ).exists()
392
+ self.assertTrue(rule_exists)
393
+
394
+ def test_post_disables_required_rule(self):
395
+ """POST with DEVICE_NAME_REQUIRED=False should delete the RequiredValidationRule."""
396
+ user = get_user_model().objects.create_user(username="testuser", is_staff=True)
397
+ self.client.force_login(user)
398
+ RequiredValidationRule.objects.create(
399
+ name="Required Name rule",
400
+ content_type=self.device_ct,
401
+ field="name",
402
+ )
403
+ self.assertTrue(RequiredValidationRule.objects.filter(content_type=self.device_ct, field="name").exists())
404
+
405
+ response = self.client.post(
406
+ self.url,
407
+ {
408
+ "DEVICE_UNIQUENESS": DeviceUniquenessChoices.LOCATION_TENANT_NAME,
409
+ "DEVICE_NAME_REQUIRED": False,
410
+ },
411
+ follow=True,
412
+ )
413
+ self.assertRedirects(response, self.url)
414
+ self.assertEqual(config.DEVICE_UNIQUENESS, DeviceUniquenessChoices.LOCATION_TENANT_NAME)
415
+
416
+ self.assertFalse(RequiredValidationRule.objects.filter(content_type=self.device_ct, field="name").exists())
417
+
418
+ def test_invalid_post_rerenders_form(self):
419
+ """If form is invalid, the view should re-render without redirect for multiple invalid inputs."""
420
+ user = get_user_model().objects.create_user(username="testuser", is_staff=True)
421
+ self.client.force_login(user)
422
+
423
+ invalid_inputs = [
424
+ {"DEVICE_UNIQUENESS": ""},
425
+ {"DEVICE_UNIQUENESS": "invalid_value"},
426
+ ]
427
+
428
+ for post_data in invalid_inputs:
429
+ with self.subTest(post_data=post_data):
430
+ response = self.client.post(self.url, post_data)
431
+ self.assertEqual(response.status_code, 200)
432
+ self.assertTemplateUsed(response, "data_validation/device_constraints.html")
433
+ self.assertIn("form", response.context)
434
+ self.assertTrue(response.context["form"].errors)
435
+ self.assertEqual(config.DEVICE_UNIQUENESS, self.initial_setting)
@@ -15,10 +15,7 @@ router.register("required-rules", views.RequiredValidationRuleUIViewSet)
15
15
  router.register("unique-rules", views.UniqueValidationRuleUIViewSet)
16
16
 
17
17
  urlpatterns = [
18
- path(
19
- "data-compliance/<model>/<uuid:id>/",
20
- views.DataComplianceObjectView.as_view(),
21
- name="data-compliance-tab",
22
- ),
18
+ path("device-constraints/", views.DeviceConstraintsView.as_view(), name="device-constraints"),
23
19
  ]
20
+
24
21
  urlpatterns += router.urls
@@ -1,15 +1,17 @@
1
1
  """Views for data_validation."""
2
2
 
3
- from django.apps import apps as global_apps
3
+ from constance import config
4
+ from django.contrib import messages
4
5
  from django.contrib.contenttypes.models import ContentType
5
- from django_tables2 import RequestConfig
6
+ from django.shortcuts import redirect, render
6
7
 
8
+ from nautobot.apps.ui import Breadcrumbs, Titles, ViewNameBreadcrumbItem
7
9
  from nautobot.core.ui.choices import SectionChoices
8
10
  from nautobot.core.ui.object_detail import (
9
11
  ObjectDetailContent,
10
12
  ObjectFieldsPanel,
11
13
  )
12
- from nautobot.core.views.generic import ObjectView
14
+ from nautobot.core.views.generic import GenericView
13
15
  from nautobot.core.views.mixins import (
14
16
  ObjectBulkDestroyViewMixin,
15
17
  ObjectChangeLogViewMixin,
@@ -18,18 +20,10 @@ from nautobot.core.views.mixins import (
18
20
  ObjectListViewMixin,
19
21
  ObjectNotesViewMixin,
20
22
  )
21
- from nautobot.core.views.paginator import EnhancedPaginator, get_paginate_count
22
23
  from nautobot.core.views.viewsets import NautobotUIViewSet
23
- from nautobot.data_validation import filters, forms, tables
24
+ from nautobot.data_validation import filters, forms, models, tables
24
25
  from nautobot.data_validation.api import serializers
25
- from nautobot.data_validation.models import (
26
- DataCompliance,
27
- MinMaxValidationRule,
28
- RegularExpressionValidationRule,
29
- RequiredValidationRule,
30
- UniqueValidationRule,
31
- )
32
- from nautobot.extras.utils import get_base_template
26
+ from nautobot.dcim.models import Device
33
27
 
34
28
  #
35
29
  # RegularExpressionValidationRules
@@ -43,7 +37,7 @@ class RegularExpressionValidationRuleUIViewSet(NautobotUIViewSet):
43
37
  filterset_class = filters.RegularExpressionValidationRuleFilterSet
44
38
  filterset_form_class = forms.RegularExpressionValidationRuleFilterForm
45
39
  form_class = forms.RegularExpressionValidationRuleForm
46
- queryset = RegularExpressionValidationRule.objects.all()
40
+ queryset = models.RegularExpressionValidationRule.objects.all()
47
41
  serializer_class = serializers.RegularExpressionValidationRuleSerializer
48
42
  table_class = tables.RegularExpressionValidationRuleTable
49
43
  object_detail_content = ObjectDetailContent(
@@ -69,7 +63,7 @@ class MinMaxValidationRuleUIViewSet(NautobotUIViewSet):
69
63
  filterset_class = filters.MinMaxValidationRuleFilterSet
70
64
  filterset_form_class = forms.MinMaxValidationRuleFilterForm
71
65
  form_class = forms.MinMaxValidationRuleForm
72
- queryset = MinMaxValidationRule.objects.all()
66
+ queryset = models.MinMaxValidationRule.objects.all()
73
67
  serializer_class = serializers.MinMaxValidationRuleSerializer
74
68
  table_class = tables.MinMaxValidationRuleTable
75
69
  object_detail_content = ObjectDetailContent(
@@ -95,7 +89,7 @@ class RequiredValidationRuleUIViewSet(NautobotUIViewSet):
95
89
  filterset_class = filters.RequiredValidationRuleFilterSet
96
90
  filterset_form_class = forms.RequiredValidationRuleFilterForm
97
91
  form_class = forms.RequiredValidationRuleForm
98
- queryset = RequiredValidationRule.objects.all()
92
+ queryset = models.RequiredValidationRule.objects.all()
99
93
  serializer_class = serializers.RequiredValidationRuleSerializer
100
94
  table_class = tables.RequiredValidationRuleTable
101
95
  object_detail_content = ObjectDetailContent(
@@ -121,7 +115,7 @@ class UniqueValidationRuleUIViewSet(NautobotUIViewSet):
121
115
  filterset_class = filters.UniqueValidationRuleFilterSet
122
116
  filterset_form_class = forms.UniqueValidationRuleFilterForm
123
117
  form_class = forms.UniqueValidationRuleForm
124
- queryset = UniqueValidationRule.objects.all()
118
+ queryset = models.UniqueValidationRule.objects.all()
125
119
  serializer_class = serializers.UniqueValidationRuleSerializer
126
120
  table_class = tables.UniqueValidationRuleTable
127
121
  object_detail_content = ObjectDetailContent(
@@ -151,7 +145,7 @@ class DataComplianceUIViewSet( # pylint: disable=W0223
151
145
  """Views for the DataComplianceUIViewSet model."""
152
146
 
153
147
  lookup_field = "pk"
154
- queryset = DataCompliance.objects.all()
148
+ queryset = models.DataCompliance.objects.all()
155
149
  table_class = tables.DataComplianceTable
156
150
  filterset_class = filters.DataComplianceFilterSet
157
151
  filterset_form_class = forms.DataComplianceFilterForm
@@ -162,33 +156,72 @@ class DataComplianceUIViewSet( # pylint: disable=W0223
162
156
  ObjectFieldsPanel(
163
157
  section=SectionChoices.LEFT_HALF,
164
158
  weight=100,
165
- fields="__all__",
159
+ fields=[
160
+ "content_type",
161
+ "compliance_class_name",
162
+ "last_validation_date",
163
+ "validated_object",
164
+ "validated_attribute",
165
+ "validated_attribute_value",
166
+ "valid",
167
+ "message",
168
+ ],
166
169
  ),
167
170
  )
168
171
  )
169
172
 
170
173
 
171
- class DataComplianceObjectView(ObjectView):
172
- """View for the Audit Results tab dynamically generated on specific object detail views."""
173
-
174
- template_name = "data_validation/datacompliance_tab.html"
175
- queryset = None
176
-
177
- def dispatch(self, request, *args, **kwargs):
178
- """Set the queryset for the given object and call the inherited dispatch method."""
179
- model = kwargs.pop("model")
180
- if not self.queryset:
181
- self.queryset = global_apps.get_model(model).objects.all()
182
- return super().dispatch(request, *args, **kwargs)
174
+ class DeviceConstraintsView(GenericView):
175
+ template_name = "data_validation/device_constraints.html"
176
+ view_titles = Titles(titles={"*": "Device Constraints"})
177
+ breadcrumbs = Breadcrumbs(
178
+ items={
179
+ "*": [
180
+ ViewNameBreadcrumbItem(view_name="data_validation:device-constraints", label="Device Constraints"),
181
+ ],
182
+ },
183
+ )
183
184
 
184
- def get_extra_context(self, request, instance):
185
- """Generate extra context for rendering the DataComplianceObjectView template."""
186
- compliance_objects = DataCompliance.objects.filter(
187
- content_type=ContentType.objects.get_for_model(instance), object_id=instance.id
185
+ def get(self, request):
186
+ form = forms.DeviceConstraintsForm(user=request.user)
187
+ return render(
188
+ request,
189
+ self.template_name,
190
+ {
191
+ "form": form,
192
+ "view_titles": self.get_view_titles(),
193
+ "breadcrumbs": self.get_breadcrumbs(),
194
+ },
188
195
  )
189
- compliance_table = tables.DataComplianceTableTab(compliance_objects)
190
- base_template = get_base_template(None, instance)
191
196
 
192
- paginate = {"paginator_class": EnhancedPaginator, "per_page": get_paginate_count(request)}
193
- RequestConfig(request, paginate).configure(compliance_table)
194
- return {"active_tab": request.GET["tab"], "table": compliance_table, "base_template": base_template}
197
+ def post(self, request):
198
+ if not request.user.is_staff:
199
+ return self.handle_no_permission()
200
+ form = forms.DeviceConstraintsForm(request.POST)
201
+ if form.is_valid():
202
+ config.DEVICE_UNIQUENESS = form.cleaned_data["DEVICE_UNIQUENESS"]
203
+ device_ct = ContentType.objects.get_for_model(Device)
204
+ if form.cleaned_data["DEVICE_NAME_REQUIRED"]:
205
+ models.RequiredValidationRule.objects.get_or_create(
206
+ content_type=device_ct,
207
+ field="name",
208
+ defaults={"name": "Require Device Name"},
209
+ )
210
+ else:
211
+ models.RequiredValidationRule.objects.filter(
212
+ content_type=device_ct,
213
+ field="name",
214
+ ).delete()
215
+
216
+ messages.success(request, "Device constraints have been updated successfully.")
217
+ return redirect("data_validation:device-constraints")
218
+
219
+ return render(
220
+ request,
221
+ self.template_name,
222
+ {
223
+ "form": form,
224
+ "view_titles": self.get_view_titles(),
225
+ "breadcrumbs": self.get_breadcrumbs(),
226
+ },
227
+ )
@@ -23,7 +23,6 @@ from nautobot.core.api.utils import (
23
23
  )
24
24
  from nautobot.core.models.utils import get_all_concrete_models
25
25
  from nautobot.core.utils.config import get_settings_or_config
26
- from nautobot.core.utils.deprecation import class_deprecated_in_favor_of
27
26
  from nautobot.dcim.choices import (
28
27
  CableLengthUnitChoices,
29
28
  CableTypeChoices,
@@ -137,12 +136,6 @@ class CableTerminationModelSerializerMixin(serializers.ModelSerializer):
137
136
  return None
138
137
 
139
138
 
140
- # TODO: remove in 2.2
141
- @class_deprecated_in_favor_of(CableTerminationModelSerializerMixin)
142
- class CableTerminationSerializer(CableTerminationModelSerializerMixin):
143
- pass
144
-
145
-
146
139
  class PathEndpointModelSerializerMixin(ValidatedModelSerializer):
147
140
  connected_endpoint_type = serializers.SerializerMethodField(read_only=True)
148
141
  connected_endpoint = serializers.SerializerMethodField(read_only=True)
@@ -183,12 +176,6 @@ class PathEndpointModelSerializerMixin(ValidatedModelSerializer):
183
176
  return None
184
177
 
185
178
 
186
- # TODO: remove in 2.2
187
- @class_deprecated_in_favor_of(PathEndpointModelSerializerMixin)
188
- class ConnectedEndpointSerializer(PathEndpointModelSerializerMixin):
189
- pass
190
-
191
-
192
179
  class ModularDeviceComponentTemplateSerializerMixin:
193
180
  def validate(self, data):
194
181
  """Validate device_type and module_type field constraints for modular device component templates."""
nautobot/dcim/apps.py CHANGED
@@ -1,4 +1,5 @@
1
1
  from nautobot.core.apps import NautobotConfig
2
+ from nautobot.extras.plugins import register_custom_validators
2
3
 
3
4
 
4
5
  class DCIMConfig(NautobotConfig):
@@ -26,4 +27,7 @@ class DCIMConfig(NautobotConfig):
26
27
 
27
28
  def ready(self):
28
29
  super().ready()
30
+ from nautobot.dcim.custom_validators import custom_validators
31
+
32
+ register_custom_validators(custom_validators)
29
33
  import nautobot.dcim.signals # noqa: F401 # unused-import -- but this import installs the signals
nautobot/dcim/choices.py CHANGED
@@ -159,6 +159,20 @@ class DeviceStatusChoices(ChoiceSet):
159
159
  )
160
160
 
161
161
 
162
+ class DeviceUniquenessChoices(ChoiceSet):
163
+ LOCATION_TENANT_NAME = "location_tenant_name"
164
+ NAME = "name"
165
+ NONE = "none"
166
+
167
+ DEFAULT = LOCATION_TENANT_NAME
168
+
169
+ CHOICES = [
170
+ (LOCATION_TENANT_NAME, "Location + Tenant + Name"),
171
+ (NAME, "Device name must be globally unique"),
172
+ (NONE, "No enforced uniqueness"),
173
+ ]
174
+
175
+
162
176
  #
163
177
  # ConsolePorts
164
178
  #
@@ -764,6 +778,7 @@ class InterfaceTypeChoices(ChoiceSet):
764
778
  TYPE_VIRTUAL = "virtual"
765
779
  TYPE_BRIDGE = "bridge"
766
780
  TYPE_LAG = "lag"
781
+ TYPE_TUNNEL = "tunnel"
767
782
 
768
783
  # Ethernet
769
784
  TYPE_100ME_FX = "100base-fx"
@@ -932,6 +947,7 @@ class InterfaceTypeChoices(ChoiceSet):
932
947
  (TYPE_VIRTUAL, "Virtual"),
933
948
  (TYPE_BRIDGE, "Bridge"),
934
949
  (TYPE_LAG, "Link Aggregation Group (LAG)"),
950
+ (TYPE_TUNNEL, "Tunnel"),
935
951
  ),
936
952
  ),
937
953
  (
@@ -0,0 +1,84 @@
1
+ from django.contrib.contenttypes.models import ContentType
2
+
3
+ from nautobot.apps.models import CustomValidator
4
+ from nautobot.core.utils.config import get_settings_or_config
5
+ from nautobot.data_validation.models import RequiredValidationRule
6
+ from nautobot.dcim.choices import DeviceUniquenessChoices
7
+ from nautobot.dcim.models import Device
8
+
9
+
10
+ class DeviceUniquenessValidator(CustomValidator):
11
+ """Custom validator enforcing device uniqueness based on DEVICE_UNIQUENESS setting."""
12
+
13
+ model = "dcim.device"
14
+
15
+ def clean(self):
16
+ obj = self.context["object"]
17
+ try:
18
+ uniqueness_mode = get_settings_or_config("DEVICE_UNIQUENESS", fallback=DeviceUniquenessChoices.DEFAULT)
19
+ except AttributeError:
20
+ uniqueness_mode = DeviceUniquenessChoices.DEFAULT
21
+ device_name_required = RequiredValidationRule.objects.filter(
22
+ content_type=ContentType.objects.get_for_model(Device), field="name"
23
+ ).exists()
24
+
25
+ # Rule 1: If we don't set DEVICE_NAME_REQUIRED then it's acceptable for any number of devices to be "unnamed",
26
+ # regardless of the DEVICE_UNIQUENESS setting. Note that we consider both `None` and `""` to be "unnamed".
27
+ if not obj.name and not device_name_required:
28
+ return
29
+
30
+ # If not obj.name and device_name_required, this will be detected by RequiredValidationRule.
31
+
32
+ if uniqueness_mode == DeviceUniquenessChoices.LOCATION_TENANT_NAME:
33
+ # Rule 2: name is not None, tenant is None, given location --> no duplicates
34
+ # Rule 3: tenant is None, name is duplicated --> error
35
+ if obj.tenant is None:
36
+ duplicates = Device.objects.filter(
37
+ name=obj.name,
38
+ tenant__isnull=True,
39
+ location=obj.location,
40
+ ).exclude(pk=obj.pk)
41
+ if duplicates.exists():
42
+ self.validation_error(
43
+ {
44
+ "__all__": (
45
+ f"A device named '{obj.name}' with no tenant already exists in this location: {obj.location}. "
46
+ "Device names must be unique when tenant is None and DEVICE_UNIQUENESS='location_tenant_name'."
47
+ )
48
+ }
49
+ )
50
+ else:
51
+ # When tenant is set, enforce uniqueness per (location, tenant, name)
52
+ duplicates = Device.objects.filter(
53
+ name=obj.name,
54
+ tenant=obj.tenant,
55
+ location=obj.location,
56
+ ).exclude(pk=obj.pk)
57
+ if duplicates.exists():
58
+ self.validation_error(
59
+ {
60
+ "__all__": (
61
+ f"A device named '{obj.name}' already exists in this location: {obj.location} and tenant: {obj.tenant}. "
62
+ "Device names must be unique per (Location, Tenant) when DEVICE_UNIQUENESS='location_tenant_name'."
63
+ )
64
+ }
65
+ )
66
+
67
+ elif uniqueness_mode == DeviceUniquenessChoices.NAME:
68
+ duplicates = Device.objects.filter(name=obj.name).exclude(pk=obj.pk)
69
+ if duplicates.exists():
70
+ self.validation_error(
71
+ {
72
+ "name": (
73
+ f"At least one other device named '{obj.name}' already exists. "
74
+ "Device names must be globally unique when DEVICE_UNIQUENESS='name'."
75
+ )
76
+ }
77
+ )
78
+
79
+ elif uniqueness_mode == "none":
80
+ # Explicitly no uniqueness enforcement
81
+ return
82
+
83
+
84
+ custom_validators = [DeviceUniquenessValidator]