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,6 +18,7 @@
18
18
  {% render_field form.locations %}
19
19
  {% render_field form.roles %}
20
20
  {% render_field form.device_types %}
21
+ {% render_field form.device_families %}
21
22
  {% render_field form.platforms %}
22
23
  {% render_field form.cluster_groups %}
23
24
  {% render_field form.clusters %}
@@ -43,7 +43,7 @@
43
43
  <div class="tab-content">
44
44
  <div class="tab-pane active" id="filter-form">
45
45
  {% if filter_form %}
46
- <span class="form-text py-6">
46
+ <span class="form-text mb-12">
47
47
  Select the filtering criteria to determine membership of objects matching
48
48
  the Content Type for this Dynamic Group. Fields that are not a dropdown are
49
49
  expected to have string inputs and do not support multiple values.
@@ -2,8 +2,6 @@
2
2
  {% load helpers %}
3
3
  {% load static %}
4
4
 
5
- {% block title %}{{ object }} - Change Log{% endblock %}
6
-
7
5
  {% block content %}
8
6
  <div class="row">
9
7
  <div class="col-lg-5">
@@ -70,7 +70,7 @@
70
70
  {% if app_data.availability %}
71
71
  <tr>
72
72
  <td>License</td>
73
- <td><label class="badge bg-transparent">{{ app_data.availability }}</label></td>
73
+ <td><label class="badge border border-primary text-primary">{{ app_data.availability }}</label></td>
74
74
  </tr>
75
75
  {% endif %}
76
76
  </table>
@@ -82,11 +82,11 @@
82
82
  <table class="table table-hover card-body attr-table">
83
83
  <tr>
84
84
  <td>Min Nautobot Version</td>
85
- <td>v{{ object.min_version | placeholder }}</td>
85
+ <td>{% if object.min_version %}v{{ object.min_version }}{% else %}{{ None|placeholder }}{% endif %}</td>
86
86
  </tr>
87
87
  <tr>
88
88
  <td>Max Nautobot Version</td>
89
- <td>v{{ object.max_version | placeholder }}</td>
89
+ <td>{% if object.max_version %}v{{ object.max_version }}{% else %}{{ None|placeholder }}{% endif %}</td>
90
90
  </tr>
91
91
  </table>
92
92
  </div>
@@ -13,7 +13,7 @@
13
13
  <div class="card">
14
14
  <div class="card-header"><strong>Parameters</strong></div>
15
15
  <div class="card-body">
16
- <span class="form-text">
16
+ <span class="form-text mb-12">
17
17
  All parameter values can be entered as Jinja2 templates if desired.
18
18
  The requesting object (device, Git repository, etc.), if any, will be provided
19
19
  as context variable <code>obj</code>.
@@ -1,15 +1,14 @@
1
1
  from django.contrib.contenttypes.models import ContentType
2
- from django.test import tag
3
2
  from django.urls import reverse
4
3
 
5
- from nautobot.core.testing.integration import SeleniumTestCase
4
+ from nautobot.core.testing.integration import ObjectDetailsMixin, ObjectsListMixin, SeleniumTestCase
6
5
  from nautobot.dcim.models import Device
7
6
  from nautobot.extras.models import ComputedField
8
7
 
9
8
  from . import create_test_device
10
9
 
11
10
 
12
- class ComputedFieldsTestCase(SeleniumTestCase):
11
+ class ComputedFieldsTestCase(SeleniumTestCase, ObjectDetailsMixin, ObjectsListMixin):
13
12
  """
14
13
  Integration test to check nautobot.extras.models.ComputedField.advanced_ui functionality
15
14
  and computed fields appearing in an object list
@@ -26,7 +25,6 @@ class ComputedFieldsTestCase(SeleniumTestCase):
26
25
  template="{{ obj.name }} is awesome!",
27
26
  )
28
27
 
29
- @tag("fix_in_v3")
30
28
  def test_computed_field_advanced_ui(self):
31
29
  """
32
30
  This test creates a device and a computed field for that device.
@@ -41,7 +39,7 @@ class ComputedFieldsTestCase(SeleniumTestCase):
41
39
  self.assertTrue(self.browser.is_text_present("Device Computed Field"))
42
40
  self.assertTrue(self.browser.is_text_present(f"{self.device.name} is awesome!"))
43
41
  # # Check the computed field does NOT appear in the advanced tab
44
- self.browser.links.find_by_partial_text("Advanced")[0].click()
42
+ self.switch_tab("Advanced")
45
43
  self.assertFalse(self.browser.is_text_present("Device Computed Field"))
46
44
  self.assertFalse(self.browser.is_text_present(f"{self.device.name} is awesome!"))
47
45
  # Set the custom_field to only show in the advanced tab
@@ -53,18 +51,20 @@ class ComputedFieldsTestCase(SeleniumTestCase):
53
51
  self.assertFalse(self.browser.is_text_present("Device Computed Field"))
54
52
  self.assertFalse(self.browser.is_text_present(f"{self.device.name} is awesome!"))
55
53
  # Check the computed field appears in the advanced tab
56
- self.browser.links.find_by_partial_text("Advanced")[0].click()
54
+ self.switch_tab("Advanced")
57
55
  self.assertTrue(self.browser.is_text_present("Device Computed Field"))
58
56
  self.assertTrue(self.browser.is_text_present(f"{self.device.name} is awesome!"))
59
57
 
60
- @tag("fix_in_v3")
61
58
  def test_computed_field_appears_in_object_list(self):
62
59
  """
63
60
  This test sets the computed field to be visible on the object list
64
61
  and then checks to see if it actually appears there.
65
62
  """
66
63
  self.browser.visit(f"{self.live_server_url}/dcim/devices/")
67
- self.click_button("#table-configure-button")
64
+ self.click_button("""button[title="Configure table"]""")
65
+ nb_drawer_open_xpath = "//section[contains(@class, 'nb-drawer') and contains(@class, 'nb-drawer-open')]"
66
+ self.assertTrue(self.browser.is_element_present_by_xpath(nb_drawer_open_xpath, wait_time=5))
67
+ self.assertTrue(self.browser.find_by_xpath(nb_drawer_open_xpath).is_visible(wait_time=5))
68
68
  select_option = self.browser.find_by_xpath(
69
69
  ".//select[@id='id_columns']/option[contains(text(), 'Device Computed Field')]"
70
70
  )
@@ -72,6 +72,5 @@ class ComputedFieldsTestCase(SeleniumTestCase):
72
72
  select_option.click()
73
73
  self.browser.find_by_xpath(".//input[@value='Save']").click()
74
74
  self.assertTrue(self.browser.is_text_present(f"{self.device.name} is awesome!"))
75
- self.browser.find_by_xpath(".//button[@data-original-title='Configure table']").click()
76
75
  self.browser.find_by_xpath(".//input[@value='Reset']").click()
77
76
  self.assertFalse(self.browser.is_text_present(f"{self.device.name} is awesome!"))
@@ -304,9 +304,7 @@ class CustomFieldTestCase(SeleniumTestCase, ObjectDetailsMixin):
304
304
  # Visit the device edit page
305
305
  self.browser.visit(f"{self.live_server_url}{reverse('dcim:device_edit', kwargs={'pk': device.pk})}")
306
306
  # Get the first item selected on the custom field
307
- self.browser.execute_script(
308
- 'document.querySelector(\'label:has(+ * [name="cf_test_selection_field"])\').scrollIntoView({ behavior: "instant" });'
309
- )
307
+ self.scroll_element_into_view(css='label:has(+ * [name="cf_test_selection_field"])')
310
308
  self.browser.find_by_xpath(".//label[contains(text(), 'Device Selection Field')]").click()
311
309
  active_web_element = self.browser.driver.switch_to.active_element
312
310
  active_web_element.send_keys(Keys.ENTER)
@@ -1,16 +1,15 @@
1
1
  from django.contrib.contenttypes.models import ContentType
2
- from django.test import tag
3
2
  from django.urls import reverse
4
3
  from selenium.webdriver.common.keys import Keys
5
4
 
6
- from nautobot.core.testing.integration import SeleniumTestCase
5
+ from nautobot.core.testing.integration import ObjectsListMixin, SeleniumTestCase
7
6
  from nautobot.dcim.models import Device
8
7
  from nautobot.extras.models import DynamicGroup
9
8
 
10
9
  from . import create_test_device
11
10
 
12
11
 
13
- class DynamicGroupTestCase(SeleniumTestCase):
12
+ class DynamicGroupTestCase(SeleniumTestCase, ObjectsListMixin):
14
13
  """
15
14
  Integration test to check nautobot.extras.models.DynamicGroup add/edit functionality.
16
15
  """
@@ -19,7 +18,6 @@ class DynamicGroupTestCase(SeleniumTestCase):
19
18
  super().setUp()
20
19
  self.login_as_superuser()
21
20
 
22
- @tag("fix_in_v3")
23
21
  def test_create_and_update(self):
24
22
  """
25
23
  Test initial add and then update of a new DynamicGroup
@@ -32,7 +30,7 @@ class DynamicGroupTestCase(SeleniumTestCase):
32
30
  self.click_navbar_entry("Organization", "Dynamic Groups")
33
31
 
34
32
  # Click add button
35
- self.browser.find_by_id("add-button").click()
33
+ self.click_add_item()
36
34
 
37
35
  # Fill out the form.
38
36
  name = "devices-active"
@@ -40,19 +38,20 @@ class DynamicGroupTestCase(SeleniumTestCase):
40
38
  self.browser.select("content_type", ct_label)
41
39
 
42
40
  # Click that "Create" button
43
- self.browser.find_by_text("Create").click()
41
+ self.click_edit_form_create_button()
44
42
 
45
43
  # Verify form redirect and presence of content.
46
44
  self.assertTrue(self.browser.is_text_present(f"Created dynamic group {name}"))
47
45
  self.assertTrue(self.browser.is_text_present("Edit"))
48
46
 
49
47
  # Edit the newly created DynamicGroup (Click that "Edit" button)
50
- self.browser.find_by_id("edit-button").click()
48
+ self.click_button("#edit-button")
51
49
 
52
50
  # Find the "Status" dynamic multi-select and type into it. Xpath is used
53
51
  # to find the next "input" after the "status" select field.
54
52
  status_field = self.browser.find_by_name("filter-status").first
55
53
  status_input = status_field.find_by_xpath("./following::input[1]").first
54
+ self.scroll_element_into_view(element=status_input)
56
55
  status_input.click() # Force focus on the input field to bring it on-screen
57
56
 
58
57
  # Fill in "Status: Active".
@@ -61,7 +60,7 @@ class DynamicGroupTestCase(SeleniumTestCase):
61
60
  status_input.type(Keys.ENTER)
62
61
 
63
62
  # Click that "Update" button
64
- self.browser.find_by_text("Update").click()
63
+ self.browser.find_by_xpath("//button[normalize-space()='Update']").click()
65
64
 
66
65
  # Verify form redirect and presence of content.
67
66
  self.assertTrue(self.browser.is_text_present(f"Modified dynamic group {name}"))
@@ -1,5 +1,4 @@
1
1
  from django.contrib.contenttypes.models import ContentType
2
- from django.test import tag
3
2
  from django.urls import reverse
4
3
 
5
4
  from nautobot.core.testing.integration import ObjectDetailsMixin, SeleniumTestCase
@@ -19,7 +18,6 @@ class RelationshipsTestCase(SeleniumTestCase, ObjectDetailsMixin):
19
18
  super().setUp()
20
19
  self.login_as_superuser()
21
20
 
22
- @tag("fix_in_v3")
23
21
  def test_relationship_advanced_ui(self):
24
22
  """
25
23
  This test creates a device and a relationship for that device.
@@ -2252,6 +2252,69 @@ class JobTest(
2252
2252
  self.assertTrue(schedule.approval_required)
2253
2253
  self.assertEqual(schedule.kwargs["var4"], str(device_role.pk))
2254
2254
 
2255
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2256
+ @mock.patch("nautobot.extras.api.views.get_worker_count")
2257
+ @mock.patch("nautobot.extras.models.jobs.JobResult.enqueue_job")
2258
+ def test_run_job_filtered_approval(self, mock_enqueue_job, mock_get_worker_count):
2259
+ """
2260
+ Run a job with a defined approval workflow whose filter should or should not include it.
2261
+ """
2262
+ workflow = ApprovalWorkflowDefinition(
2263
+ name="Test Approval Workflow Definition 1",
2264
+ model_content_type=ContentType.objects.get_for_model(ScheduledJob),
2265
+ weight=0,
2266
+ model_constraints={"job_model__job_class_name": "APITestJob"},
2267
+ )
2268
+ workflow.validated_save()
2269
+
2270
+ # Do the stuff.
2271
+ mock_get_worker_count.return_value = 1
2272
+ self.add_permissions("extras.run_job")
2273
+ device_role = Role.objects.get_for_model(Device).first()
2274
+ job_data = {
2275
+ "var1": "FooBar",
2276
+ "var2": 123,
2277
+ "var3": False,
2278
+ "var4": device_role.pk,
2279
+ }
2280
+
2281
+ data = {
2282
+ "data": job_data,
2283
+ # schedule is omitted
2284
+ }
2285
+
2286
+ url = self.get_run_url()
2287
+ response = self.client.post(url, data, format="json", **self.header)
2288
+ self.assertHttpStatus(response, self.run_success_response_status)
2289
+
2290
+ # Assert that a JobResult for this job was NOT created.
2291
+ self.assertFalse(JobResult.objects.filter(name=self.job_model.name).exists())
2292
+
2293
+ # Assert that we have an immediate ScheduledJob and that it matches the job_model.
2294
+ schedule = ScheduledJob.objects.last()
2295
+ self.assertIsNotNone(schedule)
2296
+ self.assertEqual(schedule.interval, JobExecutionType.TYPE_FUTURE)
2297
+ self.assertTrue(schedule.approval_required)
2298
+ self.assertEqual(schedule.kwargs["var4"], str(device_role.pk))
2299
+ mock_enqueue_job.assert_not_called()
2300
+
2301
+ # Change the workflow definition so that it no longer applies to this job model
2302
+ workflow.model_constraints = {"job_model__job_class_name__istartswith": "SomeOtherJob"}
2303
+ workflow.validated_save()
2304
+
2305
+ mock_enqueue_job.return_value = None
2306
+ deserialized_data = self.job_class.deserialize_data(job_data)
2307
+ response = self.client.post(url, data, format="json", **self.header)
2308
+ self.assertHttpStatus(response, self.run_success_response_status)
2309
+ expected_enqueue_job_args = (self.job_model, self.user)
2310
+ expected_enqueue_job_kwargs = {
2311
+ "job_queue": self.job_model.default_job_queue,
2312
+ **self.job_class.serialize_data(deserialized_data),
2313
+ }
2314
+ mock_enqueue_job.assert_called_with(*expected_enqueue_job_args, **expected_enqueue_job_kwargs)
2315
+ # No new scheduled job should be created
2316
+ self.assertEqual(schedule, ScheduledJob.objects.last())
2317
+
2255
2318
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2256
2319
  @mock.patch("nautobot.extras.api.views.get_worker_count")
2257
2320
  @mock.patch("nautobot.extras.models.jobs.JobResult.enqueue_job")
@@ -542,13 +542,35 @@ class ObjectChangeModelTest(TestCase): # TODO: change to BaseModelTestCase once
542
542
  def setUpTestData(cls):
543
543
  cls.location_status = Status.objects.get_for_model(Location).first()
544
544
 
545
+ def test_m2m_fields_not_excluded(self):
546
+ """Ensure that m2m fields are included in object changes, even if exclude_m2m is the default in the REST API."""
547
+ with context_managers.web_request_context(self.user):
548
+ location_type = LocationType.objects.create(name="Test m2m locationtype")
549
+
550
+ with context_managers.web_request_context(self.user):
551
+ location_type.content_types.set(ContentType.objects.filter(app_label="dcim"))
552
+
553
+ object_changes = get_changes_for_model(location_type)
554
+ self.assertEqual(object_changes.count(), 2)
555
+
556
+ snapshots = object_changes.first().get_snapshots()
557
+ self.assertIsNotNone(snapshots["differences"]["removed"])
558
+ self.assertIsNotNone(snapshots["differences"]["added"])
559
+ self.assertIn("content_types", snapshots["differences"]["removed"])
560
+ self.assertIn("content_types", snapshots["differences"]["added"])
561
+ self.assertEqual(
562
+ len(snapshots["differences"]["added"]["content_types"]),
563
+ ContentType.objects.filter(app_label="dcim").count(),
564
+ )
565
+
545
566
  def test_opt_out(self):
546
567
  """Hidden static group associations can "opt out" of change logging."""
547
568
  dg = DynamicGroup.objects.exclude(group_type=DynamicGroupTypeChoices.TYPE_STATIC).first()
548
569
  # Force reassignment of all cached memberships:
549
570
  members = list(dg.members)
550
- dg._set_members([])
551
- dg._set_members(members)
571
+ with context_managers.web_request_context(self.user):
572
+ dg._set_members([])
573
+ dg._set_members(members)
552
574
 
553
575
  for sga in dg.static_group_associations(manager="all_objects").all():
554
576
  self.assertIsNone(get_changes_for_model(sga).first())
@@ -14,6 +14,7 @@ from nautobot.core.testing import FilterTestCases
14
14
  from nautobot.dcim.filters import DeviceFilterSet
15
15
  from nautobot.dcim.models import (
16
16
  Device,
17
+ DeviceFamily,
17
18
  DeviceType,
18
19
  Interface,
19
20
  Location,
@@ -605,6 +606,8 @@ class ConfigContextTestCase(FilterTestCases.FilterTestCase):
605
606
  ("cluster_group", "cluster_groups__id"),
606
607
  ("cluster_group", "cluster_groups__name"),
607
608
  ("cluster_group_id", "cluster_groups__id"),
609
+ ("device_family", "device_families__id"),
610
+ ("device_family", "device_families__name"),
608
611
  ("device_type", "device_types__id"),
609
612
  ("device_type", "device_types__model"),
610
613
  ("device_type_id", "device_types__id"),
@@ -631,10 +634,28 @@ class ConfigContextTestCase(FilterTestCases.FilterTestCase):
631
634
 
632
635
  manufacturer = Manufacturer.objects.first()
633
636
 
637
+ cls.device_families = (
638
+ DeviceFamily.objects.create(name="Device Family A"),
639
+ DeviceFamily.objects.create(name="Device Family B"),
640
+ DeviceFamily.objects.create(name="Device Family C"),
641
+ )
642
+
634
643
  device_types = (
635
- DeviceType.objects.create(model="Device Type 1", manufacturer=manufacturer),
636
- DeviceType.objects.create(model="Device Type 2", manufacturer=manufacturer),
637
- DeviceType.objects.create(model="Device Type 3", manufacturer=manufacturer),
644
+ DeviceType.objects.create(
645
+ model="Device Type 1",
646
+ manufacturer=manufacturer,
647
+ device_family=cls.device_families[0],
648
+ ),
649
+ DeviceType.objects.create(
650
+ model="Device Type 2",
651
+ manufacturer=manufacturer,
652
+ device_family=cls.device_families[1],
653
+ ),
654
+ DeviceType.objects.create(
655
+ model="Device Type 3",
656
+ manufacturer=manufacturer,
657
+ device_family=cls.device_families[2],
658
+ ),
638
659
  )
639
660
  cls.device_types = device_types
640
661
 
@@ -669,6 +690,7 @@ class ConfigContextTestCase(FilterTestCases.FilterTestCase):
669
690
  )
670
691
  c.locations.set([cls.locations[i]])
671
692
  c.roles.set([device_roles[i]])
693
+ c.device_families.set([cls.device_families[i]])
672
694
  c.device_types.set([device_types[i]])
673
695
  c.platforms.set([platforms[i]])
674
696
  c.cluster_groups.set([cluster_groups[i]])
@@ -2142,6 +2164,8 @@ class SecretTestCase(FilterTestCases.FilterTestCase):
2142
2164
  ("created",),
2143
2165
  ("last_updated",),
2144
2166
  ("name",),
2167
+ ("secrets_groups", "secrets_groups__id"),
2168
+ ("secrets_groups", "secrets_groups__name"),
2145
2169
  ("tags", "tags__id"),
2146
2170
  ("tags", "tags__name"),
2147
2171
  )
@@ -2170,6 +2194,15 @@ class SecretTestCase(FilterTestCases.FilterTestCase):
2170
2194
  secrets[0].tags.set(Tag.objects.get_for_model(Secret))
2171
2195
  secrets[1].tags.set(Tag.objects.get_for_model(Secret)[:3])
2172
2196
 
2197
+ secrets_groups = (
2198
+ SecretsGroup.objects.create(
2199
+ name="Secrets Group 1",
2200
+ ),
2201
+ SecretsGroup.objects.create(name="Secrets Group 2"),
2202
+ )
2203
+ secrets_groups[0].secrets.set([secrets[0]])
2204
+ secrets_groups[1].secrets.set([secrets[1]])
2205
+
2173
2206
  def test_provider(self):
2174
2207
  params = {"provider": ["environment-variable"]}
2175
2208
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -27,6 +27,7 @@ from nautobot.core.testing import TestCase
27
27
  from nautobot.core.testing.models import ModelTestCases
28
28
  from nautobot.dcim.models import (
29
29
  Device,
30
+ DeviceFamily,
30
31
  DeviceType,
31
32
  Location,
32
33
  LocationType,
@@ -677,8 +678,11 @@ class ConfigContextTest(ModelTestCases.BaseModelTestCase):
677
678
 
678
679
  @classmethod
679
680
  def setUpTestData(cls):
680
- manufacturer = Manufacturer.objects.first()
681
- cls.devicetype = DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 1")
681
+ cls.manufacturer = Manufacturer.objects.first()
682
+ cls.devicefamily = DeviceFamily.objects.create(name="Device Family 1")
683
+ cls.devicetype = DeviceType.objects.create(
684
+ manufacturer=cls.manufacturer, model="Device Type 1", device_family=cls.devicefamily
685
+ )
682
686
  cls.devicerole = Role.objects.get_for_model(Device).first()
683
687
  root_location_type = LocationType.objects.create(name="Root Location Type")
684
688
  parent_location_type = LocationType.objects.create(name="Parent Location Type", parent=root_location_type)
@@ -1217,6 +1221,38 @@ class ConfigContextTest(ModelTestCases.BaseModelTestCase):
1217
1221
  self.assertNotIn("cluster_group_2", context.keys())
1218
1222
  self.assertNotIn("cluster_group_12", context.keys())
1219
1223
 
1224
+ def test_device_family_context(self):
1225
+ """
1226
+ A config context assigned to the device's DeviceFamily is included in get_config_context().
1227
+ """
1228
+
1229
+ # Create a Family-level context
1230
+ cc_family = ConfigContext.objects.create(
1231
+ name="Device Family 1",
1232
+ weight=100,
1233
+ data={
1234
+ "device_family": "Device Family 1",
1235
+ },
1236
+ )
1237
+ cc_family.device_families.add(self.devicefamily)
1238
+ ctx1 = self.device.get_config_context()
1239
+
1240
+ # Create a second device for a negative test and verify that it does NOT receive the family context
1241
+ device_type2 = DeviceType.objects.create(manufacturer=self.manufacturer, model="Device Type2")
1242
+ device2 = Device.objects.create(
1243
+ name="Device 2",
1244
+ location=self.location,
1245
+ tenant=self.tenant,
1246
+ platform=self.platform,
1247
+ role=self.devicerole,
1248
+ status=self.device_status,
1249
+ device_type=device_type2,
1250
+ )
1251
+ ctx2 = device2.get_config_context()
1252
+
1253
+ self.assertEqual(ctx1.get("device_family"), "Device Family 1")
1254
+ self.assertEqual(ctx2.get("device_family"), None)
1255
+
1220
1256
 
1221
1257
  class ConfigContextSchemaTestCase(ModelTestCases.BaseModelTestCase):
1222
1258
  """
@@ -4,7 +4,7 @@ import uuid
4
4
  from django.core.cache import cache
5
5
 
6
6
  from nautobot.core.testing import TestCase
7
- from nautobot.dcim.models import Device, LocationType
7
+ from nautobot.dcim.models import Cable, Device, PowerPort
8
8
  from nautobot.extras.choices import JobQueueTypeChoices
9
9
  from nautobot.extras.models import JobQueue
10
10
  from nautobot.extras.registry import registry
@@ -23,11 +23,10 @@ class UtilsTestCase(TestCase):
23
23
  self.assertEqual(get_base_template("dcim/device/base.html", Device), "dcim/device/base.html")
24
24
 
25
25
  with self.subTest("<model>.html wins over <model>_retrieve.html"):
26
- # TODO: why do we even have both locationtype.html and locationtype_retrieve.html?
27
- self.assertEqual(get_base_template(None, LocationType), "dcim/locationtype.html")
26
+ self.assertEqual(get_base_template(None, PowerPort), "dcim/powerport.html")
28
27
 
29
28
  with self.subTest("<model>_retrieve.html is used if present"):
30
- self.assertEqual(get_base_template(None, JobQueue), "extras/jobqueue_retrieve.html")
29
+ self.assertEqual(get_base_template(None, Cable), "dcim/cable_retrieve.html")
31
30
 
32
31
  with self.subTest("generic/object_retrieve.html is used as a fallback"):
33
32
  self.assertEqual(get_base_template(None, Token), "generic/object_retrieve.html")