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
@@ -35,7 +35,7 @@ class BreadcrumbItemsTestCase(TestCase):
35
35
  item = ViewNameBreadcrumbItem(view_name="home", label="Home")
36
36
  context = Context({})
37
37
 
38
- url, label = item.as_pair(context)
38
+ url, label = next(item.as_pair(context))
39
39
 
40
40
  self.assertEqual(url, "/")
41
41
  self.assertEqual(label, "Home")
@@ -50,7 +50,7 @@ class BreadcrumbItemsTestCase(TestCase):
50
50
  )
51
51
  context = Context({})
52
52
 
53
- url, label = item.as_pair(context)
53
+ url, label = next(item.as_pair(context))
54
54
 
55
55
  self.assertEqual(url, f"/dcim/location-types/{self.location_type.pk}/?name=test")
56
56
  self.assertEqual(label, "Filtered Locations Types")
@@ -65,7 +65,7 @@ class BreadcrumbItemsTestCase(TestCase):
65
65
  )
66
66
  context = Context({"object": self.location_type})
67
67
 
68
- url, label = item.as_pair(context)
68
+ url, label = next(item.as_pair(context))
69
69
 
70
70
  self.assertEqual(
71
71
  url, f"/dcim/location-types/{self.location_type.pk}/?{urlencode({'name': self.location_type.name})}"
@@ -79,7 +79,7 @@ class BreadcrumbItemsTestCase(TestCase):
79
79
  label=lambda context: f"Hi, {context['user']}!",
80
80
  )
81
81
  context = Context({"user": "Frodo"})
82
- url, label = item.as_pair(context)
82
+ url, label = next(item.as_pair(context))
83
83
  self.assertEqual(url, "/")
84
84
  self.assertEqual(label, "Hi, Frodo!")
85
85
 
@@ -148,7 +148,7 @@ class BreadcrumbItemsTestCase(TestCase):
148
148
  item = ModelBreadcrumbItem(**test_case["kwargs"])
149
149
  context = Context({"object": self.location_type, "model_type": Device, "device_name": "abc"})
150
150
 
151
- url, label = item.as_pair(context)
151
+ url, label = next(item.as_pair(context))
152
152
 
153
153
  self.assertEqual(url, test_case["expected_url"])
154
154
  self.assertEqual(label, test_case["expected_label"])
@@ -157,7 +157,7 @@ class BreadcrumbItemsTestCase(TestCase):
157
157
  item = ModelBreadcrumbItem(model_key="object", label="custom LaBeL")
158
158
  context = Context({"object": self.location_type})
159
159
 
160
- _, label = item.as_pair(context)
160
+ _, label = next(item.as_pair(context))
161
161
  self.assertEqual(label, "custom LaBeL")
162
162
 
163
163
  def test_model_item_from_context(self):
@@ -165,7 +165,7 @@ class BreadcrumbItemsTestCase(TestCase):
165
165
  item = ModelBreadcrumbItem(model_key="object")
166
166
  context = Context({"object": self.location_type})
167
167
 
168
- url, label = item.as_pair(context)
168
+ url, label = next(item.as_pair(context))
169
169
 
170
170
  self.assertEqual(url, "/dcim/location-types/")
171
171
  self.assertEqual(label, "Location Types")
@@ -175,7 +175,7 @@ class BreadcrumbItemsTestCase(TestCase):
175
175
  item = InstanceBreadcrumbItem(instance_key="object")
176
176
  context = Context({"object": self.location_type})
177
177
 
178
- url, label = item.as_pair(context)
178
+ url, label = next(item.as_pair(context))
179
179
 
180
180
  self.assertEqual(url, f"/dcim/location-types/{self.location_type.pk}/")
181
181
  self.assertEqual(label, str(self.location_type))
@@ -192,7 +192,7 @@ class BreadcrumbItemsTestCase(TestCase):
192
192
 
193
193
  for item in items:
194
194
  with self.subTest():
195
- _, label = item.as_pair(context)
195
+ _, label = next(item.as_pair(context))
196
196
  self.assertEqual(label, "Custom Label")
197
197
 
198
198
  def test_no_reverse_match(self):
@@ -200,7 +200,7 @@ class BreadcrumbItemsTestCase(TestCase):
200
200
  item = ViewNameBreadcrumbItem(view_name="nonexistent")
201
201
  context = Context({})
202
202
 
203
- url, label = item.as_pair(context)
203
+ url, label = next(item.as_pair(context))
204
204
 
205
205
  self.assertEqual(url, "")
206
206
  self.assertEqual(label, "")
@@ -210,12 +210,12 @@ class BreadcrumbItemsTestCase(TestCase):
210
210
  context = Context({})
211
211
  item = InstanceBreadcrumbItem(instance_key="missing_key")
212
212
 
213
- url, label = item.as_pair(context)
213
+ url, label = next(item.as_pair(context))
214
214
  self.assertEqual(url, "")
215
215
  self.assertEqual(label, "")
216
216
 
217
217
  item = ModelBreadcrumbItem(model_key="missing_key")
218
- url, label = item.as_pair(context)
218
+ url, label = next(item.as_pair(context))
219
219
  self.assertEqual(url, "")
220
220
  self.assertEqual(label, "")
221
221
 
@@ -223,7 +223,7 @@ class BreadcrumbItemsTestCase(TestCase):
223
223
  context = Context({"object": LocationType.objects.create(name="custom name")})
224
224
  item = InstanceBreadcrumbItem()
225
225
 
226
- _, label = item.as_pair(context)
226
+ _, label = next(item.as_pair(context))
227
227
  self.assertEqual(label, "custom name")
228
228
 
229
229
  def test_instance_item_is_working_with_directly_passed_instance(self):
@@ -231,7 +231,7 @@ class BreadcrumbItemsTestCase(TestCase):
231
231
  instance = LocationType.objects.create(name="cUsToM CoUnTrY")
232
232
  item = InstanceBreadcrumbItem(instance=instance)
233
233
 
234
- url, label = item.as_pair(context)
234
+ url, label = next(item.as_pair(context))
235
235
  self.assertEqual(url, f"/dcim/location-types/{instance.pk}/")
236
236
  self.assertEqual(label, "cUsToM CoUnTrY")
237
237
 
@@ -244,17 +244,17 @@ class BreadcrumbItemsTestCase(TestCase):
244
244
  context = Context({"object": location, "status": status})
245
245
 
246
246
  item = InstanceBreadcrumbItem(instance=context_object_attr("location_type"))
247
- url, label = item.as_pair(context)
247
+ url, label = next(item.as_pair(context))
248
248
  self.assertEqual(url, f"/dcim/location-types/{location_type.pk}/")
249
- self.assertEqual(label, "Country → State")
249
+ self.assertEqual(label, "State")
250
250
 
251
251
  item = BaseBreadcrumbItem(label=context_object_attr("name", context_key="status"))
252
- url, label = item.as_pair(context)
252
+ url, label = next(item.as_pair(context))
253
253
  self.assertEqual(url, "")
254
254
  self.assertEqual(label, status.name)
255
255
 
256
256
  item = InstanceBreadcrumbItem(instance=context_object_attr("location_type.parent"))
257
- url, label = item.as_pair(context)
257
+ url, label = next(item.as_pair(context))
258
258
  self.assertEqual(url, f"/dcim/location-types/{parent_location_type.pk}/")
259
259
  self.assertEqual(label, "Country")
260
260
 
@@ -263,7 +263,7 @@ class BreadcrumbItemsTestCase(TestCase):
263
263
  context = Context({"object": LocationType.objects.create(name="custom name", parent=parent_location_type)})
264
264
  item = InstanceParentBreadcrumbItem()
265
265
 
266
- _, label = item.as_pair(context)
266
+ _, label = next(item.as_pair(context))
267
267
  self.assertEqual(label, "cUsToM CoUnTrY")
268
268
 
269
269
  def test_instance_parent_is_create_proper_url(self):
@@ -273,12 +273,12 @@ class BreadcrumbItemsTestCase(TestCase):
273
273
  context = Context({"object": location_type})
274
274
 
275
275
  item = InstanceParentBreadcrumbItem()
276
- url, label = item.as_pair(context)
276
+ url, label = next(item.as_pair(context))
277
277
  self.assertEqual(url, f"/dcim/location-types/?parent={parent_location_type.pk}")
278
278
  self.assertEqual(label, "Country")
279
279
 
280
280
  item = InstanceParentBreadcrumbItem(parent_query_param="location", parent_lookup_key="name")
281
- url, label = item.as_pair(context)
281
+ url, label = next(item.as_pair(context))
282
282
  self.assertEqual(url, "/dcim/location-types/?location=Country")
283
283
  self.assertEqual(label, "Country")
284
284
 
@@ -16,7 +16,7 @@ from nautobot.core.jobs import ExportObjectList
16
16
  from nautobot.core.jobs.cleanup import CleanupTypes
17
17
  from nautobot.core.testing import create_job_result_and_run_job, TransactionTestCase
18
18
  from nautobot.core.testing.context import load_event_broker_override_settings
19
- from nautobot.dcim.models import Device, DeviceType, Location, LocationType, Manufacturer
19
+ from nautobot.dcim.models import Device, DeviceType, FrontPortTemplate, Location, LocationType, Manufacturer
20
20
  from nautobot.extras.choices import DynamicGroupTypeChoices, JobResultStatusChoices, LogLevelChoices
21
21
  from nautobot.extras.factory import JobResultFactory, ObjectChangeFactory
22
22
  from nautobot.extras.models import (
@@ -35,7 +35,7 @@ from nautobot.extras.models import (
35
35
  )
36
36
  from nautobot.extras.models.metadata import ObjectMetadata
37
37
  from nautobot.ipam.models import IPAddress, Namespace, Prefix
38
- from nautobot.users.models import ObjectPermission
38
+ from nautobot.users.models import ObjectPermission, User
39
39
 
40
40
 
41
41
  class ExportObjectListTest(TransactionTestCase):
@@ -1252,10 +1252,8 @@ class BulkDeleteTestCase(TransactionTestCase):
1252
1252
 
1253
1253
 
1254
1254
  class RefreshDynamicGroupCacheJobButtonReceiverTestCase(TransactionTestCase):
1255
- def setUp(self):
1256
- super().setUp()
1257
- self.job_module = "nautobot.core.jobs.groups"
1258
- self.job_name = "RefreshDynamicGroupCacheJobButtonReceiver"
1255
+ job_module = "nautobot.core.jobs.groups"
1256
+ job_name = "RefreshDynamicGroupCacheJobButtonReceiver"
1259
1257
 
1260
1258
  def test_successful_cache_refresh(self):
1261
1259
  LocationType.objects.create(name="DG Test LT 1")
@@ -1322,3 +1320,72 @@ class RefreshDynamicGroupCacheJobButtonReceiverTestCase(TransactionTestCase):
1322
1320
  log_fail.message,
1323
1321
  "The members of this Dynamic Group are statically defined and do not need to be recalculated.",
1324
1322
  )
1323
+
1324
+
1325
+ class ValidateModelDataTestCase(TransactionTestCase):
1326
+ job_module = "nautobot.core.jobs"
1327
+ job_name = "ValidateModelData"
1328
+
1329
+ def test_successful_validation(self):
1330
+ job_result = create_job_result_and_run_job(
1331
+ self.job_module,
1332
+ self.job_name,
1333
+ content_types=[ContentType.objects.get_for_model(Status).pk],
1334
+ verbose=True,
1335
+ )
1336
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_SUCCESS)
1337
+
1338
+ def test_failure_on_invalid_dg(self):
1339
+ dg = DynamicGroup(
1340
+ name="Legacy rear_port_template filter",
1341
+ filter={"rear_port_template": "74aac78c-fabb-468c-a036-26c46c56f27a"},
1342
+ content_type=ContentType.objects.get_for_model(FrontPortTemplate),
1343
+ )
1344
+ dg.save(update_cached_members=False)
1345
+ job_result = create_job_result_and_run_job(
1346
+ self.job_module,
1347
+ self.job_name,
1348
+ content_types=[ContentType.objects.get_for_model(DynamicGroup).pk],
1349
+ verbose=False,
1350
+ )
1351
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
1352
+ log_fail = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_FAILURE)
1353
+ self.assertIn("Enter a list of values", log_fail.message)
1354
+
1355
+ def test_warning_without_permission(self):
1356
+ job_result = create_job_result_and_run_job(
1357
+ self.job_module,
1358
+ self.job_name,
1359
+ username=self.user.username, # otherwise run_job_for_testing defaults to a superuser account
1360
+ content_types=[ContentType.objects.get_for_model(Status).pk],
1361
+ verbose=True,
1362
+ )
1363
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_SUCCESS)
1364
+ log_warn = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_WARNING)
1365
+ self.assertEqual("No statuses found", log_warn.message)
1366
+ self.assertIsNone(
1367
+ JobLogEntry.objects.filter(
1368
+ job_result=job_result, log_level=LogLevelChoices.LOG_SUCCESS, message="Validated successfully"
1369
+ ).first()
1370
+ )
1371
+
1372
+ def test_no_restrict_superuser(self):
1373
+ job_result = create_job_result_and_run_job(
1374
+ self.job_module,
1375
+ self.job_name,
1376
+ content_types=[ContentType.objects.get_for_model(User).pk],
1377
+ verbose=True,
1378
+ )
1379
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_SUCCESS)
1380
+
1381
+ def test_no_restrict_non_superuser(self):
1382
+ job_result = create_job_result_and_run_job(
1383
+ self.job_module,
1384
+ self.job_name,
1385
+ username=self.user.username, # otherwise run_job_for_testing defaults to a superuser account
1386
+ content_types=[ContentType.objects.get_for_model(User).pk],
1387
+ verbose=True,
1388
+ )
1389
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
1390
+ log_fail = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_FAILURE)
1391
+ self.assertIn("Unable to apply access permissions to users.user", log_fail.message)
@@ -0,0 +1,59 @@
1
+ from unittest.mock import patch
2
+
3
+ from django.test import override_settings, RequestFactory
4
+ from django.urls import reverse
5
+ from rest_framework.response import Response
6
+
7
+ from nautobot.cloud.views import CloudResourceTypeUIViewSet
8
+ from nautobot.core.testing import TestCase
9
+ from nautobot.core.ui.titles import Titles
10
+ from nautobot.core.views.renderers import NautobotHTMLRenderer
11
+
12
+
13
+ class ObjectListViewTitlesTest(TestCase):
14
+ """
15
+ Test suite for verifying that Titles are correctly set in ObjectListView rendering.
16
+ """
17
+
18
+ user_permissions = ["cloud.view_cloudresourcetype"]
19
+
20
+ def setUp(self):
21
+ super().setUp()
22
+ self.factory = RequestFactory()
23
+
24
+ @override_settings(ALLOWED_HOSTS=["*"], PAGINATE_COUNT=5, MAX_PAGE_SIZE=10)
25
+ def test_uiviewset_list_view_title(self):
26
+ """
27
+ Test that the list view title is correctly set from the Titles configuration.
28
+ """
29
+
30
+ path = reverse("cloud:cloudresourcetype_list")
31
+ request = self.factory.get(path)
32
+ request.user = self.user
33
+ viewset_class = CloudResourceTypeUIViewSet
34
+ with patch.object(CloudResourceTypeUIViewSet, "view_titles", Titles(titles={"list": "Burritos"})):
35
+ view = viewset_class()
36
+ view.action_map = {"get": "list"}
37
+
38
+ request = view.initialize_request(request)
39
+
40
+ view.setup(request)
41
+ view.initial(request)
42
+
43
+ renderer = NautobotHTMLRenderer()
44
+ context = renderer.get_context(
45
+ data={},
46
+ accepted_media_type="text/html",
47
+ renderer_context={"view": view, "request": request, "response": Response({})},
48
+ )
49
+
50
+ # Verify that the title is set in the context
51
+ self.assertIn("view_titles", context)
52
+ self.assertNotIn(
53
+ "title", context
54
+ ) # title is used within the render path but should not be directly in context
55
+ self.assertEqual(context["view_titles"], viewset_class.view_titles)
56
+
57
+ # Finally, render the view and verify the title appears in the response
58
+ response = self.client.get(path)
59
+ self.assertContains(response, "Burritos")
@@ -132,6 +132,7 @@ class SettingsJSONSchemaTestCase(TestCase):
132
132
  "CSRF_FAILURE_VIEW",
133
133
  "DATA_UPLOAD_MAX_NUMBER_FIELDS",
134
134
  "DEFAULT_AUTO_FIELD",
135
+ "DJANGO_TABLES2_TEMPLATE",
135
136
  "DRF_REACT_TEMPLATE_TYPE_MAP",
136
137
  "EXEMPT_EXCLUDE_MODELS",
137
138
  "FILTERS_NULL_CHOICE_LABEL",
@@ -63,6 +63,9 @@ class NautobotTemplatetagsHelperTest(TestCase):
63
63
  self.assertEqual(helpers.pre_tag(None), '<span class="text-secondary">&mdash;</span>')
64
64
  self.assertEqual(helpers.pre_tag([]), "<pre>[]</pre>")
65
65
  self.assertEqual(helpers.pre_tag("something"), "<pre>something</pre>")
66
+ self.assertEqual(helpers.pre_tag("", format_empty_value=False), '<span class="text-secondary">&mdash;</span>')
67
+ self.assertEqual(helpers.pre_tag([], format_empty_value=False), '<span class="text-secondary">&mdash;</span>')
68
+ self.assertEqual(helpers.pre_tag("something", format_empty_value=False), "<pre>something</pre>")
66
69
 
67
70
  def test_add_html_id(self):
68
71
  # Case where what we have isn't actually a HTML element but just a bare string
@@ -194,7 +197,13 @@ class NautobotTemplatetagsHelperTest(TestCase):
194
197
  def test_humanize_speed(self):
195
198
  self.assertEqual(helpers.humanize_speed(1544), "1.544 Mbps")
196
199
  self.assertEqual(helpers.humanize_speed(100000), "100 Mbps")
200
+ self.assertEqual(helpers.humanize_speed(2500000), "2.5 Gbps")
197
201
  self.assertEqual(helpers.humanize_speed(10000000), "10 Gbps")
202
+ self.assertEqual(helpers.humanize_speed(100000000), "100 Gbps")
203
+ self.assertEqual(helpers.humanize_speed(1000000000), "1 Tbps")
204
+ self.assertEqual(helpers.humanize_speed(1600000000), "1.6 Tbps")
205
+ self.assertEqual(helpers.humanize_speed(10000000000), "10 Tbps")
206
+ self.assertEqual(helpers.humanize_speed(100000000000), "100 Tbps")
198
207
 
199
208
  def test_tzoffset(self):
200
209
  self.assertTrue(callable(helpers.tzoffset))
@@ -107,22 +107,6 @@ class TitlesTestCase(TestCase):
107
107
  "context": {"view_action": "bulk_update", "objs_count": 10, "verbose_name_plural": "devices"},
108
108
  "expected": "Editing 10 Devices",
109
109
  },
110
- {
111
- "name": "changelog_action",
112
- "context": {
113
- "view_action": "changelog",
114
- "object": location_type,
115
- },
116
- "expected": "Test Location Type Title - Change Log",
117
- },
118
- {
119
- "name": "notes_action",
120
- "context": {
121
- "view_action": "notes",
122
- "object": location_type,
123
- },
124
- "expected": "Test Location Type Title - Notes",
125
- },
126
110
  {
127
111
  "name": "approve_action",
128
112
  "context": {"view_action": "approve", "verbose_name": "device"},
@@ -32,7 +32,9 @@ from nautobot.core.ui.object_detail import (
32
32
  SectionChoices,
33
33
  )
34
34
  from nautobot.dcim.models import Device, DeviceRedundancyGroup, Location
35
+ from nautobot.dcim.tables import DeviceModuleInterfaceTable
35
36
  from nautobot.dcim.tables.devices import DeviceTable
37
+ from nautobot.dcim.views import DeviceUIViewSet
36
38
  from nautobot.ipam.models import Prefix
37
39
 
38
40
 
@@ -355,9 +357,80 @@ class EChartsBaseTests(TestCase):
355
357
  self.assertEqual(config["series"][0]["name"], "S1")
356
358
  self.assertEqual(config["series"][1]["name"], "S2")
357
359
 
358
- def test_get_config_with_callable_data(self):
359
- chart = EChartsBase(data=lambda: self.data_normalized)
360
- config = chart.get_config()
360
+ def test_get_config_combined_charts_with_complex_data(self):
361
+ chart2 = EChartsBase(
362
+ chart_type=EChartsTypeChoices.LINE,
363
+ data={
364
+ "Compliant": {"aaa1": 5, "dns1": 12, "ntp1": 8},
365
+ "Non Compliant": {"aaa1": 10, "dns1": 20, "ntp1": 15},
366
+ },
367
+ )
368
+ chart1 = EChartsBase(
369
+ chart_type=EChartsTypeChoices.BAR,
370
+ data={
371
+ "Compliant": {"aaa": 5, "dns": 12, "ntp": 8},
372
+ "Non Compliant": {"aaa": 10, "dns": 20, "ntp": 15},
373
+ },
374
+ combined_with=chart2,
375
+ )
376
+ config = chart1.get_config()
377
+ self.assertEqual(len(config["series"]), 4)
378
+
379
+ self.assertEqual(config["series"][0], {"name": "Compliant", "type": "bar", "data": [5, 12, 8]})
380
+ self.assertEqual(config["series"][1], {"name": "Non Compliant", "type": "bar", "data": [10, 20, 15]})
381
+ self.assertEqual(
382
+ config["series"][2],
383
+ {"name": "Compliant", "type": "line", "data": [5, 12, 8], "smooth": False, "lineStyle": {}},
384
+ )
385
+ self.assertEqual(
386
+ config["series"][3],
387
+ {"name": "Non Compliant", "type": "line", "data": [10, 20, 15], "smooth": False, "lineStyle": {}},
388
+ )
389
+
390
+ def test_get_config_with_context_callable_and_combined_chart(self):
391
+ def main_data(ctx):
392
+ return {
393
+ "Compliant": {"aaa1": ctx.get("aaa1_count", 0), "dns1": 13, "ntp1": 8},
394
+ "Non Compliant": {"aaa1": 10, "dns1": 20, "ntp1": 15},
395
+ }
396
+
397
+ def combined_data(ctx):
398
+ return {
399
+ "Compliant": {"aaa": 5, "dns": ctx.get("dns_count", 0), "ntp1": 8},
400
+ "Non Compliant": {"aaa": 10, "dns": 20, "ntp1": 15},
401
+ }
402
+
403
+ ctx = Context({"aaa1_count": 5, "dns_count": 12})
404
+ combined_chart = EChartsBase(chart_type=EChartsTypeChoices.LINE, data=combined_data)
405
+ main_chart = EChartsBase(
406
+ chart_type=EChartsTypeChoices.BAR,
407
+ data=main_data,
408
+ combined_with=combined_chart,
409
+ )
410
+ config = main_chart.get_config(context=ctx)
411
+ self.assertEqual(len(config["series"]), 4)
412
+
413
+ self.assertEqual(config["series"][0]["data"][0], 5) # aaa1_count
414
+ self.assertEqual(config["series"][2]["data"][1], 12) # dns_count
415
+
416
+ def test_get_config_with_context_callable(self):
417
+ def dynamic_data(ctx):
418
+ return {"data": {"Devices": ctx.get("device_count", 0)}}
419
+
420
+ ctx = Context({"device_count": 42})
421
+ chart = EChartsBase(chart_type=EChartsTypeChoices.PIE, data=dynamic_data)
422
+ config = chart.get_config(context=ctx)
423
+ # check that after getting config data doesn't change it's still dynami_data function
424
+ self.assertEqual(chart.data, dynamic_data)
425
+ self.assertEqual(config["series"][0]["data"][0]["value"], 42)
426
+ ctx = Context({"device_count": 45})
427
+ config = chart.get_config(context=ctx)
428
+ self.assertEqual(config["series"][0]["data"][0]["value"], 45)
429
+
430
+ def test_get_config_with_context_ignored_when_data_is_not_callable(self):
431
+ ctx = Context({"some": "value"})
432
+ chart = EChartsBase(data=self.data_normalized)
433
+ config = chart.get_config(context=ctx)
361
434
  self.assertEqual(config["series"][0]["data"], [1, 2])
362
435
 
363
436
 
@@ -492,6 +565,52 @@ class ObjectDetailContentExtraTabsTest(TestCase):
492
565
  self.default_tabs_id.append("services")
493
566
  self.assertListEqual(tab_ids, self.default_tabs_id)
494
567
 
568
+ def test_tab_id_url_as_action(self):
569
+ """
570
+ Test that when you create a panel with a tab_id that matches a viewset action,
571
+ the return_url is constructed correctly.
572
+ """
573
+ self.add_permissions("dcim.add_interface", "dcim.change_interface")
574
+ device_info = Device.objects.first()
575
+
576
+ panel = DeviceUIViewSet.DeviceInterfacesTablePanel(
577
+ weight=100,
578
+ section=SectionChoices.FULL_WIDTH,
579
+ table_title="Interfaces",
580
+ table_class=DeviceModuleInterfaceTable,
581
+ table_attribute="vc_interfaces",
582
+ related_field_name="device",
583
+ tab_id="interfaces",
584
+ )
585
+ context = {"request": self.request, "object": device_info}
586
+ panel_context = panel.get_extra_context(context)
587
+
588
+ return_url = f"/dcim/devices/{device_info.pk}/interfaces/"
589
+ self.assertTrue(panel_context["body_content_table_add_url"].endswith(return_url))
590
+
591
+ def test_tab_id_url_as_param(self):
592
+ """
593
+ Test that when you create a panel with a tab_id that does NOT matches a viewset action,
594
+ the return_url is constructed correctly.
595
+ """
596
+ self.add_permissions("dcim.add_interface", "dcim.change_interface")
597
+ device_info = Device.objects.first()
598
+
599
+ panel = DeviceUIViewSet.DeviceInterfacesTablePanel(
600
+ weight=100,
601
+ section=SectionChoices.FULL_WIDTH,
602
+ table_title="Interfaces",
603
+ table_class=DeviceModuleInterfaceTable,
604
+ table_attribute="vc_interfaces",
605
+ related_field_name="device",
606
+ tab_id="interfaces-not-exist",
607
+ )
608
+ context = {"request": self.request, "object": device_info}
609
+ panel_context = panel.get_extra_context(context)
610
+
611
+ return_url = f"&return_url=/dcim/devices/{device_info.pk}/?tab=interfaces-not-exist"
612
+ self.assertTrue(panel_context["body_content_table_add_url"].endswith(return_url))
613
+
495
614
  def test_extra_tab_panel_context(self):
496
615
  """
497
616
  Confirming that extra tab panels produce the correct context,
@@ -22,7 +22,13 @@ from nautobot.core.utils.cache import construct_cache_key
22
22
  from nautobot.core.utils.migrations import update_object_change_ct_for_replaced_models
23
23
  from nautobot.core.utils.module_loading import check_name_safe_to_import_privately, import_string_optional
24
24
  from nautobot.data_validation import models as data_validation_models
25
- from nautobot.dcim import filters as dcim_filters, forms as dcim_forms, models as dcim_models, tables
25
+ from nautobot.dcim import (
26
+ filters as dcim_filters,
27
+ forms as dcim_forms,
28
+ models as dcim_models,
29
+ tables,
30
+ views as dcim_views,
31
+ )
26
32
  from nautobot.extras import models as extras_models, utils as extras_utils
27
33
  from nautobot.extras.choices import ObjectChangeActionChoices, RelationshipTypeChoices
28
34
  from nautobot.extras.filters import StatusFilterSet
@@ -329,6 +335,26 @@ class GetFooForModelTest(TestCase):
329
335
  instance = extras_models.GraphQLQuery.objects.create(name="FizzBuzz", query="{devices { name }}")
330
336
  self.assertIsNone(lookup.get_user_from_instance(instance))
331
337
 
338
+ def test_get_breadcrumbs_for_model(self):
339
+ breadcrumbs = lookup.get_breadcrumbs_for_model(dcim_models.Device)
340
+ self.assertEqual(breadcrumbs.items, dcim_views.DeviceUIViewSet.get_breadcrumbs(dcim_models.Device).items)
341
+ breadcrumbs = lookup.get_breadcrumbs_for_model(dcim_models.Device, view_type="")
342
+ self.assertEqual(
343
+ breadcrumbs.items, dcim_views.DeviceUIViewSet.get_breadcrumbs(dcim_models.Device, view_type="").items
344
+ )
345
+
346
+ def test_get_detail_view_components_context_for_model(self):
347
+ context = lookup.get_detail_view_components_context_for_model(dcim_models.Device)
348
+ self.assertEqual(
349
+ context["breadcrumbs"].items, lookup.get_breadcrumbs_for_model(dcim_models.Device, view_type="").items
350
+ )
351
+ self.assertEqual(
352
+ context["object_detail_content"], lookup.get_object_detail_content_for_model(dcim_models.Device)
353
+ )
354
+ self.assertEqual(
355
+ context["view_titles"].titles, lookup.get_view_titles_for_model(dcim_models.Device, view_type="").titles
356
+ )
357
+
332
358
  def test_get_filterset_for_model(self):
333
359
  """
334
360
  Test that `get_filterset_for_model` returns the right FilterSet for various inputs.
@@ -351,6 +377,12 @@ class GetFooForModelTest(TestCase):
351
377
  self.assertEqual(lookup.get_form_for_model("dcim.location"), dcim_forms.LocationForm)
352
378
  self.assertEqual(lookup.get_form_for_model(dcim_models.Location), dcim_forms.LocationForm)
353
379
 
380
+ def test_get_object_detail_content_for_model(self):
381
+ self.assertEqual(
382
+ lookup.get_object_detail_content_for_model(dcim_models.Device),
383
+ dcim_views.DeviceUIViewSet.object_detail_content,
384
+ )
385
+
354
386
  def test_get_related_field_for_models(self):
355
387
  """
356
388
  Test that `get_related_field_for_models` returns the appropriate field for various inputs.
@@ -463,6 +495,14 @@ class GetFooForModelTest(TestCase):
463
495
  # Testing unconventional table name
464
496
  self.assertEqual(lookup.get_table_class_string_from_view_name("ipam:prefix_list"), "PrefixDetailTable")
465
497
 
498
+ def test_get_view_titles_for_model(self):
499
+ view_titles = lookup.get_view_titles_for_model(dcim_models.Device)
500
+ self.assertEqual(view_titles.titles, dcim_views.DeviceUIViewSet.get_view_titles(dcim_models.Device).titles)
501
+ view_titles = lookup.get_view_titles_for_model(dcim_models.Device, view_type="")
502
+ self.assertEqual(
503
+ view_titles.titles, dcim_views.DeviceUIViewSet.get_view_titles(dcim_models.Device, view_type="").titles
504
+ )
505
+
466
506
 
467
507
  class IsTaggableTest(TestCase):
468
508
  def test_is_taggable_true(self):