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
@@ -2,10 +2,14 @@
2
2
  {% if object.last_updated %}
3
3
  <small class="align-items-center d-inline-flex gap-4 text-secondary">
4
4
  {% if object.created %}
5
- <span class="mdi mdi-bookmark-plus"></span> {{ object.created }}
6
- <span class="vr mx-4"></span>
5
+ <span data-bs-toggle="tooltip" title="Created at">
6
+ <span class="mdi mdi-bookmark-plus"></span> {{ object.created }}
7
+ <span class="vr mx-4"></span>
8
+ </span>
7
9
  {% endif %}
8
- <span class="mdi mdi-clock"></span> {{ object.last_updated|timesince }} ago
10
+ <span data-bs-toggle="tooltip" title="Last updated">
11
+ <span class="mdi mdi-clock"></span> {{ object.last_updated|timesince }} ago
12
+ </span>
9
13
  </small>
10
14
  {% endif %}
11
15
  </p>
@@ -10,7 +10,7 @@
10
10
  data-nb-toggle="collapse-all"
11
11
  type="button"
12
12
  >
13
- Collapse All
13
+ Collapse All Groups
14
14
  </button>
15
15
  </div>
16
16
  <table id="custom_fields_accordion_{{ advanced_ui }}" class="table table-hover card-body attr-table">
@@ -0,0 +1,6 @@
1
+ <div class="mb-10 d-md-flex">
2
+ <label class="col-md-3 col-form-label{% if required %} nb-required{% endif %}">{{ label }}</label>
3
+ <div class="col-md-9">
4
+ <p class="form-control-plaintext">{{ value }}</p>
5
+ </div>
6
+ </div>
@@ -60,7 +60,7 @@
60
60
  {% firstof request.user.get_full_name request.user.username %}
61
61
  </button>
62
62
  <ul class="dropdown-menu dropdown-menu-end">
63
- <li><a class="dropdown-item" href="{% url 'about' %}"><span class="mdi mdi-information"></span>About</a></li>
63
+ <li><a class="dropdown-item" href="{% url 'about' %}"><span class="mdi mdi-information"></span>About Nautobot</a></li>
64
64
  <li><a class="dropdown-item" href="{% url 'user:profile' %}"><span class="mdi mdi-account-box"></span>Profile</a></li>
65
65
  <li><a class="dropdown-item" href="{% url 'extras:approver_dashboard' %}"><span class="mdi mdi-check-bold"></span>Approval Dashboard</a></li>
66
66
  {% if request.user.is_staff %}
@@ -1,3 +1,4 @@
1
+ {% comment %}3.0 TODO: Potentially remove this template pending no other references to it.{% endcomment %}
1
2
  {% if images %}
2
3
  <table class="table table-hover card-body">
3
4
  <tr>
@@ -33,4 +34,4 @@
33
34
  <div class="card-body">
34
35
  <span class="text-secondary">None</span>
35
36
  </div>
36
- {% endif %}
37
+ {% endif %}
@@ -10,6 +10,7 @@
10
10
  aria-expanded="{% if cookie_sidenav_collapsed == 'true' %}false{% else %}true{% endif %}"
11
11
  aria-label="Collapse sidenav"
12
12
  class="bg-warning btn btm-sm btn-warning overflow-hidden p-4 position-absolute nb-sidenav-toggler"
13
+ data-bs-theme="dark"
13
14
  type="button"
14
15
  ></button>
15
16
 
@@ -20,7 +21,7 @@
20
21
  <img src="{% custom_branding_or_static 'logo' 'img/nautobot_logo.svg' %}"/>
21
22
  </a>
22
23
 
23
- <ul class="list-unstyled my-0 overflow-auto pt-10 w-100">
24
+ <ul class="list-unstyled my-0 overflow-x-hidden overflow-y-auto pt-10 w-100">
24
25
  {% if request.user.is_authenticated %}
25
26
  {% if nav_menu_version_control %}
26
27
  <li>
@@ -21,13 +21,13 @@
21
21
  {% endfor %}
22
22
  </div>
23
23
  <div class="justify-content-between mb-n20 nb-form-sticky-footer">
24
- <button type="reset" class="btn btn-secondary">
25
- <span aria-hidden="true" class="mdi mdi-close me-4"></span><!--
26
- -->Clear All
27
- </button>
28
24
  <button type="submit" class="btn btn-primary">
29
25
  <span class="mdi mdi-check me-4" aria-hidden="true"></span><!--
30
26
  -->Apply Specified
31
27
  </button>
28
+ <button type="reset" class="btn btn-secondary">
29
+ <span aria-hidden="true" class="mdi mdi-close me-4"></span><!--
30
+ -->Clear All
31
+ </button>
32
32
  </div>
33
33
  </form>
@@ -2,9 +2,11 @@
2
2
  {% load helpers %}
3
3
  {% load form_helpers %}
4
4
 
5
+ {% block title %}Home{% endblock title %}
6
+
5
7
  {% block content %}
6
- <div class="row" style="margin-block-start: {% if 'BANNER_LOGIN'|settings_or_config %}6.25{% else %}9.375{% endif %}rem;">
7
- <div class="col-md-4 offset-md-4">
8
+ <div class="row justify-content-center" style="margin-block-start: {% if 'BANNER_LOGIN'|settings_or_config %}6.25{% else %}9.375{% endif %}rem;">
9
+ <div class="col-sm-12 col-md-9 col-lg-7 nb-lg-max-width">
8
10
  {% if "BANNER_LOGIN"|settings_or_config %}
9
11
  <div class="alert alert-info text-center" role="alert">
10
12
  {{ "BANNER_LOGIN"|settings_or_config|render_markdown }}
@@ -358,11 +358,12 @@ SECRET_KEY = os.getenv("NAUTOBOT_SECRET_KEY", "{{ secret_key }}")
358
358
  # DEPLOYMENT_ID = os.environ["NAUTOBOT_DEPLOYMENT_ID"]
359
359
 
360
360
  # Device names are not guaranteed globally-unique by Nautobot but in practice they often are.
361
- # Set this to True to use the device name alone as the natural key for Device objects.
362
- # Set this to False to use the sequence (name, tenant, location) as the natural key instead.
363
- #
364
- # if "NAUTOBOT_DEVICE_NAME_AS_NATURAL_KEY" in os.environ and os.environ["NAUTOBOT_DEVICE_NAME_AS_NATURAL_KEY"] != "":
365
- # DEVICE_NAME_AS_NATURAL_KEY = is_truthy(os.environ["NAUTOBOT_DEVICE_NAME_AS_NATURAL_KEY"])
361
+ # Select how Devices are uniquely identified:
362
+ # - 'location_tenant_name': combination of Location + Tenant + Name
363
+ # - 'name': Device name must be globally unique
364
+ # - 'none': No enforced uniqueness (rely on other validation rules or custom validators)
365
+ # if "NAUTOBOT_DEVICE_UNIQUENESS" in os.environ and os.environ["NAUTOBOT_DEVICE_UNIQUENESS"] != "":
366
+ # DEVICE_UNIQUENESS = os.environ["NAUTOBOT_DEVICE_UNIQUENESS"]
366
367
 
367
368
  # Event Brokers
368
369
  # EVENT_BROKERS = {}
@@ -51,6 +51,13 @@
51
51
 
52
52
  {# Redoc doesn't change outer page styles. #}
53
53
  body { margin: 0; padding: 0; }
54
+
55
+ [data-bs-theme="dark"] .redoc-wrap,
56
+ [data-bs-theme="dark"] .redoc-wrap > :last-child,
57
+ [data-bs-theme="dark"] .redoc-wrap .api-content > [data-section-id] > [data-section-id] > :last-child,
58
+ [data-bs-theme="dark"] .redoc-wrap .menu-content .operation-type {
59
+ filter: hue-rotate(180deg) invert(1);
60
+ }
54
61
  </style>
55
62
  {% endblock extra_styles %}
56
63
 
@@ -32,7 +32,7 @@
32
32
  {% for obj_type in results %}
33
33
  <a href="#{{ obj_type.name|lower }}" class="list-group-item">
34
34
  {{ obj_type.name|bettertitle }}
35
- <span class="badge">{{ obj_type.table.page.paginator.count }}</span>
35
+ <span class="badge bg-primary">{{ obj_type.table.page.paginator.count }}</span>
36
36
  </a>
37
37
  {% endfor %}
38
38
  </div>
@@ -7,13 +7,27 @@
7
7
  {% block extra_styles %}
8
8
  <link rel="stylesheet" href="{% static 'drf_spectacular_sidecar/swagger-ui-dist/swagger-ui.css' %}">
9
9
  <style>
10
- /* Manual styling inherited from drf-spectacular */
10
+ /* Manual styling inherited from drf-spectacular */
11
11
  html { box-sizing: border-box; overflow-y: scroll; }
12
12
  *, *:after, *:before { box-sizing: inherit; }
13
- body { background: #fafafa; margin: 0; }
14
- /* Style overrides to address conflicts between Swagger styling and Nautobot/Bootstrap styling */
13
+ body { background: var(--bs-body-bg); margin: 0; }
14
+ /* Style overrides to address conflicts between Swagger styling and Nautobot/Bootstrap styling */
15
15
  pre.version { border: none; background: none; }
16
16
  .loading-container .loading { display: block; }
17
+ /* Dark theme support */
18
+ [data-bs-theme="dark"] .swagger-ui,
19
+ [data-bs-theme="dark"] .swagger-ui .backdrop-ux,
20
+ [data-bs-theme="dark"] .swagger-ui .microlight,
21
+ [data-bs-theme="dark"] .swagger-ui .opblock .opblock-summary-method {
22
+ filter: hue-rotate(180deg) invert(1);
23
+ }
24
+ [data-bs-theme="dark"] .swagger-ui input:is([type="email"], [type="file"], [type="password"], [type="search"], [type="text"]),
25
+ [data-bs-theme="dark"] .swagger-ui textarea {
26
+ color: var(--bs-body-bg);
27
+ }
28
+ [data-bs-theme="dark"] .swagger-ui .scheme-container {
29
+ background: transparent;
30
+ }
17
31
  </style>
18
32
  {% endblock extra_styles %}
19
33
 
@@ -164,10 +164,9 @@
164
164
  <div class="modal-dialog" role="document">
165
165
  <div class="modal-content">
166
166
  <div class="modal-header">
167
+ <h4 class="modal-title">${field.name} Choices</h4>
167
168
  <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
168
- <span aria-hidden="true">&times;</span>
169
169
  </button>
170
- <h4 class="modal-title"><code>${field.name}</code> Choices</h4>
171
170
  </div>
172
171
  <table class="table table-hover modal-body">
173
172
  <tr><th>Import Value</th><th>Label</th></tr>
@@ -3,7 +3,7 @@
3
3
 
4
4
  {% block content %}
5
5
  <div class="row">
6
- <div class="col-md-6 mx-auto">
6
+ <div class="col-md-8 col-lg-6 mx-auto nb-lg-max-width">
7
7
  <form action="" method="post">
8
8
  {% csrf_token %}
9
9
  {% for field in form.hidden_fields %}
@@ -14,7 +14,7 @@
14
14
  {% block title %}{% endblock %}
15
15
  </div>
16
16
  <div class="card-body">
17
- {% block message %}<p>Are you sure?</p>{% endblock %}
17
+ <div class="mb-16">{% block message %}<p>Are you sure?</p>{% endblock %}</div>
18
18
  <div class="gap-8 hstack justify-content-end">
19
19
  <button type="submit" name="_confirm" class="btn btn-{{ button_class|default:'danger' }}">
20
20
  <span aria-hidden="true" class="mdi mdi-check me-4"></span><!--
@@ -14,12 +14,20 @@
14
14
  </div>
15
15
  <div class="hstack gap-8">
16
16
  {% if bulk_edit_url and permissions.change %}
17
- <button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if bulk_querystring %}?{{ bulk_querystring }}{% elif request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm" disabled="disabled">
17
+ <button type="submit"
18
+ name="_edit"
19
+ formaction="{% url bulk_edit_url %}{% if bulk_querystring %}?{{ bulk_querystring }}{% elif request.GET %}?{{ request.GET.urlencode }}{% endif %}"
20
+ class="btn btn-warning btn-sm"
21
+ disabled>
18
22
  <span class="mdi mdi-pencil me-4" aria-hidden="true"></span>Edit All
19
23
  </button>
20
24
  {% endif %}
21
25
  {% if bulk_delete_url and permissions.delete %}
22
- <button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if bulk_querystring %}?{{ bulk_querystring }}{% elif request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm" disabled="disabled">
26
+ <button type="submit"
27
+ name="_delete"
28
+ formaction="{% url bulk_delete_url %}{% if bulk_querystring %}?{{ bulk_querystring }}{% elif request.GET %}?{{ request.GET.urlencode }}{% endif %}"
29
+ class="btn btn-danger btn-sm"
30
+ disabled>
23
31
  <span class="mdi mdi-trash-can-outline me-4" aria-hidden="true"></span>Delete All
24
32
  </button>
25
33
  {% endif %}
@@ -1,15 +1,12 @@
1
1
  {% load form_helpers %}
2
2
 
3
- <div class="mb-10 d-flex justify-content-center{% if field.errors %} has-error{% endif %}">
3
+ <div class="mb-10 d-md-flex justify-content-center{% if field.errors %} has-error{% endif %}">
4
4
  {% if field|widget_type == 'checkboxinput' %}
5
5
  <div class="col-md-9 offset-md-3">
6
6
  <div class="form-check mt-6{% if field.errors %} has-error{% endif %}">
7
7
  {{ field }}
8
8
  <label class="form-check-label" for="{{ field.id_for_label }}">
9
9
  {{ field.label }}
10
- {% if field.help_text %}
11
- <span class="form-text">{{ field.help_text|safe }}</span>
12
- {% endif %}
13
10
  </label>
14
11
  </div>
15
12
  {% if bulk_nullable %}
@@ -18,6 +15,9 @@
18
15
  <label class="form-check-label" for="{{ field.id_for_label }}_nullify">Set null</label>
19
16
  </div>
20
17
  {% endif %}
18
+ {% if field.help_text %}
19
+ <span class="form-text">{{ field.help_text|safe }}</span>
20
+ {% endif %}
21
21
  {% elif field|widget_type == 'textarea' and not field.label %}
22
22
  <div class="col-md-12">
23
23
  {{ field }}
@@ -33,9 +33,6 @@
33
33
  {% else %}
34
34
  <label class="col-md-3 col-form-label{% if field.field.required %} nb-required{% endif %}" for="{{ field.id_for_label }}">
35
35
  {{ field.label }}
36
- {% if field.help_text %}
37
- <span class="form-text">{{ field.help_text|safe }}</span>
38
- {% endif %}
39
36
  </label>
40
37
  <div class="col-md-9">
41
38
  {{ field }}
@@ -45,6 +42,9 @@
45
42
  <label class="form-check-label" for="{{ field.id_for_label }}_nullify">Set null</label>
46
43
  </div>
47
44
  {% endif %}
45
+ {% if field.help_text %}
46
+ <span class="form-text">{{ field.help_text|safe }}</span>
47
+ {% endif %}
48
48
  {% endif %}
49
49
  {% if field.errors %}
50
50
  <ul class="mb-0 mt-4">
@@ -26,13 +26,13 @@
26
26
  <div class="card">
27
27
  <div class="card-header"><strong>Template</strong></div>
28
28
  <div class="card-body">
29
- <div class="mb-10 d-flex justify-content-center">
29
+ <div class="mb-10 d-lg-flex">
30
30
  <label class="col-lg-3 col-form-label nb-required" for="id_template_code">Jinja Template</label>
31
31
  <div class="col-lg-9">
32
32
  <textarea cols="40" rows="10" class="form-control" id="id_template_code" required=""></textarea>
33
33
  </div>
34
34
  </div>
35
- <div class="mb-10 d-flex justify-content-center">
35
+ <div class="mb-10 d-lg-flex">
36
36
  <label class="col-lg-3 col-form-label" for="id_context">Data</label>
37
37
  <div class="col-lg-9">
38
38
  <textarea cols="40" rows="10" class="form-control" placeholder="{}" id="id_context">{}</textarea>
@@ -75,14 +75,14 @@
75
75
  </div>
76
76
 
77
77
  <div class="d-print-none justify-content-between mb-n20 nb-form-sticky-footer">
78
- <button type="reset" class="btn btn-secondary">
79
- <span aria-hidden="true" class="mdi mdi-close me-4"></span><!--
80
- -->Clear All
81
- </button>
82
78
  <button type="submit" class="btn btn-primary">
83
79
  <span class="mdi mdi-check me-4" aria-hidden="true"></span><!--
84
80
  -->Apply Specified
85
81
  </button>
82
+ <button type="reset" class="btn btn-secondary">
83
+ <span aria-hidden="true" class="mdi mdi-close me-4"></span><!--
84
+ -->Clear All
85
+ </button>
86
86
  </div>
87
87
  </form>
88
88
  </div>
@@ -56,7 +56,7 @@
56
56
  {% export_button content_type list_element=True %}
57
57
  </ul>
58
58
  </div>
59
- <div class="btn-group">
59
+ <div class="dropdown d-inline-flex">
60
60
  <button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
61
61
  Actions
62
62
  </button>
@@ -234,11 +234,11 @@
234
234
  <div class="mb-10 d-flex justify-content-center">
235
235
  <label class="col-md-3 col-form-label nb-required" for="id_status">
236
236
  Status
237
- <span class="form-text">Help text for this form field</span>
238
237
  </label>
239
238
  <div class="col-md-9">
240
239
  <select name="status" class="nautobot-select2-api form-control select2-hidden-accessible" data-query-param-content_types="[&quot;dcim.device&quot;]" display-field="display" value-field="id" data-depth="0" required="" placeholder="Status" data-url="/api/extras/statuses/" id="id_status">
241
240
  </select>
241
+ <span class="form-text">Help text for this form field</span>
242
242
  </div>
243
243
  </div>
244
244
  </div>
@@ -471,6 +471,15 @@
471
471
  <button type="button" class="btn btn-link disabled">Link</button>
472
472
  </td>
473
473
  </tr>
474
+ <tr>
475
+ <td><code>btn-group</code> like radio buttons</td>
476
+ <td>
477
+ <div class="btn-group" role="group">
478
+ <button type="button" class="btn btn-primary bg-primary nb-text-body-bg">Current Active Option</button>
479
+ <button type="button" class="btn btn-primary">Alternative Option</button>
480
+ </div>
481
+ </td>
482
+ </tr>
474
483
  </table>
475
484
  </div>
476
485
  </div>
@@ -1120,13 +1129,14 @@ This:
1120
1129
  <h1>Nautobot Icons</h1>
1121
1130
  <div class="row">
1122
1131
  <div class="col-12">
1123
- <div class="column-gap-8 d-flex flex-wrap px-20 py-16 rounded row-gap-16" style="background-color: #121a25;">
1132
+ <div class="column-gap-8 d-flex flex-wrap px-20 py-16 rounded row-gap-16" style="background-color: #121212;">
1124
1133
  <div class="icon-preview"><img alt="360-degrees icon" src="{% static 'nautobot-icons/360-degrees.svg' %}">360-degrees</div>
1125
1134
  <div class="icon-preview"><img alt="arrow-decision icon" src="{% static 'nautobot-icons/arrow-decision.svg' %}">arrow-decision</div>
1126
1135
  <div class="icon-preview"><img alt="arrows-expand-rec icon" src="{% static 'nautobot-icons/arrows-expand-rec.svg' %}">arrows-expand-rec</div>
1127
1136
  <div class="icon-preview"><img alt="arrows-move-rec icon" src="{% static 'nautobot-icons/arrows-move-rec.svg' %}">arrows-move-rec</div>
1128
1137
  <div class="icon-preview"><img alt="arrows-move-2-rec icon" src="{% static 'nautobot-icons/arrows-move-2-rec.svg' %}">arrows-move-2-rec</div>
1129
1138
  <div class="icon-preview"><img alt="atom icon" src="{% static 'nautobot-icons/atom.svg' %}">atom</div>
1139
+ <div class="icon-preview"><img alt="power icon" src="{% static 'nautobot-icons/battery-3.svg' %}">battery-3</div>
1130
1140
  <div class="icon-preview"><img alt="branch icon" src="{% static 'nautobot-icons/branch.svg' %}">branch</div>
1131
1141
  <div class="icon-preview"><img alt="briefcase-2 icon" src="{% static 'nautobot-icons/briefcase-2.svg' %}">briefcase-2</div>
1132
1142
  <div class="icon-preview"><img alt="cable-data icon" src="{% static 'nautobot-icons/cable-data.svg' %}">cable-data</div>
@@ -1145,6 +1155,7 @@ This:
1145
1155
  <div class="icon-preview"><img alt="device-lifecycle icon" src="{% static 'nautobot-icons/device-lifecycle.svg' %}">device-lifecycle</div>
1146
1156
  <div class="icon-preview"><img alt="direction icon" src="{% static 'nautobot-icons/direction.svg' %}">direction</div>
1147
1157
  <div class="icon-preview"><img alt="elements icon" src="{% static 'nautobot-icons/elements.svg' %}">elements</div>
1158
+ <div class="icon-preview"><img alt="extensibility icon" src="{% static 'nautobot-icons/extensibility.svg' %}">extensibility</div>
1148
1159
  <div class="icon-preview"><img alt="globe icon" src="{% static 'nautobot-icons/globe.svg' %}">globe</div>
1149
1160
  <div class="icon-preview"><img alt="globe-2 icon" src="{% static 'nautobot-icons/globe-2.svg' %}">globe-2</div>
1150
1161
  <div class="icon-preview"><img alt="hammer icon" src="{% static 'nautobot-icons/hammer.svg' %}">hammer</div>
@@ -1154,6 +1165,7 @@ This:
1154
1165
  <div class="icon-preview"><img alt="lightning icon" src="{% static 'nautobot-icons/lightning.svg' %}">lightning</div>
1155
1166
  <div class="icon-preview"><img alt="list-unordered icon" src="{% static 'nautobot-icons/list-unordered.svg' %}">list-unordered</div>
1156
1167
  <div class="icon-preview"><img alt="map-view icon" src="{% static 'nautobot-icons/map-view.svg' %}">map-view</div>
1168
+ <div class="icon-preview"><img alt="organization icon" src="{% static 'nautobot-icons/organization.svg' %}">organization</div>
1157
1169
  <div class="icon-preview"><img alt="pin-2 icon" src="{% static 'nautobot-icons/pin-2.svg' %}">pin-2</div>
1158
1170
  <div class="icon-preview"><img alt="pin-3 icon" src="{% static 'nautobot-icons/pin-3.svg' %}">pin-3</div>
1159
1171
  <div class="icon-preview"><img alt="plug icon" src="{% static 'nautobot-icons/plug.svg' %}">plug</div>
@@ -1162,6 +1174,7 @@ This:
1162
1174
  <div class="icon-preview"><img alt="rotate-cw icon" src="{% static 'nautobot-icons/rotate-cw.svg' %}">rotate-cw</div>
1163
1175
  <div class="icon-preview"><img alt="route icon" src="{% static 'nautobot-icons/route.svg' %}">route</div>
1164
1176
  <div class="icon-preview"><img alt="secrets icon" src="{% static 'nautobot-icons/secrets.svg' %}">secrets</div>
1177
+ <div class="icon-preview"><img alt="security icon" src="{% static 'nautobot-icons/security.svg' %}">security</div>
1165
1178
  <div class="icon-preview"><img alt="server icon" src="{% static 'nautobot-icons/server.svg' %}">server</div>
1166
1179
  <div class="icon-preview"><img alt="server-2 icon" src="{% static 'nautobot-icons/server-2.svg' %}">server-2</div>
1167
1180
  <div class="icon-preview"><img alt="share icon" src="{% static 'nautobot-icons/share.svg' %}">share</div>
@@ -1 +1,3 @@
1
- <option value="{{ widget.value }}"{% include "django/forms/widgets/attrs.html" %}{% if widget.label.disabled %} disabled="disabled"{% endif %}>{{ widget.label.label|default:widget.label }}</option>
1
+ <option value="{{ widget.value }}"
2
+ {% include "django/forms/widgets/attrs.html" %}
3
+ {% if widget.label.disabled %} disabled{% endif %}>{{ widget.label.label|default:widget.label }}</option>
@@ -1,5 +1,6 @@
1
1
  from collections.abc import Iterable
2
2
  import datetime
3
+ from importlib import resources
3
4
  import json
4
5
  import logging
5
6
  import re
@@ -7,6 +8,7 @@ from typing import Literal
7
8
  from urllib.parse import parse_qs, quote_plus
8
9
 
9
10
  from django import template
11
+ from django.apps import apps
10
12
  from django.conf import settings
11
13
  from django.contrib import messages
12
14
  from django.contrib.auth.models import AnonymousUser
@@ -14,9 +16,11 @@ from django.contrib.staticfiles.finders import find
14
16
  from django.core.exceptions import ObjectDoesNotExist
15
17
  from django.templatetags.static import static, StaticNode
16
18
  from django.urls import NoReverseMatch, reverse
19
+ from django.utils.formats import date_format
17
20
  from django.utils.html import format_html, format_html_join, strip_tags
18
21
  from django.utils.safestring import mark_safe
19
22
  from django.utils.text import slugify as django_slugify
23
+ from django.utils.translation import gettext as _
20
24
  from django_jinja import library
21
25
  from markdown import markdown
22
26
  import yaml
@@ -483,11 +487,13 @@ def percentage(x, y):
483
487
  @library.filter()
484
488
  @register.filter()
485
489
  def get_docs_url(model):
486
- """Return the likely static documentation path for the specified model, if it can be found/predicted.
490
+ """Return the documentation URL for the specified model, if it can be found/predicted.
487
491
 
488
492
  - Core models, as of 2.0, are usually at `docs/user-guide/core-data-model/{app_label}/{model_name}.html`.
489
493
  - Models in the `extras` app are usually at `docs/user-guide/platform-functionality/{model_name}.html`.
490
- - Apps (plugins) are generally expected to be documented at `{app_label}/docs/models/{model_name}.html`.
494
+ - Apps (plugins) are expected to be documented within their package at
495
+ ``docs/models/{model_name}.html`` and are served dynamically through
496
+ the ``AppDocsView`` endpoint (``/docs/<app_name>/<path>``).
491
497
 
492
498
  Any model can define a `documentation_static_path` class attribute if it needs to override the above expectations.
493
499
 
@@ -502,10 +508,26 @@ def get_docs_url(model):
502
508
  Example:
503
509
  >>> get_docs_url(location_instance)
504
510
  "static/docs/models/dcim/location.html"
511
+ >>> get_docs_url(example_model)
512
+ "/docs/example-app/models/examplemodel.html"
505
513
  """
506
514
  if hasattr(model, "documentation_static_path"):
507
515
  path = model.documentation_static_path
508
516
  elif model._meta.app_label in settings.PLUGINS:
517
+ app_label = model._meta.app_label
518
+ app_config = apps.get_app_config(app_label)
519
+ app_base_url = getattr(app_config, "base_url", None) or app_config.label
520
+ path = f"models/{model._meta.model_name}.html"
521
+ # Check that the file actually exists inside the app's docs folder
522
+ try:
523
+ base_dir = resources.files(app_label) / "docs"
524
+ file_path = base_dir / path
525
+ if file_path.is_file():
526
+ return reverse("docs_file", kwargs={"app_base_url": app_base_url, "path": path})
527
+ except ModuleNotFoundError:
528
+ pass
529
+ logger.debug("No documentation found for %s (expected at %s)", type(model), path)
530
+ # define path to try to get static
509
531
  path = f"{model._meta.app_label}/docs/models/{model._meta.model_name}.html"
510
532
  elif model._meta.app_label == "extras":
511
533
  path = f"docs/user-guide/platform-functionality/{model._meta.model_name}.html"
@@ -759,7 +781,7 @@ def render_address(address):
759
781
  quote_plus(address),
760
782
  )
761
783
  address = format_html_join("", "{}<br>", ((line,) for line in address.split("\n")))
762
- return format_html('<div class="pull-right d-print-none">{}</div>{}', map_link, address)
784
+ return format_html('<div class="float-end d-print-none">{}</div>{}', map_link, address)
763
785
  return HTML_NONE
764
786
 
765
787
 
@@ -798,7 +820,11 @@ def render_button_class(value):
798
820
  """
799
821
  if value:
800
822
  base = value.split()[0]
801
- return format_html('<button class="btn btn-{}">{}</button>', base.lower(), base.capitalize())
823
+ return format_html(
824
+ '<button class="btn btn-{}">{}</button>',
825
+ base.lower() if base.lower() != "default" else "secondary",
826
+ base.capitalize(),
827
+ )
802
828
  return ""
803
829
 
804
830
 
@@ -831,6 +857,26 @@ def label_list(value, suffix=""):
831
857
  )
832
858
 
833
859
 
860
+ @library.filter()
861
+ @register.filter()
862
+ def format_timezone(time_zone):
863
+ """
864
+ Return a human-readable representation of a time zone including:
865
+ - Time zone name and UTC offset on the first line
866
+ - Local date and time on the next line (in smaller font)
867
+ """
868
+ if not time_zone:
869
+ return HTML_NONE
870
+
871
+ now = datetime.datetime.now(time_zone)
872
+
873
+ # Locale-aware formatting (respects USE_L10N + active locale)
874
+ local_time = date_format(now, format="DATETIME_FORMAT", use_l10n=True)
875
+
876
+ result = f"{time_zone} (UTC {now.strftime('%z')})<br><small>{_('Local time')}: {local_time}</small>"
877
+ return format_html(result)
878
+
879
+
834
880
  #
835
881
  # Tags
836
882
  #
@@ -899,7 +945,7 @@ def django_querystring(context, query_dict=None, **kwargs):
899
945
  {% django_querystring my_query_dict foo=3 %}
900
946
  """
901
947
  if query_dict is None:
902
- query_dict = context.request.GET
948
+ query_dict = context["request"].GET
903
949
  params = query_dict.copy()
904
950
  for key, value in kwargs.items():
905
951
  if value is None:
@@ -932,7 +978,7 @@ def table_config_button(table, table_name=None, extra_classes="", disabled=False
932
978
  <span class="mdi mdi-cog" aria-hidden="true"></span>
933
979
  <span class="visually-hidden">Configure</span>
934
980
  </button>"""
935
- return format_html(html_template, extra_classes, table_name, 'disabled="disabled"' if disabled else "", table_name)
981
+ return format_html(html_template, extra_classes, table_name, "disabled" if disabled else "", table_name)
936
982
 
937
983
 
938
984
  @register.inclusion_tag("utilities/templatetags/utilization_graph.html")