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
@@ -18,7 +18,9 @@ from nautobot.dcim.choices import (
18
18
  ConsolePortTypeChoices,
19
19
  DeviceFaceChoices,
20
20
  DeviceUniquenessChoices,
21
+ InterfaceDuplexChoices,
21
22
  InterfaceModeChoices,
23
+ InterfaceSpeedChoices,
22
24
  InterfaceTypeChoices,
23
25
  PortTypeChoices,
24
26
  PowerFeedBreakerPoleChoices,
@@ -727,6 +729,101 @@ class InterfaceTemplateTestCase(ModularDeviceComponentTemplateTestCaseMixin, Tes
727
729
  first_status = Status.objects.get_for_model(Interface).first()
728
730
  self.assertIsNotNone(device_2.interfaces.get(name="Test_Template_1").status, first_status)
729
731
 
732
+ def test_speed_disallowed_for_lag_virtual_wireless(self):
733
+ """speed must be None for LAG, virtual, and wireless templates."""
734
+ manufacturer = Manufacturer.objects.first()
735
+ device_type = DeviceType.objects.create(manufacturer=manufacturer, model="SpeedGuard 1000")
736
+
737
+ for if_type in (
738
+ InterfaceTypeChoices.TYPE_LAG,
739
+ InterfaceTypeChoices.TYPE_VIRTUAL,
740
+ InterfaceTypeChoices.TYPE_80211N,
741
+ ):
742
+ with self.subTest(if_type=if_type):
743
+ with self.assertRaises(ValidationError) as cm:
744
+ InterfaceTemplate(
745
+ device_type=device_type,
746
+ name=f"bad-{if_type}",
747
+ type=if_type,
748
+ speed=InterfaceSpeedChoices.SPEED_1G,
749
+ ).full_clean()
750
+ self.assertIn("Speed is not applicable to this interface type.", str(cm.exception))
751
+
752
+ def test_duplex_disallowed_for_lag_virtual_wireless(self):
753
+ """duplex must be blank for LAG, virtual, and wireless templates."""
754
+ manufacturer = Manufacturer.objects.first()
755
+ device_type = DeviceType.objects.create(manufacturer=manufacturer, model="DuplexGuard 1000")
756
+
757
+ for itype in (
758
+ InterfaceTypeChoices.TYPE_LAG,
759
+ InterfaceTypeChoices.TYPE_VIRTUAL,
760
+ InterfaceTypeChoices.TYPE_80211N,
761
+ ):
762
+ with self.assertRaises(ValidationError):
763
+ InterfaceTemplate(
764
+ device_type=device_type,
765
+ name=f"bad-{itype}",
766
+ type=itype,
767
+ duplex=InterfaceDuplexChoices.DUPLEX_FULL,
768
+ ).full_clean()
769
+
770
+ def test_duplex_disallowed_for_non_base_t(self):
771
+ """duplex must be blank for non-BASE-T physical types (e.g., SFP)."""
772
+ manufacturer = Manufacturer.objects.first()
773
+ device_type = DeviceType.objects.create(manufacturer=manufacturer, model="SfpGuard 1000")
774
+
775
+ with self.assertRaises(ValidationError) as cm:
776
+ InterfaceTemplate(
777
+ device_type=device_type,
778
+ name="sfp0",
779
+ type=InterfaceTypeChoices.TYPE_1GE_SFP,
780
+ duplex=InterfaceDuplexChoices.DUPLEX_FULL,
781
+ ).full_clean()
782
+ self.assertIn("Duplex is only applicable to copper twisted-pair interfaces.", str(cm.exception))
783
+
784
+ def test_duplex_and_speed_allowed_for_base_t(self):
785
+ """BASE-T physical types accept duplex and speed values."""
786
+ manufacturer = Manufacturer.objects.first()
787
+ device_type = DeviceType.objects.create(manufacturer=manufacturer, model="CopperOK 1000")
788
+
789
+ tmpl = InterfaceTemplate(
790
+ device_type=device_type,
791
+ name="eth0",
792
+ type=InterfaceTypeChoices.TYPE_1GE_FIXED,
793
+ speed=InterfaceSpeedChoices.SPEED_1G,
794
+ duplex=InterfaceDuplexChoices.DUPLEX_FULL,
795
+ )
796
+ tmpl.full_clean() # should not raise
797
+
798
+ def test_instantiation_propagates_speed_and_duplex(self):
799
+ """Interface created from template inherits speed and duplex."""
800
+ statuses = Status.objects.get_for_model(Device)
801
+ location = Location.objects.filter(location_type=LocationType.objects.get(name="Campus")).first()
802
+ manufacturer = Manufacturer.objects.first()
803
+ device_role = Role.objects.get_for_model(Device).first()
804
+ device_type = DeviceType.objects.create(manufacturer=manufacturer, model="Propagate 2000")
805
+
806
+ InterfaceTemplate.objects.create(
807
+ device_type=device_type,
808
+ name="EthX",
809
+ type=InterfaceTypeChoices.TYPE_1GE_FIXED,
810
+ mgmt_only=False,
811
+ speed=InterfaceSpeedChoices.SPEED_1G,
812
+ duplex=InterfaceDuplexChoices.DUPLEX_FULL,
813
+ )
814
+
815
+ device = Device.objects.create(
816
+ device_type=device_type,
817
+ role=device_role,
818
+ status=statuses[0],
819
+ name="Device-Prop",
820
+ location=location,
821
+ )
822
+
823
+ iface = device.interfaces.get(name="EthX")
824
+ self.assertEqual(iface.speed, InterfaceSpeedChoices.SPEED_1G)
825
+ self.assertEqual(iface.duplex, InterfaceDuplexChoices.DUPLEX_FULL)
826
+
730
827
 
731
828
  class InterfaceRedundancyGroupTestCase(ModelTestCases.BaseModelTestCase):
732
829
  model = InterfaceRedundancyGroup
@@ -2779,6 +2876,7 @@ class InterfaceTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.Base
2779
2876
  name="VLAN 1", vid=100, location=location, status=vlan_status, vlan_group=vlan_group
2780
2877
  )
2781
2878
  status = Status.objects.get_for_model(Device).first()
2879
+ cls.intf_status = Status.objects.get_for_model(Interface).first()
2782
2880
  cls.device = Device.objects.create(
2783
2881
  name="Device 1",
2784
2882
  device_type=devicetype,
@@ -3008,6 +3106,173 @@ class InterfaceTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.Base
3008
3106
  self.device.refresh_from_db()
3009
3107
  self.assertEqual(self.device.primary_ip6, None)
3010
3108
 
3109
+ def _assert_invalid_speed_duplex(self, if_type, speed=None, duplex="", expected_error=""):
3110
+ iface = Interface(
3111
+ device=self.device,
3112
+ name=f"test-{if_type}",
3113
+ type=if_type,
3114
+ status=self.intf_status,
3115
+ speed=speed,
3116
+ duplex=duplex,
3117
+ )
3118
+ with self.assertRaises(ValidationError) as cm:
3119
+ iface.full_clean()
3120
+ self.assertIn(expected_error, str(cm.exception))
3121
+
3122
+ def test_disallowed_speed_and_duplex_matrix(self):
3123
+ """Test that interface types with no speed or duplex disallow those settings."""
3124
+ test_cases = [
3125
+ # LAG
3126
+ (
3127
+ InterfaceTypeChoices.TYPE_LAG,
3128
+ InterfaceSpeedChoices.SPEED_1M,
3129
+ None,
3130
+ "Speed is not applicable to this interface type.",
3131
+ ),
3132
+ (
3133
+ InterfaceTypeChoices.TYPE_LAG,
3134
+ None,
3135
+ InterfaceDuplexChoices.DUPLEX_FULL,
3136
+ "Duplex is not applicable to this interface type.",
3137
+ ),
3138
+ # Virtual
3139
+ (
3140
+ InterfaceTypeChoices.TYPE_VIRTUAL,
3141
+ InterfaceSpeedChoices.SPEED_1M,
3142
+ None,
3143
+ "Speed is not applicable to this interface type.",
3144
+ ),
3145
+ (
3146
+ InterfaceTypeChoices.TYPE_VIRTUAL,
3147
+ None,
3148
+ InterfaceDuplexChoices.DUPLEX_FULL,
3149
+ "Duplex is not applicable to this interface type.",
3150
+ ),
3151
+ # Wireless
3152
+ (
3153
+ InterfaceTypeChoices.TYPE_80211AC,
3154
+ InterfaceSpeedChoices.SPEED_1M,
3155
+ None,
3156
+ "Speed is not applicable to this interface type.",
3157
+ ),
3158
+ (
3159
+ InterfaceTypeChoices.TYPE_80211AC,
3160
+ None,
3161
+ InterfaceDuplexChoices.DUPLEX_FULL,
3162
+ "Duplex is not applicable to this interface type.",
3163
+ ),
3164
+ # Copper (negative speed is invalid)
3165
+ (InterfaceTypeChoices.TYPE_1GE_FIXED, -100, None, "Ensure this value is greater than or equal to 0."),
3166
+ # Copper (speed as a string is invalid)
3167
+ (InterfaceTypeChoices.TYPE_1GE_FIXED, "100 Mbps", None, "value must be an integer."),
3168
+ # Copper (invalid duplex is invalid)
3169
+ (
3170
+ InterfaceTypeChoices.TYPE_1GE_FIXED,
3171
+ InterfaceSpeedChoices.SPEED_1M,
3172
+ "invalid",
3173
+ "Value 'invalid' is not a valid choice.",
3174
+ ),
3175
+ # Optical (no duplex allowed)
3176
+ (
3177
+ InterfaceTypeChoices.TYPE_10GE_SFP_PLUS,
3178
+ InterfaceSpeedChoices.SPEED_1M,
3179
+ InterfaceDuplexChoices.DUPLEX_FULL,
3180
+ "Duplex is only applicable to copper twisted-pair interfaces.",
3181
+ ),
3182
+ ]
3183
+ for if_type, speed, duplex, expected_error in test_cases:
3184
+ with self.subTest(f"{if_type} with speed={speed} and duplex={duplex}"):
3185
+ self._assert_invalid_speed_duplex(if_type, speed, duplex, expected_error)
3186
+
3187
+ def test_copper_allows_duplex_and_non_negative_speed(self):
3188
+ """Test that copper interfaces allow duplex and non-negative speed."""
3189
+ iface = Interface(
3190
+ device=self.device,
3191
+ name="eth1",
3192
+ type=InterfaceTypeChoices.TYPE_1GE_FIXED, # 1000BASE-T
3193
+ status=self.intf_status,
3194
+ speed=InterfaceSpeedChoices.SPEED_1G,
3195
+ duplex=InterfaceDuplexChoices.DUPLEX_FULL,
3196
+ )
3197
+ # Should not raise
3198
+ iface.full_clean()
3199
+
3200
+ iface.speed = 0
3201
+ iface.full_clean()
3202
+
3203
+ def test_lag_allows_no_speed_or_duplex(self):
3204
+ """Test that LAG interfaces pass validation when speed and duplex are not set."""
3205
+ iface = Interface(
3206
+ device=self.device,
3207
+ name="Port-Channel1",
3208
+ type=InterfaceTypeChoices.TYPE_LAG,
3209
+ status=self.intf_status,
3210
+ )
3211
+ # Should not raise when speed and duplex are not set
3212
+ iface.full_clean()
3213
+
3214
+ def test_optical_disallows_duplex_allows_speed(self):
3215
+ """Test that optical interfaces do not allow duplex and allow positive speed."""
3216
+ # Duplex set should error
3217
+ iface_bad = Interface(
3218
+ device=self.device,
3219
+ name="xe0",
3220
+ type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS,
3221
+ status=self.intf_status,
3222
+ duplex=InterfaceDuplexChoices.DUPLEX_FULL,
3223
+ )
3224
+ with self.assertRaises(ValidationError) as cm:
3225
+ iface_bad.full_clean()
3226
+ self.assertIn("Duplex is only applicable to copper twisted-pair interfaces.", str(cm.exception))
3227
+
3228
+ # Speed positive should pass
3229
+ iface_ok = Interface(
3230
+ device=self.device,
3231
+ name="xe1",
3232
+ type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS,
3233
+ status=self.intf_status,
3234
+ speed=InterfaceSpeedChoices.SPEED_10G,
3235
+ )
3236
+ iface_ok.full_clean()
3237
+
3238
+ def test_changing_copper_interface_with_speed_and_duplex_to_optical_fails(self):
3239
+ """Test that changing a copper interface with speed and duplex to an optical interface fails."""
3240
+
3241
+ with self.subTest("speed"):
3242
+ iface = Interface(
3243
+ device=self.device,
3244
+ name="eth3",
3245
+ type=InterfaceTypeChoices.TYPE_1GE_FIXED,
3246
+ status=self.intf_status,
3247
+ speed=InterfaceSpeedChoices.SPEED_1G,
3248
+ )
3249
+ iface.full_clean()
3250
+
3251
+ iface.type = InterfaceTypeChoices.TYPE_LAG
3252
+ with self.assertRaises(ValidationError) as cm:
3253
+ iface.full_clean()
3254
+ self.assertIn("Speed is not applicable to this interface type.", str(cm.exception))
3255
+
3256
+ with self.subTest("duplex"):
3257
+ iface = Interface(
3258
+ device=self.device,
3259
+ name="eth3",
3260
+ type=InterfaceTypeChoices.TYPE_1GE_FIXED,
3261
+ status=self.intf_status,
3262
+ duplex=InterfaceDuplexChoices.DUPLEX_FULL,
3263
+ )
3264
+ iface.full_clean()
3265
+
3266
+ iface.type = InterfaceTypeChoices.TYPE_10GE_SFP_PLUS
3267
+ with self.assertRaises(ValidationError) as cm:
3268
+ iface.full_clean()
3269
+ self.assertIn("Duplex is only applicable to copper twisted-pair interfaces.", str(cm.exception))
3270
+
3271
+ iface.type = InterfaceTypeChoices.TYPE_10GE_SFP_PLUS
3272
+ with self.assertRaises(ValidationError) as cm:
3273
+ iface.full_clean()
3274
+ self.assertIn("Duplex is only applicable to copper twisted-pair interfaces.", str(cm.exception))
3275
+
3011
3276
 
3012
3277
  class SoftwareImageFileTestCase(ModelTestCases.BaseModelTestCase):
3013
3278
  model = SoftwareImageFile
@@ -0,0 +1,160 @@
1
+ from django.test import TestCase
2
+
3
+ from nautobot.dcim.choices import InterfaceDuplexChoices, InterfaceSpeedChoices, InterfaceTypeChoices
4
+ from nautobot.dcim.models import Device, DeviceType, Interface, InterfaceTemplate, Location, LocationType, Manufacturer
5
+ from nautobot.dcim.tables.devices import DeviceModuleInterfaceTable, InterfaceTable
6
+ from nautobot.dcim.tables.devicetypes import InterfaceTemplateTable
7
+ from nautobot.extras.models import Role, Status
8
+
9
+
10
+ class InterfaceTableRenderMixin:
11
+ """Mixin for testing render_speed methods on interface tables."""
12
+
13
+ table_class = None
14
+
15
+ @classmethod
16
+ def setUpTestData(cls):
17
+ manufacturer = Manufacturer.objects.create(name="Test Manufacturer")
18
+ device_type = DeviceType.objects.create(manufacturer=manufacturer, model="Test Device Type")
19
+ device_role = Role.objects.get_for_model(Device).first()
20
+ location_type = LocationType.objects.get(name="Campus")
21
+ location = Location.objects.filter(location_type=location_type).first()
22
+ device_status = Status.objects.get_for_model(Device).first()
23
+ cls.interface_status = Status.objects.get_for_model(Interface).first()
24
+
25
+ cls.device = Device.objects.create(
26
+ name="Test Device",
27
+ device_type=device_type,
28
+ role=device_role,
29
+ location=location,
30
+ status=device_status,
31
+ )
32
+
33
+ def test_render_speed_duplex_with_value(self):
34
+ """Test that the table renders humanized speed values."""
35
+ interface = Interface.objects.create(
36
+ device=self.device,
37
+ name="eth0",
38
+ type=InterfaceTypeChoices.TYPE_1GE_FIXED,
39
+ status=self.interface_status,
40
+ speed=InterfaceSpeedChoices.SPEED_1G,
41
+ duplex=InterfaceDuplexChoices.DUPLEX_FULL,
42
+ )
43
+
44
+ queryset = Interface.objects.filter(pk=interface.pk)
45
+ table = self.table_class(queryset) # pylint: disable=not-callable
46
+ bound_row = table.rows[0]
47
+ rendered_speed = bound_row.get_cell("speed")
48
+ rendered_duplex = bound_row.get_cell("duplex")
49
+
50
+ self.assertEqual(rendered_speed, "1 Gbps")
51
+ self.assertEqual(rendered_duplex, "Full")
52
+
53
+ def test_render_speed_duplex_with_none(self):
54
+ """Test that the table handles None speed value and renders an emdash."""
55
+ emdash = "\u2014"
56
+ interface = Interface.objects.create(
57
+ device=self.device,
58
+ name="eth1",
59
+ type=InterfaceTypeChoices.TYPE_1GE_FIXED,
60
+ status=self.interface_status,
61
+ speed=None,
62
+ )
63
+
64
+ queryset = Interface.objects.filter(pk=interface.pk)
65
+ table = self.table_class(queryset) # pylint: disable=not-callable
66
+ bound_row = table.rows[0]
67
+ rendered_speed = bound_row.get_cell("speed")
68
+ rendered_duplex = bound_row.get_cell("duplex")
69
+
70
+ self.assertEqual(rendered_speed, emdash)
71
+ self.assertEqual(rendered_duplex, emdash)
72
+
73
+ def test_render_speed_various(self):
74
+ """Test that the table correctly humanizes various speed values."""
75
+ # Test all speed choices defined in InterfaceSpeedChoices
76
+ for speed_value, expected_output in InterfaceSpeedChoices.CHOICES:
77
+ with self.subTest(speed_value=speed_value, expected=expected_output):
78
+ interface = Interface.objects.create(
79
+ device=self.device,
80
+ name=f"eth-{speed_value}",
81
+ type=InterfaceTypeChoices.TYPE_1GE_FIXED,
82
+ status=self.interface_status,
83
+ speed=speed_value,
84
+ )
85
+
86
+ queryset = Interface.objects.filter(pk=interface.pk)
87
+ table = self.table_class(queryset) # pylint: disable=not-callable
88
+ bound_row = table.rows[0]
89
+ rendered_speed = bound_row.get_cell("speed")
90
+
91
+ self.assertEqual(rendered_speed, expected_output)
92
+
93
+ def test_render_duplex_various(self):
94
+ """Test that the table correctly renders various duplex values."""
95
+ for duplex_value, expected_output in InterfaceDuplexChoices.CHOICES:
96
+ with self.subTest(duplex_value=duplex_value, expected=expected_output):
97
+ interface = Interface.objects.create(
98
+ device=self.device,
99
+ name=f"eth-{duplex_value}",
100
+ type=InterfaceTypeChoices.TYPE_1GE_FIXED,
101
+ status=self.interface_status,
102
+ duplex=duplex_value,
103
+ )
104
+
105
+ queryset = Interface.objects.filter(pk=interface.pk)
106
+ table = self.table_class(queryset) # pylint: disable=not-callable
107
+ bound_row = table.rows[0]
108
+ rendered_duplex = bound_row.get_cell("duplex")
109
+
110
+ self.assertEqual(rendered_duplex, expected_output)
111
+
112
+
113
+ class InterfaceTableTestCase(InterfaceTableRenderMixin, TestCase):
114
+ """Test cases for InterfaceTable."""
115
+
116
+ table_class = InterfaceTable
117
+
118
+
119
+ class DeviceModuleInterfaceTableTestCase(InterfaceTableRenderMixin, TestCase):
120
+ """Test cases for DeviceModuleInterfaceTable."""
121
+
122
+ table_class = DeviceModuleInterfaceTable
123
+
124
+
125
+ class InterfaceTemplateTableTestCase(TestCase):
126
+ """Render tests for InterfaceTemplateTable speed/duplex columns."""
127
+
128
+ @classmethod
129
+ def setUpTestData(cls):
130
+ manufacturer = Manufacturer.objects.create(name="Test Manuf Tmpl")
131
+ cls.device_type = DeviceType.objects.create(manufacturer=manufacturer, model="DT-Tmpl")
132
+
133
+ def test_render_speed_duplex_with_value(self):
134
+ interface_template = InterfaceTemplate.objects.create(
135
+ device_type=self.device_type,
136
+ name="tmpl-eth0",
137
+ type=InterfaceTypeChoices.TYPE_1GE_FIXED,
138
+ speed=InterfaceSpeedChoices.SPEED_1G,
139
+ duplex=InterfaceDuplexChoices.DUPLEX_FULL,
140
+ )
141
+ table = InterfaceTemplateTable(InterfaceTemplate.objects.filter(pk=interface_template.pk))
142
+ bound_row = table.rows[0]
143
+ rendered_speed = bound_row.get_cell("speed") # pylint: disable=no-member
144
+ rendered_duplex = bound_row.get_cell("duplex") # pylint: disable=no-member
145
+ self.assertEqual(rendered_speed, "1 Gbps")
146
+ self.assertEqual(rendered_duplex, "Full")
147
+
148
+ def test_render_speed_duplex_with_none(self):
149
+ emdash = "\u2014"
150
+ interface_template = InterfaceTemplate.objects.create(
151
+ device_type=self.device_type,
152
+ name="tmpl-eth1",
153
+ type=InterfaceTypeChoices.TYPE_1GE_FIXED,
154
+ )
155
+ table = InterfaceTemplateTable(InterfaceTemplate.objects.filter(pk=interface_template.pk))
156
+ bound_row = table.rows[0]
157
+ rendered_speed = bound_row.get_cell("speed") # pylint: disable=no-member
158
+ rendered_duplex = bound_row.get_cell("duplex") # pylint: disable=no-member
159
+ self.assertEqual(rendered_speed, emdash)
160
+ self.assertEqual(rendered_duplex, emdash)
@@ -6,7 +6,7 @@ import zoneinfo
6
6
  from django.contrib.auth import get_user_model
7
7
  from django.contrib.contenttypes.models import ContentType
8
8
  from django.db.models import Q
9
- from django.test import override_settings, tag
9
+ from django.test import override_settings
10
10
  from django.urls import reverse
11
11
  from django.utils.html import strip_spaces_between_tags
12
12
  from netaddr import EUI
@@ -30,6 +30,7 @@ from nautobot.dcim.choices import (
30
30
  ConsolePortTypeChoices,
31
31
  DeviceFaceChoices,
32
32
  DeviceRedundancyGroupFailoverStrategyChoices,
33
+ InterfaceDuplexChoices,
33
34
  InterfaceModeChoices,
34
35
  InterfaceRedundancyGroupProtocolChoices,
35
36
  InterfaceTypeChoices,
@@ -1293,7 +1294,6 @@ class ModuleTypeTestCase(
1293
1294
  "comments": "changed comment",
1294
1295
  }
1295
1296
 
1296
- @tag("fix_in_v3")
1297
1297
  def test_list_has_correct_links(self):
1298
1298
  """Assert that the ModuleType list view has import/export buttons for both CSV and YAML/JSON formats."""
1299
1299
  self.add_permissions("dcim.add_moduletype", "dcim.view_moduletype")
@@ -1305,11 +1305,11 @@ class ModuleTypeTestCase(
1305
1305
  csv_import_url = job_import_url(ContentType.objects.get_for_model(ModuleType))
1306
1306
  # Dropdown provides both YAML/JSON and CSV import as options
1307
1307
  self.assertInHTML(
1308
- f'<a href="{yaml_import_url}"><span class="mdi mdi-database-import text-secondary" aria-hidden="true"></span> Import from JSON/YAML (single record)</a>',
1308
+ f'<a class="dropdown-item" href="{yaml_import_url}"><span class="mdi mdi-database-import text-secondary" aria-hidden="true"></span> Import from JSON/YAML (single record)</a>',
1309
1309
  content,
1310
1310
  )
1311
1311
  self.assertInHTML(
1312
- f'<a href="{csv_import_url}"><span class="mdi mdi-database-import text-secondary" aria-hidden="true"></span> Import from CSV (multiple records)</a>',
1312
+ f'<a class="dropdown-item" href="{csv_import_url}"><span class="mdi mdi-database-import text-secondary" aria-hidden="true"></span> Import from CSV (multiple records)</a>',
1313
1313
  content,
1314
1314
  )
1315
1315
 
@@ -1322,11 +1322,11 @@ class ModuleTypeTestCase(
1322
1322
  )
1323
1323
  self.assertInHTML('<input type="hidden" name="export_format" value="yaml">', content)
1324
1324
  self.assertInHTML(
1325
- '<button type="submit"><span class="mdi mdi-database-export text-secondary" aria-hidden="true"></span> Export as YAML</button>',
1325
+ '<button class="dropdown-item" type="submit"><span class="mdi mdi-database-export text-secondary" aria-hidden="true"></span> Export as YAML</button>',
1326
1326
  content,
1327
1327
  )
1328
1328
  self.assertInHTML(
1329
- '<button type="submit"><span class="mdi mdi-database-export text-secondary" aria-hidden="true"></span> Export as CSV</button>',
1329
+ '<button class="dropdown-item" type="submit"><span class="mdi mdi-database-export text-secondary" aria-hidden="true"></span> Export as CSV</button>',
1330
1330
  content,
1331
1331
  )
1332
1332
 
@@ -1838,6 +1838,68 @@ class InterfaceTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
1838
1838
  "description": "new test description",
1839
1839
  }
1840
1840
 
1841
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1842
+ def test_create_base_t_with_speed_and_duplex(self):
1843
+ self.add_permissions("dcim.add_interfacetemplate", "dcim.view_devicetype")
1844
+ url = reverse("dcim:interfacetemplate_add")
1845
+ dt = DeviceType.objects.first()
1846
+ data = {
1847
+ "device_type": dt.pk,
1848
+ "name_pattern": "Eth-View-1",
1849
+ "type": InterfaceTypeChoices.TYPE_1GE_FIXED,
1850
+ "mgmt_only": False,
1851
+ "speed": 1_000_000,
1852
+ "duplex": InterfaceDuplexChoices.DUPLEX_FULL,
1853
+ "_create": True,
1854
+ }
1855
+ response = self.client.post(url, data)
1856
+
1857
+ # Successful create redirects (JobResult or object list)
1858
+ self.assertIn(response.status_code, (302, 303))
1859
+ interface_template = InterfaceTemplate.objects.get(name="Eth-View-1")
1860
+ self.assertEqual(interface_template.speed, 1_000_000)
1861
+ self.assertEqual(interface_template.duplex, InterfaceDuplexChoices.DUPLEX_FULL)
1862
+
1863
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1864
+ def test_create_sfp_with_duplex_rejected(self):
1865
+ self.add_permissions("dcim.add_interfacetemplate", "dcim.view_devicetype")
1866
+ url = reverse("dcim:interfacetemplate_add")
1867
+ dt = DeviceType.objects.first()
1868
+ data = {
1869
+ "device_type": dt.pk,
1870
+ "name_pattern": "SFP-View-1",
1871
+ "type": InterfaceTypeChoices.TYPE_1GE_SFP,
1872
+ "duplex": InterfaceDuplexChoices.DUPLEX_FULL,
1873
+ "_create": True,
1874
+ }
1875
+ response = self.client.post(url, data)
1876
+ # Form error returns 200 with field error displayed
1877
+ self.assertEqual(response.status_code, 200)
1878
+ content = response.content.decode(response.charset)
1879
+ self.assertIn("Duplex is only applicable to copper twisted-pair interfaces.", content)
1880
+
1881
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1882
+ def test_bulk_create_with_speed_and_duplex(self):
1883
+ self.add_permissions("dcim.add_interfacetemplate", "dcim.view_devicetype")
1884
+ url = reverse("dcim:interfacetemplate_add")
1885
+ dt = DeviceType.objects.first()
1886
+ data = {
1887
+ "device_type": dt.pk,
1888
+ "name_pattern": "Et[1-2]",
1889
+ "type": InterfaceTypeChoices.TYPE_1GE_FIXED,
1890
+ "mgmt_only": False,
1891
+ "speed": 1_000_000,
1892
+ "duplex": InterfaceDuplexChoices.DUPLEX_FULL,
1893
+ "_apply": True,
1894
+ }
1895
+ response = self.client.post(url, data)
1896
+ self.assertIn(response.status_code, (302, 303))
1897
+ objs = InterfaceTemplate.objects.filter(name__in=["Et1", "Et2"]).order_by("name")
1898
+ self.assertEqual(objs.count(), 2)
1899
+ for obj in objs:
1900
+ self.assertEqual(obj.speed, 1_000_000)
1901
+ self.assertEqual(obj.duplex, InterfaceDuplexChoices.DUPLEX_FULL)
1902
+
1841
1903
 
1842
1904
  class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
1843
1905
  model = FrontPortTemplate
@@ -4822,7 +4884,7 @@ class ControllerTestCase(ViewTestCases.PrimaryObjectViewTestCase):
4822
4884
  model = Controller
4823
4885
  filterset = ControllerFilterSet
4824
4886
  custom_action_required_permissions = {
4825
- "dcim:controller_wirelessnetworks": [
4887
+ "dcim:controller_wireless_networks": [
4826
4888
  "dcim.view_controller",
4827
4889
  "wireless.view_controllermanageddevicegroupwirelessnetworkassignment",
4828
4890
  ],