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
@@ -9,7 +9,7 @@ from django.contrib.contenttypes.models import ContentType
9
9
  from django.core.exceptions import ValidationError
10
10
  from django.db.models import Q
11
11
  from django.http import QueryDict
12
- from django.test import override_settings
12
+ from django.test import override_settings, tag
13
13
 
14
14
  from nautobot.circuits import models as circuits_models
15
15
  from nautobot.core import exceptions, forms, settings_funcs
@@ -20,7 +20,7 @@ from nautobot.core.testing import TestCase
20
20
  from nautobot.core.utils import data as data_utils, filtering, lookup, querysets, requests
21
21
  from nautobot.core.utils.cache import construct_cache_key
22
22
  from nautobot.core.utils.migrations import update_object_change_ct_for_replaced_models
23
- from nautobot.core.utils.module_loading import check_name_safe_to_import_privately
23
+ from nautobot.core.utils.module_loading import check_name_safe_to_import_privately, import_string_optional
24
24
  from nautobot.data_validation import models as data_validation_models
25
25
  from nautobot.dcim import filters as dcim_filters, forms as dcim_forms, models as dcim_models, tables
26
26
  from nautobot.extras import models as extras_models, utils as extras_utils
@@ -30,8 +30,6 @@ from nautobot.extras.forms import StatusForm
30
30
  from nautobot.extras.models import ObjectChange
31
31
  from nautobot.ipam import models as ipam_models
32
32
 
33
- from example_app.models import ExampleModel
34
-
35
33
 
36
34
  class ConstructCacheKeyTest(TestCase):
37
35
  """
@@ -372,10 +370,13 @@ class GetFooForModelTest(TestCase):
372
370
  # both primary_ip4 and primary_ip6 are candidates
373
371
  lookup.get_related_field_for_models(dcim_models.Device, ipam_models.IPAddress)
374
372
 
373
+ @tag("example_app")
375
374
  def test_get_route_for_model(self):
376
375
  """
377
376
  Test that `get_route_for_model` returns the appropriate URL route name for various inputs.
378
377
  """
378
+ from example_app.models import ExampleModel
379
+
379
380
  # UI
380
381
  self.assertEqual(lookup.get_route_for_model("dcim.device", "list"), "dcim:device_list")
381
382
  self.assertEqual(lookup.get_route_for_model(dcim_models.Device, "list"), "dcim:device_list")
@@ -423,10 +424,13 @@ class GetFooForModelTest(TestCase):
423
424
  self.assertEqual(lookup.get_model_from_name("dcim.device"), dcim_models.Device)
424
425
  self.assertEqual(lookup.get_model_from_name("dcim.location"), dcim_models.Location)
425
426
 
427
+ @tag("example_app")
426
428
  def test_get_model_for_view_name(self):
427
429
  """
428
430
  Test that `get_model_for_view_name` returns the appropriate Model, if the colon separated view name provided.
429
431
  """
432
+ from example_app.models import ExampleModel
433
+
430
434
  with self.subTest("Test core UI view."):
431
435
  self.assertEqual(lookup.get_model_for_view_name("dcim:device_list"), dcim_models.Device)
432
436
  self.assertEqual(lookup.get_model_for_view_name("dcim:device"), dcim_models.Device)
@@ -1128,6 +1132,29 @@ class TestModuleLoadingUtils(TestCase):
1128
1132
  self.assertFalse(permitted)
1129
1133
  self.assertIsInstance(reason, str)
1130
1134
 
1135
+ def test_import_string_optional(self):
1136
+ with self.subTest("Nonexistent module should return None"):
1137
+ self.assertIsNone(import_string_optional("no_such_module.no_such_attribute"))
1138
+ self.assertIsNone(import_string_optional("no_such_module.no_such_submodule.no_such_attribute"))
1139
+ self.assertIsNone(import_string_optional("nautobot.no_such_submodule.no_such_attribute"))
1140
+ self.assertIsNone(import_string_optional("nautobot.core.no_such_submodule.no_such_attribute"))
1141
+
1142
+ with self.subTest("Existing module but nonexistent attribute should return None"):
1143
+ self.assertIsNone(import_string_optional("nautobot.core.no_such_attribute"))
1144
+ self.assertIsNone(import_string_optional("nautobot.core.no_such_attribute"))
1145
+ self.assertIsNone(import_string_optional("sys.no_such_attribute"))
1146
+
1147
+ with self.subTest("Other import errors should propagate upward still"):
1148
+ with self.assertRaises(ImportError):
1149
+ import_string_optional("nautobot.extras.test_jobs.invalid_import.MyJob")
1150
+ with self.assertRaises(ImportError):
1151
+ import_string_optional("nautobot.extras.test_jobs.missing_import.MyJob")
1152
+
1153
+ with self.subTest("Successful imports should succeed"):
1154
+ self.assertEqual(
1155
+ import_string_optional("nautobot.core.tests.test_utils.TestModuleLoadingUtils"), self.__class__
1156
+ )
1157
+
1131
1158
 
1132
1159
  class TestQuerySetUtils(TestCase):
1133
1160
  def test_maybe_select_related(self):
@@ -1,12 +1,12 @@
1
1
  import json
2
2
  import os
3
+ from pathlib import Path
3
4
  import re
4
5
  import tempfile
5
- from unittest import mock, skipIf
6
+ from unittest import mock
6
7
  import urllib.parse
7
8
 
8
9
  from django.apps import apps
9
- from django.conf import settings
10
10
  from django.contrib.contenttypes.models import ContentType
11
11
  from django.core.cache import cache
12
12
  from django.core.files.uploadedfile import SimpleUploadedFile
@@ -110,38 +110,25 @@ class HomeViewTestCase(TestCase):
110
110
 
111
111
  # Search bar in header
112
112
  header_search_bar_pattern = re.compile(
113
- '<header.*<form action="/search/" class="col text-center" method="get" id="navbar_search" role="search">.*</form>.*</header>'
113
+ '<header.*<form action="/search/" class="col-4 text-center" id="header_search" method="get" role="search">.*</form>.*</header>'
114
114
  )
115
115
  header_search_bar_result = header_search_bar_pattern.search(
116
116
  response.content.decode(response.charset).replace("\n", "")
117
117
  )
118
118
 
119
- # Global search bar in body/container-fluid wrapper
120
- body_search_bar_pattern = re.compile(
121
- '<div class="container-fluid wrapper" id="main-content">.*<form action="/search/" method="get" class="form-inline">.*</form>.*</div>',
122
- re.DOTALL,
123
- )
124
-
125
- body_search_bar_result = body_search_bar_pattern.search(
126
- response.content.decode(response.charset).replace("\n", "")
127
- )
128
-
129
- return header_search_bar_result, body_search_bar_result
119
+ return header_search_bar_result
130
120
 
131
121
  def test_search_bar_not_visible_if_user_not_authenticated(self):
132
122
  self.client.logout()
133
123
 
134
- header_search_bar_result, body_search_bar_result = self.make_request()
124
+ header_search_bar_result = self.make_request()
135
125
 
136
126
  self.assertIsNone(header_search_bar_result)
137
- self.assertIsNone(body_search_bar_result)
138
127
 
139
- @tag("fix_in_v3")
140
128
  def test_search_bar_visible_if_user_authenticated(self):
141
- header_search_bar_result, body_search_bar_result = self.make_request()
129
+ header_search_bar_result = self.make_request()
142
130
 
143
131
  self.assertIsNotNone(header_search_bar_result)
144
- self.assertIsNotNone(body_search_bar_result)
145
132
 
146
133
  @override_settings(VERSION="1.2.3")
147
134
  def test_footer_version_visible_authenticated_users_only(self):
@@ -190,6 +177,132 @@ class HomeViewTestCase(TestCase):
190
177
  self.assertNotIn("Welcome to Nautobot!", response.content.decode(response.charset))
191
178
 
192
179
 
180
+ class AppDocsViewTestCase(TestCase):
181
+ def setUp(self):
182
+ super().setUp()
183
+ self.test_app_label = "test_app"
184
+ self.test_base_url = "test-app"
185
+
186
+ # Create temp docs dir
187
+ # I use tearDown to clean up, so this is save
188
+ self.temp_dir = tempfile.TemporaryDirectory() # pylint: disable=consider-using-with
189
+ self.docs_path = Path(self.temp_dir.name) / "docs"
190
+ self.docs_path.mkdir(parents=True)
191
+ (self.docs_path / "index.html").write_text("<html>Test Index</html>")
192
+ (self.docs_path / "css/style.css").parent.mkdir(parents=True, exist_ok=True)
193
+ (self.docs_path / "css/style.css").write_text("body { background: #fff; }")
194
+
195
+ def tearDown(self):
196
+ self.temp_dir.cleanup()
197
+ super().tearDown()
198
+
199
+ def test_docs_index_redirect(self):
200
+ """Ensure /docs/<base_url>/ redirects to /docs/<base_url>/index.html."""
201
+ url = reverse("docs_index_redirect", kwargs={"app_base_url": self.test_base_url})
202
+ response = self.client.get(url, follow=False)
203
+ self.assertEqual(response.status_code, 302)
204
+ self.assertEqual(response["Location"], f"/docs/{self.test_base_url}/index.html")
205
+
206
+ def test_docs_index_redirect_if_not_logged_in(self):
207
+ self.client.logout()
208
+ url = reverse("docs_index_redirect", kwargs={"app_base_url": self.test_base_url})
209
+ response = self.client.get(url, follow=False)
210
+
211
+ # First, the redirect to /docs/<base_url>/index.html
212
+ self.assertEqual(response.status_code, 302)
213
+ redirect_url = f"/docs/{self.test_base_url}/index.html"
214
+ self.assertEqual(response["Location"], redirect_url)
215
+
216
+ # Follow the redirect to AppDocsView, which should require login
217
+ response = self.client.get(redirect_url)
218
+ self.assertRedirects(
219
+ response,
220
+ expected_url=f"{reverse('login')}?next={redirect_url}",
221
+ status_code=302,
222
+ target_status_code=200,
223
+ )
224
+
225
+ def test_docs_file_redirect_if_not_logged_in(self):
226
+ self.client.logout()
227
+ url = reverse("docs_file", kwargs={"app_base_url": self.test_base_url, "path": "css/style.css"})
228
+ response = self.client.get(url)
229
+ # LoginRequiredMixin redirects to /accounts/login/
230
+ self.assertRedirects(
231
+ response,
232
+ expected_url=f"{reverse('login')}?next={url}",
233
+ status_code=302,
234
+ target_status_code=200,
235
+ )
236
+
237
+ @mock.patch.dict("nautobot.core.views.BASE_URL_TO_APP_LABEL", {"test-app": "test_app"})
238
+ @mock.patch("nautobot.core.views.resources.files")
239
+ def test_access_denied_path_traversal_attempts(self, mock_resources_files):
240
+ """Ensure ../ or similar traversal patterns are rejected."""
241
+ mock_resources_files.return_value = Path(self.temp_dir.name)
242
+
243
+ malicious_paths = [
244
+ "../settings.py",
245
+ "../../etc/passwd",
246
+ "docs/../../secret.txt",
247
+ ]
248
+
249
+ for path in malicious_paths:
250
+ with self.subTest(path=path):
251
+ url = reverse("docs_file", kwargs={"app_base_url": self.test_base_url, "path": path})
252
+ response = self.client.get(url)
253
+ self.assertEqual(response.status_code, 403)
254
+
255
+ @mock.patch.dict("nautobot.core.views.BASE_URL_TO_APP_LABEL", {"test-app": "test_app"})
256
+ @mock.patch("nautobot.core.views.resources.files")
257
+ def test_serve_index_html_logged_in(self, mock_resources_files):
258
+ mock_resources_files.return_value = Path(self.temp_dir.name)
259
+ url = reverse("docs_index_redirect", kwargs={"app_base_url": self.test_base_url, "path": "index.html"})
260
+ response = self.client.get(url, follow=True)
261
+ self.assertEqual(response.status_code, 200)
262
+ self.assertContains(response, "Test Index")
263
+ self.assertEqual(response["Content-Type"], "text/html")
264
+
265
+ @mock.patch.dict("nautobot.core.views.BASE_URL_TO_APP_LABEL", {"test-app": "test_app"})
266
+ @mock.patch("nautobot.core.views.resources.files")
267
+ def test_serve_css_logged_in(self, mock_resources_files):
268
+ mock_resources_files.return_value = Path(self.temp_dir.name)
269
+ url = reverse("docs_file", kwargs={"app_base_url": self.test_base_url, "path": "css/style.css"})
270
+ response = self.client.get(url)
271
+ self.assertEqual(response.status_code, 200)
272
+ self.assertContains(response, "background: #fff;")
273
+ self.assertEqual(response["Content-Type"], "text/css")
274
+
275
+ @mock.patch.dict("nautobot.core.views.BASE_URL_TO_APP_LABEL", {"test-app": "test_app"})
276
+ @mock.patch("nautobot.core.views.resources.files")
277
+ def test_docs_index_nonexistent_app(self, mock_resources_files):
278
+ mock_resources_files.return_value = Path(self.temp_dir.name)
279
+ url = reverse("docs_index_redirect", kwargs={"app_base_url": "nonexistent-app"})
280
+ response = self.client.get(url, follow=True)
281
+ self.assertEqual(response.status_code, 404)
282
+ self.assertJSONEqual(response.content, {"detail": "Unknown base_url 'nonexistent-app'."})
283
+
284
+ @mock.patch.dict("nautobot.core.views.BASE_URL_TO_APP_LABEL", {"test-app": "test_app"})
285
+ @mock.patch("nautobot.core.views.resources.files")
286
+ def test_docs_file_nonexistent_app(self, mock_resources_files):
287
+ mock_resources_files.return_value = Path(self.temp_dir.name)
288
+ url = reverse("docs_file", kwargs={"app_base_url": "nonexistent-app", "path": "css/style.css"})
289
+ response = self.client.get(url)
290
+ self.assertEqual(response.status_code, 404)
291
+ self.assertJSONEqual(response.content, {"detail": "Unknown base_url 'nonexistent-app'."})
292
+
293
+ @mock.patch.dict("nautobot.core.views.BASE_URL_TO_APP_LABEL", {"test-app": "test_app"})
294
+ @mock.patch("nautobot.core.views.resources.files")
295
+ def test_nonexistent_file(self, mock_resources_files):
296
+ mock_resources_files.return_value = Path(self.temp_dir.name)
297
+ test_cases = ["/../missing.html", "//../missing.html", "missing.html", "missing_dir/missing.html"]
298
+ for path in test_cases:
299
+ with self.subTest(path=path):
300
+ url = reverse("docs_file", kwargs={"app_base_url": self.test_base_url, "path": path})
301
+ response = self.client.get(url)
302
+ self.assertEqual(response.status_code, 404)
303
+ self.assertIn("File", response.json()["detail"])
304
+
305
+
193
306
  class MediaViewTestCase(TestCase):
194
307
  def test_media_unauthenticated(self):
195
308
  """
@@ -270,29 +383,45 @@ class SearchFieldsTestCase(TestCase):
270
383
  # SearchForm will redirect the user to the login Page
271
384
  self.assertEqual(response.status_code, 302)
272
385
 
273
- @tag("fix_in_v3")
274
- def test_global_and_model_search_bar(self):
386
+ def test_global_search_bar_scoped_to_model(self):
275
387
  self.add_permissions("dcim.view_location", "dcim.view_device")
276
388
 
277
389
  # Assert model search bar present in list UI
278
390
  response = self.client.get(reverse("dcim:location_list"))
279
391
  self.assertBodyContains(
280
392
  response,
281
- '<input type="text" name="q" class="form-control" required placeholder="Search Locations" id="id_q">',
393
+ '<input aria-placeholder="Press Ctrl+K to search" class="form-control nb-text-transparent" name="q" type="search" value="">',
394
+ html=True,
395
+ )
396
+ self.assertBodyContains(
397
+ response,
398
+ """
399
+ <span class="badge border" data-nb-link="/dcim/locations/"><!--
400
+ -->in: Locations<!--
401
+ --><button tabindex="-1" type="button">
402
+ <span aria-hidden="true" class="mdi mdi-close"></span>
403
+ <span class="visually-hidden">Remove</span>
404
+ </button>
405
+ """,
282
406
  html=True,
283
407
  )
284
408
 
285
409
  response = self.client.get(reverse("dcim:device_list"))
286
410
  self.assertBodyContains(
287
411
  response,
288
- '<input type="text" name="q" class="form-control" required placeholder="Search Devices" id="id_q">',
412
+ '<input aria-placeholder="Press Ctrl+K to search" class="form-control nb-text-transparent" name="q" type="search" value="">',
289
413
  html=True,
290
414
  )
291
-
292
- # Assert global search bar present in UI
293
- self.assertContains( # not using assertBodyContains because this is in the nav
415
+ self.assertBodyContains(
294
416
  response,
295
- '<input type="text" name="q" class="form-control" placeholder="Search Nautobot">',
417
+ """
418
+ <span class="badge border" data-nb-link="/dcim/devices/"><!--
419
+ -->in: Devices<!--
420
+ --><button tabindex="-1" type="button">
421
+ <span aria-hidden="true" class="mdi mdi-close"></span>
422
+ <span class="visually-hidden">Remove</span>
423
+ </button>
424
+ """,
296
425
  html=True,
297
426
  )
298
427
 
@@ -560,6 +689,7 @@ class MetricsViewTestCase(TestCase):
560
689
  page_content = response.content.decode(response.charset)
561
690
  return text_string_to_metric_families(page_content)
562
691
 
692
+ @tag("example_app")
563
693
  def test_metrics_extensibility(self):
564
694
  """Assert that the example metric from the Example App shows up _exactly_ when the app is enabled."""
565
695
  test_metric_name = "nautobot_example_metric_count"
@@ -582,6 +712,7 @@ class MetricsViewTestCase(TestCase):
582
712
  self.query_and_parse_metrics()
583
713
  self.assertTrue(mock_generate_latest_with_cache.call_count == 0)
584
714
 
715
+ @tag("example_app")
585
716
  @override_settings(METRICS_EXPERIMENTAL_CACHING_DURATION=30)
586
717
  def test_enabled_metrics_cache_enabled(self):
587
718
  """Assert that multiple calls to metrics with caching returns expected response."""
@@ -762,10 +893,7 @@ class SilkUIAccessTestCase(TestCase):
762
893
 
763
894
 
764
895
  class ExampleViewWithCustomPermissionsTest(TestCase):
765
- @skipIf(
766
- "example_app" not in settings.PLUGINS,
767
- "example_app not in settings.PLUGINS",
768
- )
896
+ @tag("example_app")
769
897
  @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
770
898
  def test_permission_classes_attribute_is_enforced(self):
771
899
  """
@@ -507,14 +507,7 @@ class Breadcrumbs:
507
507
  and there is `context['detail'] = True` set in context
508
508
 
509
509
  !!! important
510
- This class automatically adds the:
511
- - `InstanceBreadcrumbItem` at the end of `detail` breadcrumbs
512
- - `ModelBreadcrumbItem` at the beginning of `list` and `detail` breadcrumbs
513
-
514
- You can override this behavior by subclassing this class and updating
515
- the `list_breadcrumb_item` or `detail_breadcrumb_item` attributes.
516
-
517
- If you're using custom action other than `list` / `detail` you need to remember to add above breadcrumbs
510
+ If you're using custom action other than `list` / `detail` you need to remember to add list view breadcrumb manually
518
511
  if you need them in your custom action.
519
512
 
520
513
  Attributes:
@@ -553,7 +546,7 @@ class Breadcrumbs:
553
546
 
554
547
  # Set the default breadcrumbs
555
548
  self.items = {
556
- "list": [*self.breadcrumb_items],
549
+ "list": [],
557
550
  "detail": [*self.breadcrumb_items],
558
551
  }
559
552
 
@@ -561,9 +554,6 @@ class Breadcrumbs:
561
554
  if items:
562
555
  self.items = {**self.items, **items}
563
556
 
564
- # Built-in feature: always add the instance details at the end of breadcrumbs path
565
- self.items["detail"].append(InstanceBreadcrumbItem(label=detail_item_label))
566
-
567
557
  def get_breadcrumbs_items(self, context: Context) -> list[tuple[str, str]]:
568
558
  """
569
559
  Compute the list of breadcrumb items for the given context.
@@ -86,17 +86,149 @@ class EChartsTypeTheme(ChoiceSet):
86
86
 
87
87
  COLORS = {
88
88
  LIGHT: [
89
- UI_COLORS["blue-0"]["light"],
90
- UI_COLORS["orange-0"]["light"],
91
- UI_COLORS["green-0"]["light"],
92
- UI_COLORS["red-0"]["light"],
93
- UI_COLORS["gray-0"]["light"],
89
+ UI_COLORS["blue"]["light"],
90
+ UI_COLORS["purple"]["light"],
91
+ UI_COLORS["turquoise"]["light"],
92
+ UI_COLORS["orange"]["light"],
93
+ UI_COLORS["green"]["light"],
94
+ UI_COLORS["red"]["light"],
95
+ UI_COLORS["gray"]["light"],
96
+ UI_COLORS["blue-lighter"]["light"],
97
+ UI_COLORS["purple-lighter"]["light"],
98
+ UI_COLORS["turquoise-lighter"]["light"],
99
+ UI_COLORS["orange-lighter"]["light"],
100
+ UI_COLORS["green-lighter"]["light"],
101
+ UI_COLORS["red-lighter"]["light"],
102
+ UI_COLORS["gray-lighter"]["light"],
103
+ UI_COLORS["blue-darker"]["light"],
104
+ UI_COLORS["purple-darker"]["light"],
105
+ UI_COLORS["turquoise-darker"]["light"],
106
+ UI_COLORS["orange-darker"]["light"],
107
+ UI_COLORS["green-darker"]["light"],
108
+ UI_COLORS["red-darker"]["light"],
109
+ UI_COLORS["gray-darker"]["light"],
94
110
  ],
95
111
  DARK: [
96
- UI_COLORS["blue-0"]["dark"],
97
- UI_COLORS["orange-0"]["dark"],
98
- UI_COLORS["green-0"]["dark"],
99
- UI_COLORS["red-0"]["dark"],
100
- UI_COLORS["gray-0"]["dark"],
112
+ UI_COLORS["blue"]["dark"],
113
+ UI_COLORS["purple"]["dark"],
114
+ UI_COLORS["turquoise"]["dark"],
115
+ UI_COLORS["orange"]["dark"],
116
+ UI_COLORS["green"]["dark"],
117
+ UI_COLORS["red"]["dark"],
118
+ UI_COLORS["gray"]["dark"],
119
+ UI_COLORS["blue-lighter"]["dark"],
120
+ UI_COLORS["purple-lighter"]["dark"],
121
+ UI_COLORS["turquoise-lighter"]["dark"],
122
+ UI_COLORS["orange-lighter"]["dark"],
123
+ UI_COLORS["green-lighter"]["dark"],
124
+ UI_COLORS["red-lighter"]["dark"],
125
+ UI_COLORS["gray-lighter"]["dark"],
126
+ UI_COLORS["blue-darker"]["dark"],
127
+ UI_COLORS["purple-darker"]["dark"],
128
+ UI_COLORS["turquoise-darker"]["dark"],
129
+ UI_COLORS["orange-darker"]["dark"],
130
+ UI_COLORS["green-darker"]["dark"],
131
+ UI_COLORS["red-darker"]["dark"],
132
+ UI_COLORS["gray-darker"]["dark"],
101
133
  ],
102
134
  }
135
+
136
+
137
+ class NavigationIconChoices(ChoiceSet):
138
+ """Navigation icons for major Nautobot sections."""
139
+
140
+ DEVICES = "server"
141
+ IPAM = "sitemap-outline"
142
+ ORGANIZATION = "organization"
143
+ CIRCUITS = "cable-data"
144
+ VPN = "atom"
145
+ ROUTING = "route"
146
+ POWER = "battery-3"
147
+ WIRELESS = "wifi"
148
+ SECRETS = "secrets"
149
+ SECURITY = "security"
150
+ LOAD_BALANCERS = "arrow-decision"
151
+ VIRTUALIZATION = "cloud-upload"
152
+ CLOUD = "cloud"
153
+ DESIGN = "hammer"
154
+ APPROVAL_WORKFLOWS = "checkbox-circle"
155
+ EXTENSIBILITY = "extensibility"
156
+ GOLDEN_CONFIG = "sliders-vert-2"
157
+ JOBS = "share"
158
+ APPS = "elements"
159
+
160
+ CHOICES = (
161
+ (DEVICES, "Devices"),
162
+ (IPAM, "IPAM"),
163
+ (ORGANIZATION, "Organization"),
164
+ (CIRCUITS, "Circuits"),
165
+ (VPN, "VPN"),
166
+ (ROUTING, "Routing"),
167
+ (POWER, "Power"),
168
+ (WIRELESS, "Wireless"),
169
+ (SECRETS, "Secrets"),
170
+ (SECURITY, "Security"),
171
+ (LOAD_BALANCERS, "Load Balancers"),
172
+ (VIRTUALIZATION, "Virtualization"),
173
+ (CLOUD, "Cloud"),
174
+ (DESIGN, "Design"),
175
+ (APPROVAL_WORKFLOWS, "Approval Workflows"),
176
+ (EXTENSIBILITY, "Extensibility"),
177
+ (GOLDEN_CONFIG, "Golden Config"),
178
+ (JOBS, "Jobs"),
179
+ (APPS, "Apps"),
180
+ )
181
+
182
+
183
+ class NavigationWeightChoices(ChoiceSet):
184
+ """Navigation weights for major Nautobot sections."""
185
+
186
+ # In general we are looking to:
187
+ # - Keep data models before the default weight of 1000
188
+ # - Leave a gap between the default location of 1000
189
+ # - Keep non-model items after the default weight of 1000
190
+ # - Keep key items of GC, Jobs, and Apps at the end for easy access
191
+ DEVICES = 100
192
+ IPAM = 200
193
+ ORGANIZATION = 300
194
+ CIRCUITS = 400
195
+ VPN = 450
196
+ ROUTING = 500
197
+ POWER = 550
198
+ WIRELESS = 600
199
+ SECRETS = 650
200
+ SECURITY = 700
201
+ LOAD_BALANCERS = 750
202
+ VIRTUALIZATION = 800
203
+ CLOUD = 850
204
+ # We leave a gap here to allow for future expansion and don't use 1000
205
+ # since it the default weight for NavMenuTab if none is specified.
206
+ DESIGN = 1100
207
+ APPROVAL_WORKFLOWS = 1200
208
+ EXTENSIBILITY = 1300
209
+ # look to keep these last few items at the end of the nav for easy access
210
+ GOLDEN_CONFIG = 2000
211
+ JOBS = 2100
212
+ APPS = 2200
213
+
214
+ CHOICES = (
215
+ (DEVICES, "Devices"),
216
+ (IPAM, "IPAM"),
217
+ (ORGANIZATION, "Organization"),
218
+ (CIRCUITS, "Circuits"),
219
+ (VPN, "VPN"),
220
+ (ROUTING, "Routing"),
221
+ (POWER, "Power"),
222
+ (WIRELESS, "Wireless"),
223
+ (SECRETS, "Secrets"),
224
+ (SECURITY, "Security"),
225
+ (VIRTUALIZATION, "Virtualization"),
226
+ (LOAD_BALANCERS, "Load Balancers"),
227
+ (CLOUD, "Cloud"),
228
+ (DESIGN, "Design"),
229
+ (APPROVAL_WORKFLOWS, "Approval Workflows"),
230
+ (EXTENSIBILITY, "Extensibility"),
231
+ (GOLDEN_CONFIG, "Golden Config"),
232
+ (JOBS, "Jobs"),
233
+ (APPS, "Apps"),
234
+ )
@@ -1,22 +1,86 @@
1
1
  UI_COLORS = {
2
- "blue-0": {
2
+ "blue": {
3
3
  "light": "#007dff",
4
- "dark": "#045ab4",
4
+ "dark": "#339dff",
5
5
  },
6
- "orange-0": {
7
- "light": "#e07807",
8
- "dark": "#e07807",
6
+ "blue-darker": {
7
+ "light": "#0e4781",
8
+ "dark": "#255581",
9
+ },
10
+ "blue-lighter": {
11
+ "light": "#8cc5ff",
12
+ "dark": "#a3d3ff",
13
+ },
14
+ "gray": {
15
+ "light": "#b0b0b0",
16
+ "dark": "#828282",
17
+ },
18
+ "gray-darker": {
19
+ "light": "#5e5e5e",
20
+ "dark": "#494949",
21
+ },
22
+ "gray-lighter": {
23
+ "light": "#dbdbdb",
24
+ "dark": "#c7c7c7",
9
25
  },
10
- "green-0": {
26
+ "green": {
11
27
  "light": "#1ca92a",
12
- "dark": "#005c09",
28
+ "dark": "#2ecc40",
29
+ },
30
+ "green-darker": {
31
+ "light": "#1b5a21",
32
+ "dark": "#236a2b",
33
+ },
34
+ "green-lighter": {
35
+ "light": "#99d89f",
36
+ "dark": "#a1e8a9",
37
+ },
38
+ "orange": {
39
+ "light": "#e07807",
40
+ "dark": "#ff9933",
13
41
  },
14
- "red-0": {
42
+ "orange-darker": {
43
+ "light": "#734411",
44
+ "dark": "#815325",
45
+ },
46
+ "orange-lighter": {
47
+ "light": "#f1c28f",
48
+ "dark": "#ffd1a3",
49
+ },
50
+ "purple": {
51
+ "light": "#7b61ff",
52
+ "dark": "#a394ff",
53
+ },
54
+ "purple-darker": {
55
+ "light": "#463a81",
56
+ "dark": "#585181",
57
+ },
58
+ "purple-lighter": {
59
+ "light": "#c4b8ff",
60
+ "dark": "#d6cfff",
61
+ },
62
+ "red": {
15
63
  "light": "#e01f1f",
16
- "dark": "#960606",
64
+ "dark": "#ff4c4c",
65
+ },
66
+ "red-darker": {
67
+ "light": "#731c1c",
68
+ "dark": "#813131",
69
+ },
70
+ "red-lighter": {
71
+ "light": "#f19a9a",
72
+ "dark": "#ffaeae",
73
+ },
74
+ "turquoise": {
75
+ "light": "#00bfa5",
76
+ "dark": "#26d7c5",
77
+ },
78
+ "turquoise-darker": {
79
+ "light": "#0e6459",
80
+ "dark": "#1f6f67",
17
81
  },
18
- "gray-0": {
19
- "light": "#505d68",
20
- "dark": "#4f5868",
82
+ "turquoise-lighter": {
83
+ "light": "#8ce2d7",
84
+ "dark": "#9dede5",
21
85
  },
22
86
  }