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
@@ -28,8 +28,8 @@ class ApprovalWorkflowDefinitionManager(BaseManager.from_queryset(RestrictedQuer
28
28
  """
29
29
  content_type = ContentType.objects.get_for_model(model_instance)
30
30
 
31
- # Get all workflow definitions for this content type, ordered by priority (lowest first = highest priority)
32
- workflow_definitions = self.get_queryset().filter(model_content_type=content_type).order_by("priority")
31
+ # Get all workflow definitions for this content type, ordered by weight (highest wins)
32
+ workflow_definitions = self.get_queryset().filter(model_content_type=content_type).order_by("-weight")
33
33
 
34
34
  for workflow_definition in workflow_definitions:
35
35
  if not workflow_definition.model_constraints:
@@ -77,11 +77,12 @@ class ApprovalWorkflowDefinition(PrimaryModel):
77
77
  default=dict,
78
78
  help_text="Constraints to filter the objects that can be approved using this workflow.",
79
79
  )
80
- priority = models.IntegerField(
81
- default=0, help_text="Determines workflow relevance when multiple apply. Lower values indicate higher priority."
80
+ weight = models.IntegerField(
81
+ default=0, help_text="Determines workflow relevance when multiple apply. Higher weight wins."
82
82
  )
83
83
  documentation_static_path = "docs/user-guide/platform-functionality/approval-workflow.html"
84
84
  is_dynamic_group_associable = False
85
+ is_data_compliance_model = False
85
86
  objects = ApprovalWorkflowDefinitionManager()
86
87
  is_version_controlled = False
87
88
 
@@ -90,7 +91,7 @@ class ApprovalWorkflowDefinition(PrimaryModel):
90
91
 
91
92
  verbose_name = "Approval Workflow Definition"
92
93
  ordering = ["name"]
93
- unique_together = [["model_content_type", "priority"]]
94
+ unique_together = [["model_content_type", "weight"]]
94
95
 
95
96
  def __str__(self):
96
97
  """Stringify instance."""
@@ -114,12 +115,13 @@ class ApprovalWorkflowStageDefinition(OrganizationalModel):
114
115
  on_delete=models.CASCADE,
115
116
  help_text="Approval workflow definition to which this stage belongs.",
116
117
  )
117
- weight = models.PositiveIntegerField(
118
- help_text="The weight dictates the order in which this stage will need to be approved. The lower the number, the earlier it will be.",
118
+ sequence = models.PositiveIntegerField(
119
+ help_text="The sequence dictates the order in which this stage will need to be approved. The lower the number, the earlier it will be.",
119
120
  )
120
121
  name = models.CharField(max_length=CHARFIELD_MAX_LENGTH)
121
122
  min_approvers = models.PositiveIntegerField(
122
- help_text="Number of minimum approvers required to approve this stage.",
123
+ verbose_name="Minimum approvers",
124
+ help_text="Minimum number of approvers required to approve this stage.",
123
125
  )
124
126
  denial_message = models.CharField(
125
127
  max_length=CHARFIELD_MAX_LENGTH, blank=True, help_text="Message to show when the stage is denied."
@@ -128,18 +130,20 @@ class ApprovalWorkflowStageDefinition(OrganizationalModel):
128
130
  to=Group,
129
131
  related_name="approval_workflow_stage_definitions",
130
132
  verbose_name="Group",
131
- help_text="Group of users who are eligible to approve this stage.",
133
+ help_text="Group of users who are eligible to approve this stage. Only admin users can create new groups.",
132
134
  on_delete=models.PROTECT,
133
135
  )
134
136
  documentation_static_path = "docs/user-guide/platform-functionality/approval-workflow.html"
135
137
  is_version_controlled = False
136
138
 
139
+ is_data_compliance_model = False
140
+
137
141
  class Meta:
138
142
  """Meta class for ApprovalWorkflowStage."""
139
143
 
140
144
  verbose_name = "Approval Workflow Stage Definition"
141
- unique_together = [["approval_workflow_definition", "name"], ["approval_workflow_definition", "weight"]]
142
- ordering = ["approval_workflow_definition", "weight"]
145
+ unique_together = [["approval_workflow_definition", "name"], ["approval_workflow_definition", "sequence"]]
146
+ ordering = ["approval_workflow_definition", "sequence"]
143
147
 
144
148
  def __str__(self):
145
149
  """Stringify instance."""
@@ -195,6 +199,7 @@ class ApprovalWorkflow(OrganizationalModel):
195
199
  user_name = models.CharField(max_length=150, editable=False, db_index=True)
196
200
  documentation_static_path = "docs/user-guide/platform-functionality/approval-workflow.html"
197
201
 
202
+ is_data_compliance_model = False
198
203
  is_version_controlled = False
199
204
 
200
205
  class Meta:
@@ -225,7 +230,7 @@ class ApprovalWorkflow(OrganizationalModel):
225
230
  """
226
231
  first_nonapproved_stage = (
227
232
  self.approval_workflow_stages.exclude(state=ApprovalWorkflowStateChoices.APPROVED)
228
- .order_by("approval_workflow_stage_definition__weight")
233
+ .order_by("approval_workflow_stage_definition__sequence")
229
234
  .first()
230
235
  )
231
236
  return first_nonapproved_stage
@@ -317,12 +322,14 @@ class ApprovalWorkflowStage(OrganizationalModel):
317
322
  documentation_static_path = "docs/user-guide/platform-functionality/approval-workflow.html"
318
323
  is_version_controlled = False
319
324
 
325
+ is_data_compliance_model = False
326
+
320
327
  class Meta:
321
328
  """Meta class for ApprovalWorkflowStage."""
322
329
 
323
330
  verbose_name = "Approval Workflow Stage"
324
331
  unique_together = [["approval_workflow", "approval_workflow_stage_definition"]]
325
- ordering = ["approval_workflow", "approval_workflow_stage_definition__weight"]
332
+ ordering = ["approval_workflow", "approval_workflow_stage_definition__sequence"]
326
333
 
327
334
  def __str__(self):
328
335
  """Stringify instance."""
@@ -506,6 +513,8 @@ class ApprovalWorkflowStageResponse(BaseModel):
506
513
  documentation_static_path = "docs/user-guide/platform-functionality/approval-workflow.html"
507
514
  is_version_controlled = False
508
515
 
516
+ is_data_compliance_model = False
517
+
509
518
  class Meta:
510
519
  """Meta class for ApprovalWorkflowStageResponse."""
511
520
 
@@ -20,6 +20,7 @@ class ContactTeamSharedBase(PrimaryModel):
20
20
 
21
21
  comments = models.TextField(blank=True)
22
22
  is_contact_associable_model = False
23
+ is_data_compliance_model = False
23
24
 
24
25
  class Meta:
25
26
  abstract = True
@@ -94,6 +95,7 @@ class ContactAssociation(OrganizationalModel):
94
95
  is_contact_associable_model = False
95
96
  is_dynamic_group_associable_model = False
96
97
  is_saved_view_model = False
98
+ is_data_compliance_model = False
97
99
 
98
100
  class Meta:
99
101
  unique_together = (
@@ -76,6 +76,7 @@ class DynamicGroup(PrimaryModel):
76
76
 
77
77
  objects = BaseManager.from_queryset(DynamicGroupQuerySet)()
78
78
  is_dynamic_group_associable_model = False
79
+ is_data_compliance_model = False
79
80
 
80
81
  clone_fields = ["content_type", "group_type", "filter", "tenant"]
81
82
 
@@ -628,6 +629,10 @@ class DynamicGroup(PrimaryModel):
628
629
  else:
629
630
  # Validate against the filterset's internal form validation.
630
631
  filterset = self.filterset_class(self.filter) # pylint: disable=not-callable
632
+ # TODO: the below is more generous than one might expect. For example, passing a list of strings ["foo"]
633
+ # to a (single-input) CharFilter will quietly normalize the list to a string '["foo"]' instead of reporting
634
+ # any failure of is_valid(). We've had cases of such "should be invalid but isn't caught" DynamicGroups causing
635
+ # exceptions when trying to evaluate their membership; it would be good to be stricter here instead!
631
636
  if not filterset.is_valid():
632
637
  raise ValidationError(filterset.errors)
633
638
 
@@ -661,6 +666,22 @@ class DynamicGroup(PrimaryModel):
661
666
 
662
667
  # TODO limit most changes to self.group_type as well.
663
668
 
669
+ def save(self, *args, update_cached_members=True, **kwargs):
670
+ """
671
+ Save the DynamicGroup record.
672
+
673
+ Args:
674
+ update_cached_members (bool): If True, (re)calculate the cached members set of the related group(s) immediately.
675
+ Note that this is potentially quite expensive if there will be a large change in the members set!
676
+ If False (recommended), you can call `self.update_cached_members()` explicitly when ready.
677
+ """
678
+ super().save(*args, **kwargs)
679
+
680
+ if update_cached_members:
681
+ self.update_cached_members()
682
+ for ancestor in self.get_ancestors():
683
+ ancestor.update_cached_members()
684
+
664
685
  def _generate_query_for_filter(self, filter_field, value):
665
686
  """
666
687
  Return a `Q` object generated from a `filter_field` and `value`.
@@ -703,9 +724,12 @@ class DynamicGroup(PrimaryModel):
703
724
  # "ams02"]}`, the value being a list of location names (`["ams01", "ams02"]`).
704
725
  if value and isinstance(value, list) and isinstance(value[0], str) and not is_uuid(value[0]):
705
726
  model_field = django_filters.utils.get_model_field(self._model, filter_field.field_name)
706
- related_model = model_field.related_model
707
- lookup_kwargs = {f"{to_field_name}__in": value}
708
- gq_value = related_model.objects.filter(**lookup_kwargs)
727
+ if model_field is None:
728
+ gq_value = value
729
+ else:
730
+ related_model = model_field.related_model
731
+ lookup_kwargs = {f"{to_field_name}__in": value}
732
+ gq_value = related_model.objects.filter(**lookup_kwargs)
709
733
  else:
710
734
  gq_value = value
711
735
  query |= filter_field.generate_query(gq_value)
@@ -1172,12 +1196,26 @@ class DynamicGroupMembership(BaseModel):
1172
1196
  if self.group in self.parent_group.get_ancestors():
1173
1197
  raise ValidationError({"group": "Cannot add ancestor as a child"})
1174
1198
 
1175
- def save(self, *args, **kwargs):
1199
+ def save(self, *args, update_cached_members=True, **kwargs):
1200
+ """
1201
+ Save the DynamicGroupMembership record.
1202
+
1203
+ Args:
1204
+ update_cached_members (bool): If True, (re)calculate the cached members set of the related group(s) immediately.
1205
+ Note that this is potentially quite expensive if there will be a large change in the members set!
1206
+ If False (recommended), you can call `self.parent_group.update_cached_members()` explicitly when ready.
1207
+ """
1176
1208
  # For backwards compatibility
1177
1209
  if self.parent_group.group_type == DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER and not self.parent_group.filter:
1178
1210
  self.parent_group.group_type = DynamicGroupTypeChoices.TYPE_DYNAMIC_SET
1179
1211
  self.parent_group.save()
1180
- return super().save(*args, **kwargs)
1212
+
1213
+ super().save(*args, **kwargs)
1214
+
1215
+ if update_cached_members:
1216
+ self.parent_group.update_cached_members()
1217
+ for ancestor in self.parent_group.get_ancestors():
1218
+ ancestor.update_cached_members()
1181
1219
 
1182
1220
 
1183
1221
  class StaticGroupAssociationManager(BaseManager.from_queryset(RestrictedQuerySet)):
@@ -1218,6 +1256,7 @@ class StaticGroupAssociation(OrganizationalModel):
1218
1256
  is_contact_associable_model = False
1219
1257
  is_dynamic_group_associable_model = False
1220
1258
  is_saved_view_model = False
1259
+ is_data_compliance_model = False
1221
1260
 
1222
1261
  class Meta:
1223
1262
  unique_together = [["dynamic_group", "associated_object_type", "associated_object_id"]]
@@ -15,7 +15,7 @@ from django.contrib.contenttypes.models import ContentType
15
15
  from django.core.exceptions import ValidationError
16
16
  from django.core.validators import MinValueValidator
17
17
  from django.db import models, transaction
18
- from django.db.models import ProtectedError, signals
18
+ from django.db.models import Count, ProtectedError, Q, signals
19
19
  from django.utils import timezone
20
20
  from django.utils.functional import cached_property
21
21
  from django_celery_beat.clockedschedule import clocked
@@ -252,6 +252,7 @@ class Job(PrimaryModel):
252
252
  help_text="If set, the configured value will remain even if the underlying Job source code changes",
253
253
  )
254
254
  objects = BaseManager.from_queryset(JobQuerySet)()
255
+ is_data_compliance_model = False
255
256
 
256
257
  documentation_static_path = "docs/user-guide/platform-functionality/jobs/models.html"
257
258
 
@@ -439,6 +440,8 @@ class JobHook(OrganizationalModel):
439
440
  type_delete = models.BooleanField(default=False, help_text="Call this job hook when a matching object is deleted.")
440
441
  type_update = models.BooleanField(default=False, help_text="Call this job hook when a matching object is updated.")
441
442
 
443
+ is_data_compliance_model = False
444
+
442
445
  documentation_static_path = "docs/user-guide/platform-functionality/jobs/jobhook.html"
443
446
 
444
447
  class Meta:
@@ -529,6 +532,7 @@ class JobLogEntry(BaseModel):
529
532
  absolute_url = models.CharField(max_length=JOB_LOG_MAX_ABSOLUTE_URL_LENGTH, blank=True, default="")
530
533
 
531
534
  is_metadata_associable_model = False
535
+ is_data_compliance_model = False
532
536
 
533
537
  documentation_static_path = "docs/user-guide/platform-functionality/jobs/models.html"
534
538
  hide_in_diff_view = True
@@ -580,6 +584,7 @@ class JobQueue(PrimaryModel):
580
584
  )
581
585
 
582
586
  documentation_static_path = "docs/user-guide/platform-functionality/jobs/jobqueue.html"
587
+ is_data_compliance_model = False
583
588
 
584
589
  class Meta:
585
590
  ordering = ["name"]
@@ -610,6 +615,7 @@ class JobQueueAssignment(BaseModel):
610
615
  job = models.ForeignKey(Job, on_delete=models.CASCADE, related_name="job_queue_assignments")
611
616
  job_queue = models.ForeignKey(JobQueue, on_delete=models.CASCADE, related_name="job_assignments")
612
617
  is_metadata_associable_model = False
618
+ is_data_compliance_model = False
613
619
 
614
620
  class Meta:
615
621
  unique_together = ["job", "job_queue"]
@@ -678,11 +684,17 @@ class JobResult(SavedViewMixin, BaseModel, CustomFieldModel):
678
684
  traceback = models.TextField(blank=True, null=True) # noqa: DJ001 # django-nullable-model-string-field -- TODO: can we remove null=True?
679
685
  meta = models.JSONField(null=True, default=None, editable=False)
680
686
  scheduled_job = models.ForeignKey(to="extras.ScheduledJob", on_delete=models.SET_NULL, null=True, blank=True)
687
+ debug_log_count = models.PositiveIntegerField(blank=True, null=True, editable=False)
688
+ success_log_count = models.PositiveIntegerField(blank=True, null=True, editable=False)
689
+ info_log_count = models.PositiveIntegerField(blank=True, null=True, editable=False)
690
+ warning_log_count = models.PositiveIntegerField(blank=True, null=True, editable=False)
691
+ error_log_count = models.PositiveIntegerField(blank=True, null=True, editable=False)
681
692
 
682
693
  objects = JobResultManager()
683
694
 
684
695
  documentation_static_path = "docs/user-guide/platform-functionality/jobs/models.html"
685
696
  hide_in_diff_view = True
697
+ is_data_compliance_model = False
686
698
 
687
699
  def __init__(self, *args, **kwargs):
688
700
  super().__init__(*args, **kwargs)
@@ -762,6 +774,30 @@ class JobResult(SavedViewMixin, BaseModel, CustomFieldModel):
762
774
 
763
775
  set_status.alters_data = True
764
776
 
777
+ def count_logs_by_level(self):
778
+ """Helper method to count JobLogEntries after a Job is run, or update these values when missing or changed."""
779
+ db_log_counts = self.job_log_entries.aggregate(
780
+ debug_log_count=Count("pk", filter=Q(log_level=LogLevelChoices.LOG_DEBUG)),
781
+ success_log_count=Count("pk", filter=Q(log_level=LogLevelChoices.LOG_SUCCESS)),
782
+ info_log_count=Count("pk", filter=Q(log_level=LogLevelChoices.LOG_INFO)),
783
+ warning_log_count=Count("pk", filter=Q(log_level=LogLevelChoices.LOG_WARNING)),
784
+ error_log_count=Count(
785
+ "pk",
786
+ filter=Q(
787
+ log_level__in=[
788
+ LogLevelChoices.LOG_FAILURE,
789
+ LogLevelChoices.LOG_ERROR,
790
+ LogLevelChoices.LOG_CRITICAL,
791
+ ]
792
+ ),
793
+ ),
794
+ )
795
+ self.debug_log_count = db_log_counts["debug_log_count"]
796
+ self.success_log_count = db_log_counts["success_log_count"]
797
+ self.info_log_count = db_log_counts["info_log_count"]
798
+ self.warning_log_count = db_log_counts["warning_log_count"]
799
+ self.error_log_count = db_log_counts["error_log_count"]
800
+
765
801
  @classmethod
766
802
  def execute_job(cls, *args, **kwargs):
767
803
  """
@@ -1006,6 +1042,18 @@ class JobResult(SavedViewMixin, BaseModel, CustomFieldModel):
1006
1042
 
1007
1043
  log.alters_data = True
1008
1044
 
1045
+ def save(self, *args, **kwargs):
1046
+ """When a JobResult is saved and in a terminal state, store missing log counts for summary."""
1047
+ if self.status in JobResultStatusChoices.READY_STATES and None in [
1048
+ self.debug_log_count,
1049
+ self.info_log_count,
1050
+ self.success_log_count,
1051
+ self.warning_log_count,
1052
+ self.error_log_count,
1053
+ ]:
1054
+ self.count_logs_by_level()
1055
+ super().save(*args, **kwargs)
1056
+
1009
1057
 
1010
1058
  #
1011
1059
  # Job Button
@@ -1055,10 +1103,18 @@ class JobButton(ContactMixin, ChangeLoggedModel, DynamicGroupsModelMixin, NotesM
1055
1103
  )
1056
1104
 
1057
1105
  documentation_static_path = "docs/user-guide/platform-functionality/jobs/jobbutton.html"
1106
+ is_data_compliance_model = False
1058
1107
 
1059
1108
  class Meta:
1060
1109
  ordering = ["group_name", "weight", "name"]
1061
1110
 
1111
+ @property
1112
+ def button_class_css_class(self):
1113
+ """Map self.button_class database value to the correct CSS class for buttons."""
1114
+ if self.button_class == ButtonClassChoices.CLASS_DEFAULT:
1115
+ return "secondary"
1116
+ return self.button_class
1117
+
1062
1118
  def __str__(self):
1063
1119
  return self.name
1064
1120
 
@@ -1081,6 +1137,7 @@ class ScheduledJobs(models.Model):
1081
1137
  last_update = models.DateTimeField(null=False)
1082
1138
 
1083
1139
  objects = ScheduledJobsManager()
1140
+ is_data_compliance_model = False
1084
1141
 
1085
1142
  def __str__(self):
1086
1143
  return str(self.ident)
@@ -1218,6 +1275,7 @@ class ScheduledJob(ApprovableModelMixin, BaseModel):
1218
1275
  no_changes = False
1219
1276
 
1220
1277
  documentation_static_path = "docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html"
1278
+ is_data_compliance_model = False
1221
1279
 
1222
1280
  def __str__(self):
1223
1281
  return f"{self.name}: {self.interval}"
@@ -251,3 +251,31 @@ class SavedViewMixin(models.Model):
251
251
  abstract = True
252
252
 
253
253
  is_saved_view_model = True
254
+
255
+
256
+ class DataComplianceModelMixin:
257
+ """
258
+ Adds a `get_data_compliance_url` that can be applied to instances.
259
+ """
260
+
261
+ is_data_compliance_model = True
262
+
263
+ def get_data_compliance_url(self, api=False):
264
+ """Return the data compliance URL for a given instance."""
265
+ # If is_data_compliance_model overridden should allow to opt out
266
+ if not self.is_data_compliance_model:
267
+ return None
268
+ route = get_route_for_model(self, "data-compliance", api=api)
269
+
270
+ # Iterate the pk-like fields and try to get a URL, or return None.
271
+ fields = ["pk", "slug"]
272
+ for field in fields:
273
+ if not hasattr(self, field):
274
+ continue
275
+
276
+ try:
277
+ return reverse(route, kwargs={field: getattr(self, field)})
278
+ except NoReverseMatch:
279
+ continue
280
+
281
+ return None
@@ -353,6 +353,14 @@ class CustomLink(
353
353
  )
354
354
  new_window = models.BooleanField(help_text="Force link to open in a new window")
355
355
 
356
+ is_data_compliance_model = False
357
+
358
+ @property
359
+ def button_class_css_class(self):
360
+ if self.button_class == ButtonClassChoices.CLASS_DEFAULT:
361
+ return "secondary"
362
+ return self.button_class
363
+
356
364
  class Meta:
357
365
  ordering = ["group_name", "weight", "name"]
358
366
 
@@ -577,6 +585,7 @@ class FileAttachment(BaseModel):
577
585
  mimetype = models.CharField(max_length=CHARFIELD_MAX_LENGTH)
578
586
 
579
587
  is_metadata_associable_model = False
588
+ is_data_compliance_model = False
580
589
 
581
590
  natural_key_field_names = ["pk"]
582
591
 
@@ -638,6 +647,8 @@ class FileProxy(BaseModel):
638
647
  uploaded_at = models.DateTimeField(auto_now_add=True)
639
648
  job_result = models.ForeignKey(to=JobResult, null=True, blank=True, on_delete=models.CASCADE, related_name="files")
640
649
 
650
+ is_data_compliance_model = False
651
+
641
652
  def __str__(self):
642
653
  return self.name
643
654
 
@@ -877,6 +888,7 @@ class SavedView(BaseModel, ChangeLoggedModel):
877
888
  is_shared = models.BooleanField(default=True)
878
889
 
879
890
  documentation_static_path = "docs/user-guide/platform-functionality/savedview.html"
891
+ is_data_compliance_model = False
880
892
 
881
893
  class Meta:
882
894
  ordering = ["owner", "view", "name"]
@@ -905,6 +917,7 @@ class UserSavedViewAssociation(BaseModel):
905
917
  user = models.ForeignKey("users.User", on_delete=models.CASCADE, related_name="saved_view_assignments")
906
918
  view_name = models.CharField(max_length=CHARFIELD_MAX_LENGTH)
907
919
  is_metadata_associable_model = False
920
+ is_data_compliance_model = False
908
921
 
909
922
  class Meta:
910
923
  unique_together = [["user", "view_name"]]
@@ -141,6 +141,7 @@ class SecretsGroupAssociation(BaseModel):
141
141
  natural_key_field_names = ["secrets_group", "access_type", "secret_type", "secret"]
142
142
 
143
143
  documentation_static_path = "docs/user-guide/platform-functionality/secret.html"
144
+ is_data_compliance_model = False
144
145
 
145
146
  class Meta:
146
147
  unique_together = (
@@ -7,7 +7,6 @@ from django.utils.hashable import make_hashable
7
7
 
8
8
  from nautobot.core.models.fields import ForeignKeyLimitedByContentTypes
9
9
  from nautobot.core.models.name_color_content_types import NameColorContentTypesModel
10
- from nautobot.core.utils.deprecation import class_deprecated
11
10
  from nautobot.extras.utils import extras_features, FeatureQuery
12
11
 
13
12
 
@@ -98,17 +97,3 @@ class StatusField(ForeignKeyLimitedByContentTypes):
98
97
  f"get_{self.name}_color",
99
98
  partialmethod(_get_FIELD_color, field=self),
100
99
  )
101
-
102
-
103
- @class_deprecated(message="please directly declare `status = StatusField(...)` on your model instead")
104
- class StatusModel(models.Model):
105
- """
106
- Deprecated abstract base class for any model which may have statuses.
107
-
108
- Just directly include a StatusField instead for any new models.
109
- """
110
-
111
- status = StatusField(null=True) # for backward compatibility
112
-
113
- class Meta:
114
- abstract = True
@@ -4,11 +4,13 @@ from nautobot.core.apps import (
4
4
  NavMenuItem,
5
5
  NavMenuTab,
6
6
  )
7
+ from nautobot.core.ui.choices import NavigationIconChoices, NavigationWeightChoices
7
8
 
8
9
  menu_items = (
9
10
  NavMenuTab(
10
11
  name="Approvals",
11
- icon="checkbox-circle",
12
+ icon=NavigationIconChoices.APPROVAL_WORKFLOWS,
13
+ weight=NavigationWeightChoices.APPROVAL_WORKFLOWS,
12
14
  groups=(
13
15
  NavMenuGroup(
14
16
  name="Approval Workflows",
@@ -38,7 +40,8 @@ menu_items = (
38
40
  ),
39
41
  NavMenuTab(
40
42
  name="Organization",
41
- weight=100,
43
+ icon=NavigationIconChoices.ORGANIZATION,
44
+ weight=NavigationWeightChoices.ORGANIZATION,
42
45
  groups=(
43
46
  NavMenuGroup(
44
47
  name="Contacts",
@@ -140,8 +143,8 @@ menu_items = (
140
143
  ),
141
144
  NavMenuTab(
142
145
  name="Secrets",
143
- icon="secrets",
144
- weight=700,
146
+ icon=NavigationIconChoices.SECRETS,
147
+ weight=NavigationWeightChoices.SECRETS,
145
148
  groups=(
146
149
  NavMenuGroup(
147
150
  name="Secrets",
@@ -169,8 +172,8 @@ menu_items = (
169
172
  ),
170
173
  NavMenuTab(
171
174
  name="Jobs",
172
- icon="share",
173
- weight=800,
175
+ icon=NavigationIconChoices.JOBS,
176
+ weight=NavigationWeightChoices.JOBS,
174
177
  groups=(
175
178
  NavMenuGroup(
176
179
  name="Jobs",
@@ -258,7 +261,8 @@ menu_items = (
258
261
  ),
259
262
  NavMenuTab(
260
263
  name="Extensibility",
261
- weight=900,
264
+ icon=NavigationIconChoices.EXTENSIBILITY,
265
+ weight=NavigationWeightChoices.EXTENSIBILITY,
262
266
  groups=(
263
267
  NavMenuGroup(
264
268
  name="Logging",
@@ -532,8 +536,8 @@ menu_items = (
532
536
  ),
533
537
  NavMenuTab(
534
538
  name="Apps",
535
- icon="elements",
536
- weight=5000,
539
+ icon=NavigationIconChoices.APPS,
540
+ weight=NavigationWeightChoices.APPS,
537
541
  groups=(
538
542
  NavMenuGroup(
539
543
  name="General",