nautobot 3.0.0a3__py3-none-any.whl → 3.0.0rc1__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.
Files changed (388) hide show
  1. nautobot/apps/choices.py +4 -0
  2. nautobot/apps/ui.py +4 -0
  3. nautobot/apps/utils.py +8 -0
  4. nautobot/circuits/tests/integration/test_circuits_bulk_operations.py +0 -3
  5. nautobot/circuits/views.py +6 -2
  6. nautobot/core/api/serializers.py +1 -1
  7. nautobot/core/api/urls.py +1 -0
  8. nautobot/core/api/views.py +4 -0
  9. nautobot/core/choices.py +1 -1
  10. nautobot/core/cli/bootstrap_v3_to_v5.py +36 -13
  11. nautobot/core/cli/migrate_deprecated_templates.py +36 -9
  12. nautobot/core/filters.py +4 -0
  13. nautobot/core/forms/__init__.py +2 -0
  14. nautobot/core/forms/widgets.py +21 -2
  15. nautobot/core/jobs/__init__.py +56 -0
  16. nautobot/core/management/commands/generate_test_data.py +3 -3
  17. nautobot/core/models/__init__.py +11 -0
  18. nautobot/core/models/utils.py +1 -1
  19. nautobot/core/settings.py +17 -7
  20. nautobot/core/settings.yaml +4 -26
  21. nautobot/core/templates/admin/base.html +1 -2
  22. nautobot/core/templates/admin/change_list.html +9 -12
  23. nautobot/core/templates/base_django.html +1 -2
  24. nautobot/core/templates/components/panel/header_extra_content_table.html +1 -1
  25. nautobot/core/templates/components/tab/content_wrapper.html +4 -4
  26. nautobot/core/templates/echarts/echarts.html +21 -8
  27. nautobot/core/templates/generic/object_bulk_create.html +2 -2
  28. nautobot/core/templates/generic/object_bulk_delete.html +1 -1
  29. nautobot/core/templates/generic/object_bulk_edit.html +1 -1
  30. nautobot/core/templates/generic/object_bulk_import.html +1 -1
  31. nautobot/core/templates/generic/object_delete.html +1 -1
  32. nautobot/core/templates/generic/object_detail.html +1 -1
  33. nautobot/core/templates/generic/object_edit.html +1 -1
  34. nautobot/core/templates/generic/object_retrieve.html +2 -2
  35. nautobot/core/templates/graphene/graphiql.html +0 -1
  36. nautobot/core/templates/inc/footer.html +3 -1
  37. nautobot/core/templates/inc/header.html +10 -0
  38. nautobot/core/templates/inc/media.html +14 -0
  39. nautobot/core/templates/inc/nav_menu.html +1 -8
  40. nautobot/core/templates/inc/object_details_advanced_panel.html +2 -2
  41. nautobot/core/templates/nautobot_config.py.j2 +0 -6
  42. nautobot/core/templates/rest_framework/api.html +103 -2
  43. nautobot/core/templates/utilities/templatetags/filter_form_drawer.html +33 -0
  44. nautobot/core/templates/utilities/theme_preview.html +3 -0
  45. nautobot/core/templates/widgets/number_input_with_choices.html +44 -0
  46. nautobot/core/templatetags/helpers.py +24 -12
  47. nautobot/core/testing/integration.py +24 -13
  48. nautobot/core/testing/utils.py +18 -4
  49. nautobot/core/testing/views.py +104 -17
  50. nautobot/core/tests/integration/test_filters.py +48 -11
  51. nautobot/core/tests/integration/test_theme.py +22 -21
  52. nautobot/core/tests/nautobot_config.py +3 -0
  53. nautobot/core/tests/runner.py +1 -2
  54. nautobot/core/tests/test_breadcrumbs.py +21 -21
  55. nautobot/core/tests/test_jobs.py +73 -6
  56. nautobot/core/tests/test_renderers.py +59 -0
  57. nautobot/core/tests/test_settings_schema.py +1 -0
  58. nautobot/core/tests/test_templatetags_helpers.py +9 -0
  59. nautobot/core/tests/test_titles.py +0 -16
  60. nautobot/core/tests/test_ui.py +122 -3
  61. nautobot/core/tests/test_utils.py +41 -1
  62. nautobot/core/ui/breadcrumbs.py +68 -17
  63. nautobot/core/ui/bulk_buttons.py +1 -1
  64. nautobot/core/ui/choices.py +49 -65
  65. nautobot/core/ui/echarts.py +15 -20
  66. nautobot/core/ui/object_detail.py +54 -46
  67. nautobot/core/ui/titles.py +3 -6
  68. nautobot/core/urls.py +8 -8
  69. nautobot/core/utils/filtering.py +11 -1
  70. nautobot/core/utils/lookup.py +46 -0
  71. nautobot/core/views/mixins.py +31 -20
  72. nautobot/core/views/renderers.py +2 -3
  73. nautobot/data_validation/migrations/0002_data_migration_from_app.py +3 -2
  74. nautobot/dcim/api/serializers.py +3 -0
  75. nautobot/dcim/choices.py +49 -0
  76. nautobot/dcim/constants.py +7 -0
  77. nautobot/dcim/factory.py +1 -1
  78. nautobot/dcim/filters.py +13 -1
  79. nautobot/dcim/forms.py +89 -3
  80. nautobot/dcim/migrations/0075_interface_duplex_interface_speed_and_more.py +32 -0
  81. nautobot/dcim/migrations/{0075_add_deviceclusterassignment.py → 0076_add_deviceclusterassignment.py} +1 -1
  82. nautobot/dcim/migrations/{0076_device_cluster_to_clusters_data_migration.py → 0077_device_cluster_to_clusters_data_migration.py} +1 -1
  83. nautobot/dcim/migrations/{0077_remove_device_cluster.py → 0078_remove_device_cluster.py} +1 -1
  84. nautobot/dcim/migrations/{0078_remove_device_location_tenant_name_uniqueness.py → 0079_remove_device_location_tenant_name_uniqueness.py} +1 -1
  85. nautobot/dcim/migrations/{0079_device_name_data_migration.py → 0080_device_name_data_migration.py} +1 -1
  86. nautobot/dcim/migrations/0081_alter_device_device_redundancy_group_priority_and_more.py +25 -0
  87. nautobot/dcim/models/device_component_templates.py +33 -1
  88. nautobot/dcim/models/device_components.py +22 -1
  89. nautobot/dcim/models/devices.py +17 -4
  90. nautobot/dcim/tables/devices.py +15 -0
  91. nautobot/dcim/tables/devicetypes.py +8 -1
  92. nautobot/dcim/tables/racks.py +0 -2
  93. nautobot/dcim/tables/template_code.py +1 -1
  94. nautobot/dcim/templates/dcim/cable_trace.html +0 -2
  95. nautobot/dcim/templates/dcim/consoleport.html +1 -1
  96. nautobot/dcim/templates/dcim/consoleserverport.html +1 -1
  97. nautobot/dcim/templates/dcim/devicebay.html +1 -1
  98. nautobot/dcim/templates/dcim/frontport.html +1 -1
  99. nautobot/dcim/templates/dcim/inc/devicetype_component_table.html +1 -1
  100. nautobot/dcim/templates/dcim/inc/moduletype_component_table.html +1 -1
  101. nautobot/dcim/templates/dcim/inc/rack_elevation.html +1 -1
  102. nautobot/dcim/templates/dcim/interface.html +9 -1
  103. nautobot/dcim/templates/dcim/interface_edit.html +2 -0
  104. nautobot/dcim/templates/dcim/inventoryitem.html +1 -1
  105. nautobot/dcim/templates/dcim/module_consoleports.html +1 -1
  106. nautobot/dcim/templates/dcim/module_consoleserverports.html +1 -1
  107. nautobot/dcim/templates/dcim/module_frontports.html +1 -1
  108. nautobot/dcim/templates/dcim/module_interfaces.html +1 -1
  109. nautobot/dcim/templates/dcim/module_modulebays.html +1 -1
  110. nautobot/dcim/templates/dcim/module_poweroutlets.html +1 -1
  111. nautobot/dcim/templates/dcim/module_powerports.html +1 -1
  112. nautobot/dcim/templates/dcim/module_rearports.html +1 -1
  113. nautobot/dcim/templates/dcim/moduletype_list.html +2 -2
  114. nautobot/dcim/templates/dcim/poweroutlet.html +1 -1
  115. nautobot/dcim/templates/dcim/powerport.html +1 -1
  116. nautobot/dcim/templates/dcim/rack_elevation_list.html +1 -1
  117. nautobot/dcim/templates/dcim/rack_retrieve.html +0 -11
  118. nautobot/dcim/templates/dcim/rearport.html +1 -1
  119. nautobot/dcim/templates/dcim/trace/cable.html +1 -1
  120. nautobot/dcim/templates/dcim/virtualchassis_update.html +1 -1
  121. nautobot/dcim/tests/integration/test_controller.py +3 -6
  122. nautobot/dcim/tests/integration/test_controller_managed_device_group.py +1 -5
  123. nautobot/dcim/tests/integration/test_create_device.py +0 -2
  124. nautobot/dcim/tests/integration/test_device_bulk_operations.py +1 -3
  125. nautobot/dcim/tests/integration/test_fileinputpicker.py +6 -10
  126. nautobot/dcim/tests/integration/test_location_bulk_operations.py +0 -2
  127. nautobot/dcim/tests/integration/test_module_bay_position.py +3 -4
  128. nautobot/dcim/tests/test_api.py +186 -6
  129. nautobot/dcim/tests/test_filters.py +43 -1
  130. nautobot/dcim/tests/test_forms.py +110 -8
  131. nautobot/dcim/tests/test_graphql.py +44 -1
  132. nautobot/dcim/tests/test_models.py +265 -0
  133. nautobot/dcim/tests/test_tables.py +160 -0
  134. nautobot/dcim/tests/test_views.py +69 -7
  135. nautobot/dcim/views.py +232 -126
  136. nautobot/extras/api/views.py +51 -44
  137. nautobot/extras/datasources/git.py +3 -1
  138. nautobot/extras/filters.py +19 -2
  139. nautobot/extras/forms/forms.py +9 -2
  140. nautobot/extras/jobs.py +2 -0
  141. nautobot/extras/jobs_ui.py +4 -3
  142. nautobot/extras/management/__init__.py +2 -0
  143. nautobot/extras/management/commands/refresh_dynamic_group_member_caches.py +4 -1
  144. nautobot/extras/migrations/0131_configcontext_device_families.py +18 -0
  145. nautobot/extras/models/approvals.py +11 -1
  146. nautobot/extras/models/change_logging.py +4 -0
  147. nautobot/extras/models/jobs.py +1 -3
  148. nautobot/extras/models/models.py +10 -2
  149. nautobot/extras/plugins/marketplace_manifest.yml +49 -1
  150. nautobot/extras/plugins/views.py +0 -5
  151. nautobot/extras/querysets.py +8 -0
  152. nautobot/extras/tables.py +12 -0
  153. nautobot/extras/templates/django_ajax_tables/ajax_wrapper.html +2 -0
  154. nautobot/extras/templates/extras/configcontext_update.html +1 -0
  155. nautobot/extras/templates/extras/dynamicgroup_update.html +1 -1
  156. nautobot/extras/templates/extras/objectchange_retrieve.html +0 -2
  157. nautobot/extras/templates/extras/plugin_detail.html +3 -3
  158. nautobot/extras/templates/extras/secret_create.html +1 -1
  159. nautobot/extras/tests/integration/test_computedfields.py +8 -9
  160. nautobot/extras/tests/integration/test_customfields.py +1 -3
  161. nautobot/extras/tests/integration/test_dynamicgroups.py +7 -8
  162. nautobot/extras/tests/integration/test_relationships.py +0 -2
  163. nautobot/extras/tests/test_api.py +63 -0
  164. nautobot/extras/tests/test_changelog.py +24 -2
  165. nautobot/extras/tests/test_filters.py +36 -3
  166. nautobot/extras/tests/test_models.py +38 -2
  167. nautobot/extras/tests/test_utils.py +3 -4
  168. nautobot/extras/tests/test_views.py +22 -83
  169. nautobot/extras/urls.py +0 -14
  170. nautobot/extras/views.py +83 -52
  171. nautobot/ipam/filters.py +26 -0
  172. nautobot/ipam/tables.py +6 -0
  173. nautobot/ipam/templates/ipam/namespace_ip_addresses.html +1 -1
  174. nautobot/ipam/templates/ipam/namespace_prefixes.html +1 -1
  175. nautobot/ipam/templates/ipam/namespace_vrfs.html +1 -1
  176. nautobot/ipam/tests/test_filters.py +26 -1
  177. nautobot/ipam/tests/test_models.py +1 -1
  178. nautobot/ipam/views.py +9 -7
  179. nautobot/load_balancers/__init__.py +0 -0
  180. nautobot/load_balancers/api/__init__.py +1 -0
  181. nautobot/load_balancers/api/serializers.py +75 -0
  182. nautobot/load_balancers/api/urls.py +23 -0
  183. nautobot/load_balancers/api/views.py +61 -0
  184. nautobot/load_balancers/apps.py +17 -0
  185. nautobot/load_balancers/choices.py +167 -0
  186. nautobot/load_balancers/filters.py +225 -0
  187. nautobot/load_balancers/forms.py +532 -0
  188. nautobot/load_balancers/management/commands/__init__.py +0 -0
  189. nautobot/load_balancers/management/commands/generate_load_balancer_models_test_data.py +38 -0
  190. nautobot/load_balancers/migrations/0001_initial.py +465 -0
  191. nautobot/load_balancers/migrations/0002_create_default_statuses_pool_members.py +31 -0
  192. nautobot/load_balancers/migrations/__init__.py +0 -0
  193. nautobot/load_balancers/models.py +423 -0
  194. nautobot/load_balancers/navigation.py +80 -0
  195. nautobot/load_balancers/tables.py +255 -0
  196. nautobot/load_balancers/tests/__init__.py +474 -0
  197. nautobot/load_balancers/tests/test_api.py +353 -0
  198. nautobot/load_balancers/tests/test_filters.py +134 -0
  199. nautobot/load_balancers/tests/test_forms.py +266 -0
  200. nautobot/load_balancers/tests/test_models.py +195 -0
  201. nautobot/load_balancers/tests/test_views.py +229 -0
  202. nautobot/load_balancers/urls.py +17 -0
  203. nautobot/load_balancers/views.py +248 -0
  204. nautobot/project-static/dist/css/github-dark.min.css +10 -0
  205. nautobot/project-static/dist/css/github.min.css +10 -0
  206. nautobot/project-static/dist/css/nautobot.css +1 -11
  207. nautobot/project-static/dist/css/nautobot.css.map +1 -1
  208. nautobot/project-static/dist/js/libraries.js +1 -1
  209. nautobot/project-static/dist/js/libraries.js.map +1 -1
  210. nautobot/project-static/dist/js/nautobot.js +1 -1
  211. nautobot/project-static/dist/js/nautobot.js.map +1 -1
  212. nautobot/project-static/js/forms.js +13 -0
  213. nautobot/project-static/nautobot-icons/bus-globe.svg +3 -0
  214. nautobot/project-static/nautobot-icons/bus-shield-check.svg +3 -0
  215. nautobot/project-static/nautobot-icons/bus-shield.svg +3 -0
  216. nautobot/ui/package-lock.json +87 -4
  217. nautobot/ui/package.json +2 -1
  218. nautobot/ui/src/js/nautobot.js +0 -1
  219. nautobot/ui/src/js/select2.js +53 -2
  220. nautobot/ui/src/scss/nautobot.scss +51 -2
  221. nautobot/ui/webpack.config.js +13 -0
  222. nautobot/users/templates/users/preferences.html +11 -2
  223. nautobot/virtualization/filters.py +6 -1
  224. nautobot/virtualization/tests/test_filters.py +10 -1
  225. nautobot/virtualization/tests/test_models.py +1 -0
  226. nautobot/virtualization/views.py +4 -1
  227. nautobot/vpn/factory.py +25 -15
  228. nautobot/vpn/filters.py +1 -0
  229. nautobot/vpn/forms.py +1 -0
  230. nautobot/vpn/migrations/0001_initial.py +1 -1
  231. nautobot/vpn/models.py +16 -8
  232. nautobot/vpn/tables.py +5 -2
  233. nautobot/vpn/tests/test_api.py +0 -5
  234. nautobot/vpn/tests/test_forms.py +1 -2
  235. nautobot/vpn/tests/test_models.py +57 -7
  236. nautobot/vpn/tests/test_views.py +22 -3
  237. nautobot/vpn/views.py +78 -20
  238. {nautobot-3.0.0a3.dist-info → nautobot-3.0.0rc1.dist-info}/METADATA +4 -4
  239. {nautobot-3.0.0a3.dist-info → nautobot-3.0.0rc1.dist-info}/RECORD +243 -352
  240. nautobot/circuits/templates/circuits/circuit.html +0 -2
  241. nautobot/circuits/templates/circuits/circuit_edit.html +0 -2
  242. nautobot/circuits/templates/circuits/circuit_retrieve.html +0 -2
  243. nautobot/circuits/templates/circuits/circuit_update.html +0 -1
  244. nautobot/circuits/templates/circuits/circuittermination.html +0 -2
  245. nautobot/circuits/templates/circuits/circuittermination_edit.html +0 -2
  246. nautobot/circuits/templates/circuits/circuittermination_retrieve.html +0 -2
  247. nautobot/circuits/templates/circuits/circuittermination_update.html +0 -1
  248. nautobot/circuits/templates/circuits/circuittype.html +0 -2
  249. nautobot/circuits/templates/circuits/circuittype_retrieve.html +0 -2
  250. nautobot/circuits/templates/circuits/inc/circuit_termination.html +0 -85
  251. nautobot/circuits/templates/circuits/provider.html +0 -2
  252. nautobot/circuits/templates/circuits/provider_edit.html +0 -2
  253. nautobot/circuits/templates/circuits/provider_retrieve.html +0 -1
  254. nautobot/circuits/templates/circuits/provider_update.html +0 -1
  255. nautobot/circuits/templates/circuits/providernetwork.html +0 -2
  256. nautobot/circuits/templates/circuits/providernetwork_retrieve.html +0 -2
  257. nautobot/cloud/templates/cloud/cloudaccount_retrieve.html +0 -2
  258. nautobot/cloud/templates/cloud/cloudnetwork_retrieve.html +0 -2
  259. nautobot/cloud/templates/cloud/cloudresourcetype_retrieve.html +0 -2
  260. nautobot/cloud/templates/cloud/cloudservice_retrieve.html +0 -2
  261. nautobot/core/templates/buttons/import.html +0 -9
  262. nautobot/data_validation/templates/data_validation/datacompliance_retrieve.html +0 -1
  263. nautobot/dcim/templates/dcim/cable.html +0 -2
  264. nautobot/dcim/templates/dcim/cable_edit.html +0 -2
  265. nautobot/dcim/templates/dcim/controller/base.html +0 -2
  266. nautobot/dcim/templates/dcim/controller_retrieve.html +0 -2
  267. nautobot/dcim/templates/dcim/controller_wirelessnetworks.html +0 -2
  268. nautobot/dcim/templates/dcim/controllermanageddevicegroup_retrieve.html +0 -2
  269. nautobot/dcim/templates/dcim/device/base.html +0 -2
  270. nautobot/dcim/templates/dcim/device/consoleports.html +0 -2
  271. nautobot/dcim/templates/dcim/device/consoleserverports.html +0 -2
  272. nautobot/dcim/templates/dcim/device/devicebays.html +0 -2
  273. nautobot/dcim/templates/dcim/device/frontports.html +0 -2
  274. nautobot/dcim/templates/dcim/device/interfaces.html +0 -2
  275. nautobot/dcim/templates/dcim/device/inventory.html +0 -2
  276. nautobot/dcim/templates/dcim/device/modulebays.html +0 -2
  277. nautobot/dcim/templates/dcim/device/poweroutlets.html +0 -2
  278. nautobot/dcim/templates/dcim/device/powerports.html +0 -2
  279. nautobot/dcim/templates/dcim/device/rearports.html +0 -2
  280. nautobot/dcim/templates/dcim/device/wireless.html +0 -2
  281. nautobot/dcim/templates/dcim/device_component.html +0 -2
  282. nautobot/dcim/templates/dcim/device_edit.html +0 -2
  283. nautobot/dcim/templates/dcim/devicefamily_retrieve.html +0 -2
  284. nautobot/dcim/templates/dcim/deviceredundancygroup_retrieve.html +0 -2
  285. nautobot/dcim/templates/dcim/devicetype.html +0 -2
  286. nautobot/dcim/templates/dcim/devicetype_edit.html +0 -2
  287. nautobot/dcim/templates/dcim/devicetype_retrieve.html +0 -2
  288. nautobot/dcim/templates/dcim/inc/device_napalm_tabs.html +0 -1
  289. nautobot/dcim/templates/dcim/interfaceredundancygroup_retrieve.html +0 -2
  290. nautobot/dcim/templates/dcim/location.html +0 -2
  291. nautobot/dcim/templates/dcim/location_edit.html +0 -2
  292. nautobot/dcim/templates/dcim/location_retrieve.html +0 -2
  293. nautobot/dcim/templates/dcim/locationtype.html +0 -2
  294. nautobot/dcim/templates/dcim/locationtype_retrieve.html +0 -2
  295. nautobot/dcim/templates/dcim/manufacturer.html +0 -2
  296. nautobot/dcim/templates/dcim/modulebay_retrieve.html +0 -1
  297. nautobot/dcim/templates/dcim/platform.html +0 -2
  298. nautobot/dcim/templates/dcim/powerfeed.html +0 -2
  299. nautobot/dcim/templates/dcim/powerfeed_retrieve.html +0 -2
  300. nautobot/dcim/templates/dcim/powerpanel.html +0 -2
  301. nautobot/dcim/templates/dcim/powerpanel_edit.html +0 -2
  302. nautobot/dcim/templates/dcim/powerpanel_retrieve.html +0 -2
  303. nautobot/dcim/templates/dcim/rack.html +0 -2
  304. nautobot/dcim/templates/dcim/rack_edit.html +0 -2
  305. nautobot/dcim/templates/dcim/rackgroup.html +0 -2
  306. nautobot/dcim/templates/dcim/rackreservation.html +0 -2
  307. nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +0 -2
  308. nautobot/dcim/templates/dcim/softwareversion_retrieve.html +0 -2
  309. nautobot/dcim/templates/dcim/virtualchassis.html +0 -2
  310. nautobot/dcim/templates/dcim/virtualchassis_add.html +0 -2
  311. nautobot/dcim/templates/dcim/virtualchassis_edit.html +0 -2
  312. nautobot/dcim/templates/dcim/virtualchassis_retrieve.html +0 -2
  313. nautobot/dcim/templates/dcim/virtualdevicecontext_retrieve.html +0 -2
  314. nautobot/dcim/ui.py +0 -29
  315. nautobot/extras/templates/extras/computedfield.html +0 -2
  316. nautobot/extras/templates/extras/computedfield_retrieve.html +0 -2
  317. nautobot/extras/templates/extras/configcontext.html +0 -2
  318. nautobot/extras/templates/extras/configcontext_edit.html +0 -2
  319. nautobot/extras/templates/extras/configcontext_retrieve.html +0 -2
  320. nautobot/extras/templates/extras/configcontextschema.html +0 -2
  321. nautobot/extras/templates/extras/configcontextschema_edit.html +0 -2
  322. nautobot/extras/templates/extras/contact_retrieve.html +0 -2
  323. nautobot/extras/templates/extras/customfield.html +0 -2
  324. nautobot/extras/templates/extras/customfield_edit.html +0 -2
  325. nautobot/extras/templates/extras/customfield_retrieve.html +0 -2
  326. nautobot/extras/templates/extras/customlink.html +0 -2
  327. nautobot/extras/templates/extras/dynamicgroup.html +0 -2
  328. nautobot/extras/templates/extras/dynamicgroup_edit.html +0 -2
  329. nautobot/extras/templates/extras/exporttemplate.html +0 -2
  330. nautobot/extras/templates/extras/gitrepository.html +0 -2
  331. nautobot/extras/templates/extras/gitrepository_object_edit.html +0 -2
  332. nautobot/extras/templates/extras/graphqlquery.html +0 -2
  333. nautobot/extras/templates/extras/graphqlquery_list.html +0 -1
  334. nautobot/extras/templates/extras/graphqlquery_retrieve.html +0 -2
  335. nautobot/extras/templates/extras/job_detail.html +0 -2
  336. nautobot/extras/templates/extras/jobbutton_retrieve.html +0 -2
  337. nautobot/extras/templates/extras/jobhook.html +0 -2
  338. nautobot/extras/templates/extras/jobqueue_retrieve.html +0 -2
  339. nautobot/extras/templates/extras/jobresult.html +0 -2
  340. nautobot/extras/templates/extras/metadatatype_retrieve.html +0 -2
  341. nautobot/extras/templates/extras/note.html +0 -2
  342. nautobot/extras/templates/extras/note_retrieve.html +0 -1
  343. nautobot/extras/templates/extras/object_changelog.html +0 -2
  344. nautobot/extras/templates/extras/object_notes.html +0 -2
  345. nautobot/extras/templates/extras/objectchange.html +0 -2
  346. nautobot/extras/templates/extras/objectchange_list.html +0 -3
  347. nautobot/extras/templates/extras/relationship.html +0 -1
  348. nautobot/extras/templates/extras/secret.html +0 -1
  349. nautobot/extras/templates/extras/secret_edit.html +0 -1
  350. nautobot/extras/templates/extras/secretsgroup.html +0 -2
  351. nautobot/extras/templates/extras/secretsgroup_edit.html +0 -2
  352. nautobot/extras/templates/extras/secretsgroup_retrieve.html +0 -2
  353. nautobot/extras/templates/extras/status.html +0 -2
  354. nautobot/extras/templates/extras/tag.html +0 -2
  355. nautobot/extras/templates/extras/tag_edit.html +0 -2
  356. nautobot/extras/templates/extras/tag_retrieve.html +0 -2
  357. nautobot/extras/templates/extras/team_retrieve.html +0 -2
  358. nautobot/ipam/templates/ipam/namespace_retrieve.html +0 -1
  359. nautobot/ipam/templates/ipam/prefix.html +0 -2
  360. nautobot/ipam/templates/ipam/prefix_edit.html +0 -1
  361. nautobot/ipam/templates/ipam/prefix_retrieve.html +0 -2
  362. nautobot/ipam/templates/ipam/rir.html +0 -2
  363. nautobot/ipam/templates/ipam/routetarget.html +0 -1
  364. nautobot/ipam/templates/ipam/service.html +0 -2
  365. nautobot/ipam/templates/ipam/service_edit.html +0 -2
  366. nautobot/ipam/templates/ipam/service_retrieve.html +0 -2
  367. nautobot/ipam/templates/ipam/vlan.html +0 -2
  368. nautobot/ipam/templates/ipam/vlan_edit.html +0 -2
  369. nautobot/ipam/templates/ipam/vlan_retrieve.html +0 -2
  370. nautobot/ipam/templates/ipam/vlangroup.html +0 -2
  371. nautobot/ipam/templates/ipam/vrf.html +0 -1
  372. nautobot/tenancy/templates/tenancy/tenant.html +0 -2
  373. nautobot/tenancy/templates/tenancy/tenant_edit.html +0 -2
  374. nautobot/tenancy/templates/tenancy/tenantgroup.html +0 -2
  375. nautobot/tenancy/templates/tenancy/tenantgroup_retrieve.html +0 -1
  376. nautobot/virtualization/templates/virtualization/clustergroup.html +0 -2
  377. nautobot/virtualization/templates/virtualization/clustertype.html +0 -2
  378. nautobot/virtualization/templates/virtualization/virtualmachine.html +0 -2
  379. nautobot/virtualization/templates/virtualization/virtualmachine_edit.html +0 -2
  380. nautobot/virtualization/templates/virtualization/virtualmachine_retrieve.html +0 -2
  381. nautobot/vpn/templates/vpn/vpnprofile.html +0 -2
  382. nautobot/wireless/templates/wireless/radioprofile_retrieve.html +0 -2
  383. nautobot/wireless/templates/wireless/supporteddatarate_retrieve.html +0 -2
  384. nautobot/wireless/templates/wireless/wirelessnetwork_retrieve.html +0 -2
  385. {nautobot-3.0.0a3.dist-info → nautobot-3.0.0rc1.dist-info}/LICENSE.txt +0 -0
  386. {nautobot-3.0.0a3.dist-info → nautobot-3.0.0rc1.dist-info}/NOTICE +0 -0
  387. {nautobot-3.0.0a3.dist-info → nautobot-3.0.0rc1.dist-info}/WHEEL +0 -0
  388. {nautobot-3.0.0a3.dist-info → nautobot-3.0.0rc1.dist-info}/entry_points.txt +0 -0
@@ -4,7 +4,6 @@ import contextlib
4
4
  from dataclasses import dataclass
5
5
  from enum import Enum
6
6
  import logging
7
- from typing import Any
8
7
  import uuid
9
8
 
10
9
  from django.contrib.contenttypes.models import ContentType
@@ -43,7 +42,7 @@ from nautobot.core.templatetags.helpers import (
43
42
  from nautobot.core.ui.choices import LayoutChoices, SectionChoices
44
43
  from nautobot.core.ui.echarts import EChartsBase
45
44
  from nautobot.core.ui.utils import render_component_template
46
- from nautobot.core.utils.lookup import get_filterset_for_model, get_route_for_model
45
+ from nautobot.core.utils.lookup import get_filterset_for_model, get_route_for_model, get_view_for_model
47
46
  from nautobot.core.utils.permissions import get_permission_for_model
48
47
  from nautobot.core.views.paginator import EnhancedPaginator, get_paginate_count
49
48
  from nautobot.core.views.utils import get_obj_from_context
@@ -247,6 +246,9 @@ class Button(Component):
247
246
  """
248
247
  if self.link_name and self.link_includes_pk:
249
248
  obj = get_obj_from_context(context, self.context_object_key)
249
+ if not obj:
250
+ logger.warning("Button %s has no object to link to", self.label)
251
+ return None
250
252
  return reverse(self.link_name, kwargs={"pk": obj.pk})
251
253
  elif self.link_name:
252
254
  return reverse(self.link_name)
@@ -310,6 +312,7 @@ class FormButton(Button):
310
312
  self,
311
313
  form_id: str,
312
314
  link_name: str,
315
+ render_on_tab_id="__all__",
313
316
  template_path="components/button/formbutton.html",
314
317
  **kwargs,
315
318
  ):
@@ -331,7 +334,7 @@ class FormButton(Button):
331
334
  if not self.form_id:
332
335
  raise ValueError("FormButton requires 'form_id' to be set in ObjectsTablePanel.")
333
336
 
334
- super().__init__(link_name=link_name, template_path=template_path, **kwargs)
337
+ super().__init__(link_name=link_name, render_on_tab_id=render_on_tab_id, template_path=template_path, **kwargs)
335
338
 
336
339
  def get_extra_context(self, context: Context):
337
340
  return {
@@ -768,6 +771,7 @@ class ObjectsTablePanel(Panel):
768
771
  select_related_fields=None,
769
772
  prefetch_related_fields=None,
770
773
  order_by_fields=None,
774
+ # TODO: Is `table_title` redundant with the base Panel's `label`?
771
775
  table_title=None,
772
776
  max_display_count=None,
773
777
  paginate=True,
@@ -778,6 +782,8 @@ class ObjectsTablePanel(Panel):
778
782
  add_permissions=None,
779
783
  hide_hierarchy_ui=False,
780
784
  related_field_name=None,
785
+ related_list_url_name=None,
786
+ enable_related_link=True,
781
787
  enable_bulk_actions=False,
782
788
  tab_id=None,
783
789
  body_wrapper_template_path="components/panel/body_wrapper_table.html",
@@ -829,6 +835,11 @@ class ObjectsTablePanel(Panel):
829
835
  hide_hierarchy_ui (bool, optional): Don't display hierarchy-based indentation of tree models in this table
830
836
  related_field_name (str, optional): The name of the filter/form field for the related model that links back
831
837
  to the base model. Defaults to the same as `table_filter` if unset. Used to populate URLs.
838
+ related_list_url_name (str, optional): The URL used to generate the list button URL for the related model.
839
+ If not provided, the default table's model `list` route is used.
840
+ This can be useful when the related model is a many-to-many relationship with a custom through table.
841
+ enable_related_link (bool, optional): If True, the badge on the related model will be a link to the related model list view.
842
+ When False, the badge will still show the count of the related model, but will not be a link.
832
843
  enable_bulk_actions (bool, optional): Show the pk toggle columns on the table if the user has the
833
844
  appropriate permissions.
834
845
  tab_id (str, optional): The ID of the tab this panel belongs to. Used to append to a `return_url` when
@@ -879,6 +890,8 @@ class ObjectsTablePanel(Panel):
879
890
  self.add_permissions = add_permissions or []
880
891
  self.hide_hierarchy_ui = hide_hierarchy_ui
881
892
  self.related_field_name = related_field_name
893
+ self.related_list_url_name = related_list_url_name
894
+ self.enable_related_link = enable_related_link
882
895
  self.enable_bulk_actions = enable_bulk_actions
883
896
  self.tab_id = tab_id
884
897
  self.footer_buttons = footer_buttons
@@ -905,7 +918,12 @@ class ObjectsTablePanel(Panel):
905
918
  related_field_name = self.related_field_name or self.table_filter or obj._meta.model_name
906
919
  return_url = context.get("return_url", obj.get_absolute_url())
907
920
  if self.tab_id:
908
- return_url += f"?tab={self.tab_id}"
921
+ try:
922
+ # Check to see if the this is a NautobotUIViewset action
923
+ view = get_view_for_model(obj._meta.model)
924
+ return_url += getattr(view, self.tab_id).url_path + "/"
925
+ except AttributeError:
926
+ return_url += f"?tab={self.tab_id}"
909
927
 
910
928
  if self.add_button_route is not None:
911
929
  add_permissions = self.add_permissions
@@ -1034,29 +1052,35 @@ class ObjectsTablePanel(Panel):
1034
1052
  body_content_table_model = body_content_table.Meta.model
1035
1053
  related_field_name = self.related_field_name or self.table_filter or obj._meta.model_name
1036
1054
 
1037
- list_url = getattr(self.table_class, "list_url", None)
1038
- if not list_url:
1039
- list_url = get_route_for_model(body_content_table_model, "list")
1055
+ body_content_table_list_url = None
1056
+ body_content_table_add_url = self._get_table_add_url(context)
1057
+ table_title = self.table_title or body_content_table_model._meta.verbose_name_plural
1040
1058
 
1041
- try:
1042
- list_route = reverse(list_url)
1043
- except NoReverseMatch:
1044
- list_route = None
1059
+ if self.enable_related_link:
1060
+ list_url = self.related_list_url_name or getattr(self.table_class, "list_url", None)
1061
+ if not list_url:
1062
+ list_url = get_route_for_model(body_content_table_model, "list")
1045
1063
 
1046
- if list_route:
1047
- body_content_table_list_url = f"{list_route}?{related_field_name}={obj.pk}"
1048
- else:
1049
- body_content_table_list_url = None
1064
+ try:
1065
+ list_route = reverse(list_url)
1066
+ except NoReverseMatch:
1067
+ logger.warning(
1068
+ f"Unable to determine a valid list URL for ObjectsTablePanel `{table_title}`"
1069
+ f" related to `{body_content_table_model.__name__}` with `{list_url}`."
1070
+ " If the related object is using a through table, consider setting the `related_list_url_name`"
1071
+ " parameter or disabling the related link via 'enable_related_link=False'."
1072
+ )
1073
+ list_route = None
1050
1074
 
1051
- body_content_table_add_url = self._get_table_add_url(context)
1052
- body_content_table_verbose_name_plural = self.table_title or body_content_table_model._meta.verbose_name_plural
1075
+ if list_route:
1076
+ body_content_table_list_url = f"{list_route}?{related_field_name}={obj.pk}"
1053
1077
 
1054
1078
  return {
1055
1079
  "body_content_table": body_content_table,
1056
1080
  "body_content_table_add_url": body_content_table_add_url,
1057
1081
  "body_content_table_list_url": body_content_table_list_url,
1058
1082
  "body_content_table_verbose_name": body_content_table_model._meta.verbose_name,
1059
- "body_content_table_verbose_name_plural": body_content_table_verbose_name_plural,
1083
+ "body_content_table_verbose_name_plural": table_title,
1060
1084
  "footer_buttons": self.footer_buttons,
1061
1085
  "form_id": self.form_id,
1062
1086
  "more_queryset_count": more_queryset_count,
@@ -1316,24 +1340,6 @@ class EChartsPanel(Panel, EChartsBase):
1316
1340
  super().__init__(body_wrapper_template_path=body_wrapper_template_path, body_id=self.body_id, **kwargs)
1317
1341
  EChartsBase.__init__(self, **chart_kwargs)
1318
1342
 
1319
- def get_data(self, context: Context) -> dict[str, Any] | None:
1320
- """Get the data for chart.
1321
-
1322
- Args:
1323
- context (Context): The template or request context.
1324
-
1325
- Returns:
1326
- dict[str, Any] | None:
1327
- - A dictionary in internal chart format, e.g.:
1328
- {"x": [...], "series": [{"name": str, "data": [...]}]}
1329
- - A nested dictionary of series, e.g.:
1330
- {"Series1": {"x1": val1, "x2": val2}, ...}
1331
- - `None` if no data is set.
1332
- """
1333
- if callable(self.data):
1334
- return self.data(context) # pylint: disable=not-callable
1335
- return self.data
1336
-
1337
1343
  def should_render(self, context: Context):
1338
1344
  """Determine if the panel should be rendered."""
1339
1345
  if not super().should_render(context):
@@ -1349,8 +1355,7 @@ class EChartsPanel(Panel, EChartsBase):
1349
1355
 
1350
1356
  def get_extra_context(self, context: Context):
1351
1357
  """Add chart-specific context variables."""
1352
- self.data = self.get_data(context)
1353
- chart_config = self.get_config()
1358
+ chart_config = self.get_config(context=context)
1354
1359
  return {
1355
1360
  **super().get_extra_context(context),
1356
1361
  "chart": self,
@@ -1765,7 +1770,7 @@ class StatsPanel(Panel):
1765
1770
  value = [related_object_list_url, related_object_count, related_object_title]
1766
1771
  stats[related_object_model_class] = value
1767
1772
  related_object_model_filterset = get_filterset_for_model(related_object_model_class)
1768
- if self.filter_name not in related_object_model_filterset.get_filters():
1773
+ if self.filter_name not in related_object_model_filterset.base_filters:
1769
1774
  raise FieldDoesNotExist(
1770
1775
  f"{self.filter_name} is not a valid filter field for {related_object_model_class_meta.verbose_name}"
1771
1776
  )
@@ -2144,8 +2149,8 @@ class _ObjectDetailContactsTab(Tab):
2144
2149
  max_display_count=100, # since there isn't a separate list view for ContactAssociations!
2145
2150
  # TODO: we should provide a standard reusable component template for bulk-actions in the footer
2146
2151
  footer_content_template_path="components/panel/footer_contacts_table.html",
2147
- header_extra_content_template_path=None,
2148
- label="Contacts/Teams",
2152
+ enable_related_link=False,
2153
+ table_title="Contacts/Teams",
2149
2154
  ),
2150
2155
  )
2151
2156
  super().__init__(tab_id=tab_id, label=label, weight=weight, panels=panels, **kwargs)
@@ -2185,9 +2190,8 @@ class _ObjectDetailDataComplianceTab(DistinctViewTab):
2185
2190
  table_class=DataComplianceTable,
2186
2191
  table_attribute="associated_data_compliance",
2187
2192
  related_field_name="object_id",
2188
- label="Data Compliance",
2193
+ table_title="Data Compliance",
2189
2194
  add_button_route=None,
2190
- header_extra_content_template_path=None,
2191
2195
  include_paginator=True,
2192
2196
  ),
2193
2197
  )
@@ -2199,7 +2203,12 @@ class _ObjectDetailDataComplianceTab(DistinctViewTab):
2199
2203
  def should_render(self, context: Context):
2200
2204
  if not super().should_render(context):
2201
2205
  return False
2202
- return getattr(get_obj_from_context(context), "is_data_compliance_model", False)
2206
+ obj = get_obj_from_context(context)
2207
+ if getattr(obj, "is_data_compliance_model", False):
2208
+ if obj.get_data_compliance_url() is not None:
2209
+ return True
2210
+ logger.warning("Missing data-compliance URL for %r", obj)
2211
+ return False
2203
2212
 
2204
2213
 
2205
2214
  class DynamicGroupsTextPanel(BaseTextPanel):
@@ -2307,8 +2316,7 @@ class _ObjectDetailMetadataTab(Tab):
2307
2316
  exclude_columns=["assigned_object"],
2308
2317
  add_button_route=None,
2309
2318
  related_field_name="assigned_object_id",
2310
- header_extra_content_template_path=None,
2311
- label="Object Metadata",
2319
+ table_title="Object Metadata",
2312
2320
  ),
2313
2321
  )
2314
2322
  super().__init__(
@@ -6,17 +6,14 @@ from django.utils.html import strip_tags
6
6
  DEFAULT_TITLES: dict[str, str] = {
7
7
  "*": "{{ verbose_name_plural|bettertitle }}",
8
8
  "list": "{{ verbose_name_plural|bettertitle }}",
9
- "detail": "{{ object.display|default:object }}",
10
- "retrieve": "{{ object.display|default:object }}",
9
+ "detail": "{{ object.page_title|default:object }}",
10
+ "retrieve": "{{ object.page_title|default:object }}",
11
11
  "destroy": "Delete {{ verbose_name }}?",
12
12
  "create": "Add a new {{ verbose_name }}",
13
- "update": "Editing {{ verbose_name }} {{ object.display|default:object }}",
13
+ "update": "Editing {{ verbose_name }} {{ object.page_title|default:object }}",
14
14
  "bulk_destroy": "Delete {{ total_objs_to_delete }} {{ verbose_name_plural|bettertitle }}?",
15
15
  "bulk_rename": "Renaming {{ selected_objects|length }} {{ verbose_name_plural|bettertitle }} on {{ parent_name }}",
16
16
  "bulk_update": "Editing {{ objs_count }} {{ verbose_name_plural|bettertitle }}",
17
- "changelog": "{{ object.display|default:object }} - Change Log",
18
- "config_context": "{{ object.display|default:object }} - Config Context",
19
- "notes": "{{ object.display|default:object }} - Notes",
20
17
  "approve": "Approve {{ verbose_name|bettertitle }}?",
21
18
  "deny": "Deny {{ verbose_name|bettertitle }}?",
22
19
  }
nautobot/core/urls.py CHANGED
@@ -42,6 +42,7 @@ urlpatterns = [
42
42
  path("dcim/", include("nautobot.dcim.urls")),
43
43
  path("extras/", include("nautobot.extras.urls")),
44
44
  path("ipam/", include("nautobot.ipam.urls")),
45
+ path("load-balancers/", include("nautobot.load_balancers.urls")),
45
46
  path("tenancy/", include("nautobot.tenancy.urls")),
46
47
  # TODO: deprecate this url and use users
47
48
  path("user/", include("nautobot.users.urls")),
@@ -103,15 +104,14 @@ urlpatterns = [
103
104
 
104
105
 
105
106
  if settings.DEBUG:
106
- try:
107
- import debug_toolbar
107
+ urlpatterns += [path("theme-preview/", ThemePreviewView.as_view(), name="theme_preview")]
108
+
109
+
110
+ if "debug_toolbar" in settings.INSTALLED_APPS:
111
+ from debug_toolbar.toolbar import debug_toolbar_urls
112
+
113
+ urlpatterns += debug_toolbar_urls()
108
114
 
109
- urlpatterns += [
110
- path("__debug__/", include(debug_toolbar.urls)),
111
- path("theme-preview/", ThemePreviewView.as_view(), name="theme_preview"),
112
- ]
113
- except ImportError:
114
- pass
115
115
 
116
116
  if settings.METRICS_ENABLED:
117
117
  if settings.METRICS_AUTHENTICATED:
@@ -107,6 +107,7 @@ def get_filterset_parameter_form_field(model, parameter, filterset=None):
107
107
  BOOLEAN_CHOICES,
108
108
  DynamicModelMultipleChoiceField,
109
109
  MultipleContentTypeField,
110
+ MultiValueCharInput,
110
111
  StaticSelect2,
111
112
  StaticSelect2Multiple,
112
113
  )
@@ -127,7 +128,16 @@ def get_filterset_parameter_form_field(model, parameter, filterset=None):
127
128
  elif isinstance(field, (MultiValueDecimalFilter, MultiValueFloatFilter)):
128
129
  form_field = forms.DecimalField()
129
130
  elif isinstance(field, NumberFilter):
130
- form_field = forms.IntegerField()
131
+ # If "choices" are passed, then when 'exact' is used in an Advanced
132
+ # Filter, render a dropdown of choices instead of a free integer input
133
+ if field.lookup_expr == "exact" and getattr(field, "choices", None):
134
+ # Use a multi-value widget that allows both preset choices and free-form entries
135
+ form_field = forms.MultipleChoiceField(
136
+ choices=field.choices,
137
+ widget=MultiValueCharInput,
138
+ )
139
+ else:
140
+ form_field = forms.IntegerField()
131
141
  elif isinstance(field, ModelMultipleChoiceFilter):
132
142
  if getattr(field, "prefers_id", False):
133
143
  to_field_name = "id"
@@ -61,6 +61,14 @@ def resolve_attr(obj, dotted_field):
61
61
  return str(val) if val else None
62
62
 
63
63
 
64
+ def get_breadcrumbs_for_model(model, view_type: str = "List"):
65
+ """Get a UI Component Framework 'Breadcrumbs' instance for the given model's related UIViewSet or generic view."""
66
+ view = get_view_for_model(model)
67
+ if hasattr(view, "get_breadcrumbs"):
68
+ return view.get_breadcrumbs(model, view_type=view_type)
69
+ return None
70
+
71
+
64
72
  def get_changes_for_model(model):
65
73
  """
66
74
  Return a queryset of ObjectChanges for a model or instance. The queryset will be filtered
@@ -78,6 +86,30 @@ def get_changes_for_model(model):
78
86
  raise TypeError(f"{model!r} is not a Django Model class or instance")
79
87
 
80
88
 
89
+ def get_detail_view_components_context_for_model(model) -> dict:
90
+ """Helper method for DistinctViewTabs etc. to retrieve the UI Component Framework context for the base detail view.
91
+
92
+ Functionally equivalent to calling `get_breadcrumbs_for_model()`, `get_object_detail_content_for_model()`, and
93
+ `get_view_titles_for_model()`, but marginally more efficient.
94
+ """
95
+ context = {
96
+ "breadcrumbs": None,
97
+ "object_detail_content": None,
98
+ "view_titles": None,
99
+ }
100
+
101
+ view = get_view_for_model(model, view_type="")
102
+ if view is not None:
103
+ if hasattr(view, "get_breadcrumbs"):
104
+ context["breadcrumbs"] = view.get_breadcrumbs(model, view_type="")
105
+ if hasattr(view, "get_view_titles"):
106
+ context["view_titles"] = view.get_view_titles(model, view_type="")
107
+ if hasattr(view, "object_detail_content"):
108
+ context["object_detail_content"] = view.object_detail_content
109
+
110
+ return context
111
+
112
+
81
113
  def get_model_from_name(model_name):
82
114
  """Given a full model name in dotted format (example: `dcim.model`), a model class is returned if valid.
83
115
 
@@ -222,6 +254,12 @@ def get_form_for_model(model, form_prefix=""):
222
254
  return get_related_class_for_model(model, module_name="forms", object_suffix=object_suffix)
223
255
 
224
256
 
257
+ def get_object_detail_content_for_model(model):
258
+ """Get the UI Component Framework 'object_detail_content' for the given model's related UIViewSet or ObjectView."""
259
+ view = get_view_for_model(model)
260
+ return getattr(view, "object_detail_content", None)
261
+
262
+
225
263
  def get_related_field_for_models(from_model, to_model):
226
264
  """
227
265
  Find the field on `from_model` that is a relation to `to_model`.
@@ -289,6 +327,14 @@ def get_view_for_model(model, view_type=""):
289
327
  return result
290
328
 
291
329
 
330
+ def get_view_titles_for_model(model, view_type: str = "List"):
331
+ """Get a UI Component Framework 'Titles' instance for the given model's related UIViewSet or generic view."""
332
+ view = get_view_for_model(model)
333
+ if hasattr(view, "get_view_titles"):
334
+ return view.get_view_titles(model, view_type=view_type)
335
+ return None
336
+
337
+
292
338
  def get_model_for_view_name(view_name):
293
339
  """
294
340
  Return the model class associated with the given view_name e.g. "circuits:circuit_detail", "dcim:device_list" and etc.
@@ -263,52 +263,57 @@ class UIComponentsMixin:
263
263
  breadcrumbs: ClassVar[Optional[Breadcrumbs]] = None
264
264
  view_titles: ClassVar[Optional[Titles]] = None
265
265
 
266
- def get_view_titles(self, model: Union[None, str, Type[Model], Model] = None, view_type: str = "List") -> Titles:
266
+ @classmethod
267
+ def get_view_titles(cls, model: Union[None, str, Type[Model], Model] = None, view_type: str = "List") -> Titles:
267
268
  """
268
269
  Resolve and return the `Titles` component instance.
269
270
 
270
271
  Resolution order:
271
- 1) If `self.view_titles` is set on the current view, use it.
272
- 2) Else, if `model` is provided, copy the `view_titles` from the view
273
- class associated with that model via `lookup.get_view_for_model(model, action)`.
272
+ 1) If `.view_titles` is set on the current view, use it.
273
+ 2) Else, if `model` is provided, copy the `view_titles` from the view class associated with that model
274
+ via `lookup.get_view_for_model(model, action)`.
274
275
  3) Else, instantiate and return the default `Titles()`.
275
276
 
276
277
  Args:
277
278
  model: A Django model **class**, **instance**, dotted name string, or `None`.
278
279
  Passed to `lookup.get_view_for_model()` to find the related view class.
279
280
  If `None`, only local/default resolution is used.
280
- view_type: Logical view type used by `lookup.get_view_for_model()` (e.g., `"List"` or empty to construct `"DeviceView"` string).
281
+ view_type: Logical view type used by `lookup.get_view_for_model()`
282
+ (e.g., `"List"` or empty to construct `"DeviceView"` string).
281
283
 
282
284
  Returns:
283
285
  Titles: A concrete `Titles` component instance ready to use.
284
286
  """
285
- return self._resolve_component("view_titles", Titles, model, view_type)
287
+ return cls._resolve_component("view_titles", Titles, model, view_type)
286
288
 
289
+ @classmethod
287
290
  def get_breadcrumbs(
288
- self, model: Union[None, str, Type[Model], Model] = None, view_type: str = "List"
291
+ cls, model: Union[None, str, Type[Model], Model] = None, view_type: str = "List"
289
292
  ) -> Breadcrumbs:
290
293
  """
291
294
  Resolve and return the `Breadcrumbs` component instance.
292
295
 
293
296
  Resolution order mirrors `get_view_titles()`:
294
- 1) Use `self.breadcrumbs` if set locally.
295
- 2) Else, if `model` is provided, copy the `breadcrumbs` from the view
296
- class associated with that model via `lookup.get_view_for_model(model, action)`.
297
+ 1) Use `.breadcrumbs` if set locally.
298
+ 2) Else, if `model` is provided, copy the `breadcrumbs` from the view class associated with that model
299
+ via `lookup.get_view_for_model(model, action)`.
297
300
  3) Else return a new default `Breadcrumbs()`.
298
301
 
299
302
  Args:
300
303
  model: A Django model **class**, **instance**, dotted name string, or `None`.
301
304
  Passed to `lookup.get_view_for_model()` to find the related view class.
302
305
  If `None`, only local/default resolution is used.
303
- view_type: Logical view type used by `lookup.get_view_for_model()` (e.g., `"List"` or empty to construct `"DeviceView"` string).
306
+ view_type: Logical view type used by `lookup.get_view_for_model()`
307
+ (e.g., `"List"` or empty to construct `"DeviceView"` string).
304
308
 
305
309
  Returns:
306
310
  Breadcrumbs: A concrete `Breadcrumbs` component instance.
307
311
  """
308
- return self._resolve_component("breadcrumbs", Breadcrumbs, model, view_type)
312
+ return cls._resolve_component("breadcrumbs", Breadcrumbs, model, view_type)
309
313
 
314
+ @classmethod
310
315
  def _resolve_component(
311
- self,
316
+ cls,
312
317
  attr_name: str,
313
318
  default_cls: Type[Union[Breadcrumbs, Titles]],
314
319
  model: Union[None, str, Type[Model], Model] = None,
@@ -329,14 +334,14 @@ class UIComponentsMixin:
329
334
  Returns:
330
335
  Breadcrumbs/Title instance.
331
336
  """
332
- local = getattr(self, attr_name, None)
337
+ local = getattr(cls, attr_name, None)
333
338
  if local is not None:
334
- return self._instantiate_if_needed(local, default_cls)
339
+ return cls._instantiate_if_needed(local, default_cls)
335
340
 
336
341
  if model is not None:
337
342
  view_class = lookup.get_view_for_model(model, view_type)
338
343
  view_component = getattr(view_class, attr_name, None)
339
- return self._instantiate_if_needed(view_component, default_cls)
344
+ return cls._instantiate_if_needed(view_component, default_cls)
340
345
 
341
346
  return default_cls()
342
347
 
@@ -732,7 +737,11 @@ class NautobotViewSetMixin(GenericViewSet, UIComponentsMixin, AccessMixin, GetRe
732
737
  except TemplateDoesNotExist:
733
738
  # Try a different detail view template format
734
739
  template_name = f"{app_label}/{model_opts.model_name}.html"
735
- select_template([template_name])
740
+ try:
741
+ select_template([template_name])
742
+ except TemplateDoesNotExist:
743
+ # Catch-all fallback to just object_retrieve.html
744
+ template_name = "generic/object_retrieve.html"
736
745
  return template_name
737
746
 
738
747
  def get_form(self, *args, **kwargs):
@@ -1500,7 +1509,9 @@ class ObjectChangeLogViewMixin(NautobotViewSetMixin):
1500
1509
 
1501
1510
  base_template: Optional[str] = None
1502
1511
 
1503
- @drf_action(detail=True)
1512
+ @drf_action(
1513
+ detail=True, custom_view_base_action="view", custom_view_additional_permissions=["extras.view_objectchange"]
1514
+ )
1504
1515
  def changelog(self, request, *args, **kwargs):
1505
1516
  model = self.get_queryset().model
1506
1517
  data = {
@@ -1521,7 +1532,7 @@ class ObjectNotesViewMixin(NautobotViewSetMixin):
1521
1532
 
1522
1533
  base_template: Optional[str] = None
1523
1534
 
1524
- @drf_action(detail=True)
1535
+ @drf_action(detail=True, custom_view_base_action="view", custom_view_additional_permissions=["extras.view_note"])
1525
1536
  def notes(self, request, *args, **kwargs):
1526
1537
  model = self.get_queryset().model
1527
1538
  data = {
@@ -1536,6 +1547,6 @@ class ObjectDataComplianceViewMixin(NautobotViewSetMixin):
1536
1547
  UI Mixin for a DataCompliance to show up for a given object.
1537
1548
  """
1538
1549
 
1539
- @drf_action(detail=True)
1550
+ @drf_action(detail=True, url_path="data-compliance")
1540
1551
  def data_compliance(self, request, *args, **kwargs):
1541
1552
  return Response({})
@@ -15,7 +15,7 @@ from nautobot.core.forms import (
15
15
  TableConfigForm,
16
16
  )
17
17
  from nautobot.core.forms.forms import DynamicFilterFormSet
18
- from nautobot.core.templatetags.helpers import bettertitle, validated_viewname
18
+ from nautobot.core.templatetags.helpers import validated_viewname
19
19
  from nautobot.core.utils.config import get_settings_or_config
20
20
  from nautobot.core.utils.permissions import get_permission_for_model
21
21
  from nautobot.core.utils.requests import (
@@ -227,7 +227,7 @@ class NautobotHTMLRenderer(renderers.BrowsableAPIRenderer):
227
227
  if view.filterset is not None:
228
228
  filterset_filters = view.filterset.filters
229
229
  else:
230
- filterset_filters = view.filterset_class.get_filters()
230
+ filterset_filters = view.filterset_class.base_filters
231
231
  display_filter_params = [
232
232
  check_filter_for_display(filterset_filters, field_name, values)
233
233
  for field_name, values in view.filter_params.items()
@@ -324,7 +324,6 @@ class NautobotHTMLRenderer(renderers.BrowsableAPIRenderer):
324
324
  "action_buttons": valid_actions,
325
325
  "list_url": list_url,
326
326
  "saved_views": saved_views,
327
- "title": bettertitle(model._meta.verbose_name_plural),
328
327
  }
329
328
  )
330
329
  elif view.action in ["create", "update"]:
@@ -13,7 +13,8 @@ def update_data_validation_engine_job_module_name(apps, schema_editor):
13
13
  """
14
14
  Job = apps.get_model("extras", "Job")
15
15
  dve_jobs = Job.objects.filter(module_name="nautobot_data_validation_engine.jobs")
16
- dve_jobs.update(module_name="nautobot.data_validation.jobs")
16
+ # Now that the DVE jobs are system jobs, they should be enabled by default
17
+ dve_jobs.update(module_name="nautobot.core.jobs", enabled=True)
17
18
 
18
19
 
19
20
  def update_data_validation_engine_git_repo_contents(apps, schema_editor):
@@ -170,7 +171,7 @@ def revert_data_validation_engine_job_module_name(apps, schema_editor):
170
171
  Revert the `module_name` for the Jobs to match the old location of the data validation engine.
171
172
  """
172
173
  Job = apps.get_model("extras", "Job")
173
- dve_jobs = Job.objects.filter(module_name="nautobot.data_validation.jobs")
174
+ dve_jobs = Job.objects.filter(module_name="nautobot.core.jobs")
174
175
  dve_jobs.update(module_name="nautobot_data_validation_engine.jobs")
175
176
 
176
177
 
@@ -30,6 +30,7 @@ from nautobot.dcim.choices import (
30
30
  ControllerCapabilitiesChoices,
31
31
  DeviceFaceChoices,
32
32
  DeviceRedundancyGroupFailoverStrategyChoices,
33
+ InterfaceDuplexChoices,
33
34
  InterfaceModeChoices,
34
35
  InterfaceRedundancyGroupProtocolChoices,
35
36
  InterfaceTypeChoices,
@@ -693,6 +694,8 @@ class InterfaceSerializer(
693
694
  mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
694
695
  mac_address = serializers.CharField(allow_blank=True, allow_null=True, required=False)
695
696
  ip_address_count = serializers.IntegerField(read_only=True, source="_ip_address_count")
697
+ speed = serializers.IntegerField(required=False, allow_null=True)
698
+ duplex = ChoiceField(choices=InterfaceDuplexChoices, allow_blank=True, required=False)
696
699
 
697
700
  class Meta:
698
701
  model = Interface
nautobot/dcim/choices.py CHANGED
@@ -1154,6 +1154,55 @@ class InterfaceModeChoices(ChoiceSet):
1154
1154
  )
1155
1155
 
1156
1156
 
1157
+ class InterfaceDuplexChoices(ChoiceSet):
1158
+ DUPLEX_AUTO = "auto"
1159
+ DUPLEX_FULL = "full"
1160
+ DUPLEX_HALF = "half"
1161
+
1162
+ CHOICES = (
1163
+ (DUPLEX_AUTO, "Auto"),
1164
+ (DUPLEX_FULL, "Full"),
1165
+ (DUPLEX_HALF, "Half"),
1166
+ )
1167
+
1168
+
1169
+ class InterfaceSpeedChoices(ChoiceSet):
1170
+ # Stored in Kbps (for compatibility with circuits and humanize_speed filter)
1171
+ SPEED_1M = 1_000
1172
+ SPEED_10M = 10_000
1173
+ SPEED_100M = 100_000
1174
+ SPEED_1G = 1_000_000
1175
+ SPEED_2_5G = 2_500_000
1176
+ SPEED_5G = 5_000_000
1177
+ SPEED_10G = 10_000_000
1178
+ SPEED_25G = 25_000_000
1179
+ SPEED_40G = 40_000_000
1180
+ SPEED_50G = 50_000_000
1181
+ SPEED_100G = 100_000_000
1182
+ SPEED_200G = 200_000_000
1183
+ SPEED_400G = 400_000_000
1184
+ SPEED_800G = 800_000_000
1185
+ SPEED_1_6T = 1_600_000_000
1186
+
1187
+ CHOICES = (
1188
+ (SPEED_1M, "1 Mbps"),
1189
+ (SPEED_10M, "10 Mbps"),
1190
+ (SPEED_100M, "100 Mbps"),
1191
+ (SPEED_1G, "1 Gbps"),
1192
+ (SPEED_2_5G, "2.5 Gbps"),
1193
+ (SPEED_5G, "5 Gbps"),
1194
+ (SPEED_10G, "10 Gbps"),
1195
+ (SPEED_25G, "25 Gbps"),
1196
+ (SPEED_40G, "40 Gbps"),
1197
+ (SPEED_50G, "50 Gbps"),
1198
+ (SPEED_100G, "100 Gbps"),
1199
+ (SPEED_200G, "200 Gbps"),
1200
+ (SPEED_400G, "400 Gbps"),
1201
+ (SPEED_800G, "800 Gbps"),
1202
+ (SPEED_1_6T, "1.6 Tbps"),
1203
+ )
1204
+
1205
+
1157
1206
  class InterfaceStatusChoices(ChoiceSet):
1158
1207
  STATUS_PLANNED = "planned"
1159
1208
  STATUS_ACTIVE = "active"
@@ -37,6 +37,13 @@ VIRTUAL_IFACE_TYPES = interface_type_by_category["Virtual interfaces"]
37
37
 
38
38
  NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
39
39
 
40
+ COPPER_TWISTED_PAIR_IFACE_TYPES = [
41
+ InterfaceTypeChoices.TYPE_100ME_FIXED,
42
+ InterfaceTypeChoices.TYPE_1GE_FIXED,
43
+ InterfaceTypeChoices.TYPE_2GE_FIXED,
44
+ InterfaceTypeChoices.TYPE_5GE_FIXED,
45
+ InterfaceTypeChoices.TYPE_10GE_FIXED,
46
+ ]
40
47
 
41
48
  #
42
49
  # PowerFeeds
nautobot/dcim/factory.py CHANGED
@@ -186,7 +186,7 @@ class DeviceFactory(PrimaryModelFactory):
186
186
  )
187
187
  device_redundancy_group_priority = factory.Maybe(
188
188
  "has_device_redundancy_group",
189
- factory.Faker("pyint", min_value=1, max_value=500),
189
+ factory.Faker("pyint", min_value=1, max_value=65535),
190
190
  )
191
191
 
192
192
  controller_managed_device_group = random_instance(ControllerManagedDeviceGroup)