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
@@ -29,6 +29,7 @@ from nautobot.core.utils.permissions import get_permission_for_model
29
29
  from nautobot.dcim.models import (
30
30
  ConsolePort,
31
31
  Device,
32
+ DeviceFamily,
32
33
  DeviceType,
33
34
  Interface,
34
35
  Location,
@@ -96,7 +97,6 @@ from nautobot.extras.models import (
96
97
  from nautobot.extras.templatetags.job_buttons import NO_CONFIRM_BUTTON
97
98
  from nautobot.extras.tests.constants import BIG_GRAPHQL_DEVICE_QUERY
98
99
  from nautobot.extras.tests.test_jobs import get_job_class_and_model
99
- from nautobot.extras.tests.test_relationships import RequiredRelationshipTestMixin
100
100
  from nautobot.extras.utils import get_pending_approval_workflow_stages, RoleModelsQuery, TaggableClassesQuery
101
101
  from nautobot.ipam.models import IPAddress, Prefix, VLAN, VLANGroup, VRF
102
102
  from nautobot.tenancy.models import Tenant
@@ -730,6 +730,7 @@ class ConfigContextTestCase(
730
730
  "regions": [],
731
731
  "locations": [location.pk],
732
732
  "roles": [],
733
+ "device_families": [DeviceFamily.objects.first().pk],
733
734
  "device_types": [],
734
735
  "platforms": [],
735
736
  "tenant_groups": [],
@@ -764,6 +765,7 @@ class ConfigContextTestCase(
764
765
  "regions": [],
765
766
  "locations": [],
766
767
  "roles": [],
768
+ "device_families": [],
767
769
  "device_types": [],
768
770
  "platforms": [],
769
771
  "tenant_groups": [],
@@ -803,6 +805,7 @@ class ConfigContextTestCase(
803
805
  "regions": [],
804
806
  "locations": [],
805
807
  "roles": [],
808
+ "device_families": [],
806
809
  "device_types": [],
807
810
  "platforms": [],
808
811
  "tenant_groups": [],
@@ -2755,59 +2758,6 @@ class ScheduledJobTestCase(
2755
2758
  crontab="15 10 * * *",
2756
2759
  )
2757
2760
 
2758
- def test_only_enabled_is_listed(self):
2759
- self.add_permissions("extras.view_scheduledjob")
2760
-
2761
- # this should not appear, since it's not enabled
2762
- ScheduledJob.objects.create(
2763
- enabled=False,
2764
- name="test4",
2765
- task="pass_job.TestPassJob",
2766
- interval=JobExecutionType.TYPE_IMMEDIATELY,
2767
- user=self.user,
2768
- start_time=timezone.now(),
2769
- )
2770
-
2771
- response = self.client.get(self._get_url("list"))
2772
- self.assertHttpStatus(response, 200)
2773
- self.assertNotIn("test4", extract_page_body(response.content.decode(response.charset)))
2774
-
2775
- def test_approved_required_jobs_are_listed_only_when_approved(self):
2776
- self.add_permissions("extras.view_scheduledjob")
2777
-
2778
- # this should not appear, since it's not approved
2779
- ScheduledJob.objects.create(
2780
- enabled=True,
2781
- approval_required=True,
2782
- decision_date=None,
2783
- name="test4",
2784
- task="pass_job.TestPassJob",
2785
- interval=JobExecutionType.TYPE_IMMEDIATELY,
2786
- user=self.user,
2787
- start_time=timezone.now(),
2788
- )
2789
- ScheduledJob.objects.create(
2790
- enabled=True,
2791
- approval_required=False,
2792
- name="test5",
2793
- task="pass_job.TestPassJob",
2794
- interval=JobExecutionType.TYPE_IMMEDIATELY,
2795
- user=self.user,
2796
- start_time=timezone.now(),
2797
- )
2798
- response = self.client.get(self._get_url("list"))
2799
- self.assertHttpStatus(response, 200)
2800
- self.assertNotIn("test4", extract_page_body(response.content.decode(response.charset)))
2801
- self.assertIn("test5", extract_page_body(response.content.decode(response.charset)))
2802
-
2803
- scheduled_job = ScheduledJob.objects.get(name="test4")
2804
- scheduled_job.decision_date = timezone.now()
2805
- scheduled_job.save()
2806
-
2807
- response = self.client.get(self._get_url("list"))
2808
- self.assertHttpStatus(response, 200)
2809
- self.assertIn("test4", extract_page_body(response.content.decode(response.charset)))
2810
-
2811
2761
  def test_non_valid_crontab_syntax(self):
2812
2762
  self.add_permissions("extras.view_scheduledjob")
2813
2763
 
@@ -3291,7 +3241,6 @@ class JobTestCase(
3291
3241
 
3292
3242
  result = JobResult.objects.latest()
3293
3243
  self.assertRedirects(response, reverse("extras:jobresult", kwargs={"pk": result.pk}))
3294
- mock_begin_approval_workflow.assert_not_called()
3295
3244
 
3296
3245
  def test_rerun_job(self):
3297
3246
  self.add_permissions("extras.run_job")
@@ -3328,7 +3277,8 @@ class JobTestCase(
3328
3277
  )
3329
3278
  self.assertInHTML('<input type="hidden" name="_profile" value="True" id="id__profile">', content)
3330
3279
  self.assertInHTML(
3331
- '<input type="checkbox" name="_ignore_singleton_lock" id="id__ignore_singleton_lock" checked>', content
3280
+ '<input type="checkbox" name="_ignore_singleton_lock" id="id__ignore_singleton_lock" class="form-check-input" checked>',
3281
+ content,
3332
3282
  )
3333
3283
 
3334
3284
  @mock.patch("nautobot.extras.views.get_worker_count", return_value=1)
@@ -3511,11 +3461,13 @@ class JobTestCase(
3511
3461
  self.add_permissions("extras.run_job")
3512
3462
  self.add_permissions("extras.view_scheduledjob")
3513
3463
 
3514
- ApprovalWorkflowDefinition.objects.create(
3464
+ workflow = ApprovalWorkflowDefinition(
3515
3465
  name="Approval Definition",
3516
3466
  model_content_type=ContentType.objects.get_for_model(ScheduledJob),
3517
3467
  weight=0,
3468
+ model_constraints={"job_model__name": self.test_pass.name},
3518
3469
  )
3470
+ workflow.validated_save()
3519
3471
  data = {
3520
3472
  "_schedule_type": "future",
3521
3473
  "_schedule_name": "test",
@@ -3523,14 +3475,15 @@ class JobTestCase(
3523
3475
  }
3524
3476
 
3525
3477
  for i, run_url in enumerate(self.run_urls):
3526
- if "_schedule_name" in data:
3527
- data["_schedule_name"] = f"test {i}"
3528
- response = self.client.post(run_url, data)
3529
- scheduled_job = ScheduledJob.objects.last()
3530
- self.assertRedirects(
3531
- response,
3532
- reverse("extras:scheduledjob_approvalworkflow", args=[scheduled_job.pk]),
3533
- )
3478
+ with self.subTest(run_url=run_url):
3479
+ if "_schedule_name" in data:
3480
+ data["_schedule_name"] = f"test {i}"
3481
+ response = self.client.post(run_url, data)
3482
+ scheduled_job = ScheduledJob.objects.last()
3483
+ self.assertRedirects(
3484
+ response,
3485
+ reverse("extras:scheduledjob_approvalworkflow", args=[scheduled_job.pk]),
3486
+ )
3534
3487
 
3535
3488
  @mock.patch("nautobot.extras.views.get_worker_count", return_value=1)
3536
3489
  def test_run_scheduled_job_with_no_approval_workflow_defined(self, _):
@@ -4068,17 +4021,7 @@ class ObjectMetadataTestCase(
4068
4021
  self.assertNotIn(instance2.assigned_object.get_absolute_url(), content, msg=content)
4069
4022
 
4070
4023
 
4071
- class RelationshipTestCase(
4072
- ViewTestCases.CreateObjectViewTestCase,
4073
- ViewTestCases.DeleteObjectViewTestCase,
4074
- ViewTestCases.EditObjectViewTestCase,
4075
- ViewTestCases.BulkDeleteObjectsViewTestCase,
4076
- ViewTestCases.GetObjectViewTestCase,
4077
- ViewTestCases.GetObjectChangelogViewTestCase,
4078
- ViewTestCases.ListObjectsViewTestCase,
4079
- RequiredRelationshipTestMixin,
4080
- ViewTestCases.BulkEditObjectsViewTestCase,
4081
- ):
4024
+ class RelationshipTestCase(ViewTestCases.PrimaryObjectViewTestCase):
4082
4025
  model = Relationship
4083
4026
  slug_source = "label"
4084
4027
  slugify_function = staticmethod(slugify_dashes_to_underscores)
@@ -4592,7 +4535,6 @@ class RoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase, ViewTestCases
4592
4535
  "remove_content_types": [device_ct.pk],
4593
4536
  }
4594
4537
 
4595
- @tag("fix_in_v3")
4596
4538
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
4597
4539
  def test_view_with_content_types(self):
4598
4540
  """
@@ -4611,13 +4553,10 @@ class RoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase, ViewTestCases
4611
4553
  if content_type not in role_content_types:
4612
4554
  if result == "Contact Associations":
4613
4555
  # AssociationContact Table in the contact tab should be there.
4614
- self.assertInHTML(
4615
- f'<strong>{result}</strong><div class="float-end d-print-none">',
4616
- response_body,
4617
- )
4618
- # ContactAssociationTable related to this role instances should not be there.
4556
+ self.assertInHTML(f"<strong>{result}</strong>", response_body)
4557
+ # ContactAssociationTable related to this role instances in the main tab should not be there.
4619
4558
  self.assertNotIn(
4620
- f'<strong>{result}</strong>\n </div>\n \n\n<table class="table table-hover nb-table-headings">\n',
4559
+ f'<strong>{result}</strong>\n </div>\n \n\n\n\n <table class="table table-hover nb-table-headings">',
4621
4560
  response_body,
4622
4561
  )
4623
4562
  else:
nautobot/extras/urls.py CHANGED
@@ -5,7 +5,6 @@ from nautobot.core.views.routers import NautobotUIViewSetRouter
5
5
  from nautobot.extras import views
6
6
  from nautobot.extras.models import (
7
7
  Job,
8
- Relationship,
9
8
  ScheduledJob,
10
9
  )
11
10
 
@@ -135,19 +134,6 @@ urlpatterns = [
135
134
  name="scheduledjob_approvalworkflow",
136
135
  kwargs={"model": ScheduledJob},
137
136
  ),
138
- # Custom relationships
139
- path(
140
- "relationships/<uuid:pk>/changelog/",
141
- views.ObjectChangeLogView.as_view(),
142
- name="relationship_changelog",
143
- kwargs={"model": Relationship},
144
- ),
145
- path(
146
- "relationships/<uuid:pk>/notes/",
147
- views.ObjectNotesView.as_view(),
148
- name="relationship_notes",
149
- kwargs={"model": Relationship},
150
- ),
151
137
  # Secrets
152
138
  path(
153
139
  "secrets/provider/<str:provider_slug>/form/",
nautobot/extras/views.py CHANGED
@@ -1,3 +1,4 @@
1
+ from functools import partial
1
2
  import logging
2
3
  from typing import Optional
3
4
  from urllib.parse import parse_qs
@@ -60,6 +61,7 @@ from nautobot.core.views.mixins import (
60
61
  ObjectBulkDestroyViewMixin,
61
62
  ObjectBulkUpdateViewMixin,
62
63
  ObjectChangeLogViewMixin,
64
+ ObjectDataComplianceViewMixin,
63
65
  ObjectDestroyViewMixin,
64
66
  ObjectDetailViewMixin,
65
67
  ObjectEditViewMixin,
@@ -326,6 +328,7 @@ class ApprovalWorkflowUIViewSet(
326
328
  section=SectionChoices.RIGHT_HALF,
327
329
  exclude_columns=["approval_workflow"],
328
330
  add_button_route=None,
331
+ enable_related_link=False,
329
332
  ),
330
333
  ],
331
334
  )
@@ -412,6 +415,7 @@ class ApprovalWorkflowStageUIViewSet(
412
415
  section=SectionChoices.RIGHT_HALF,
413
416
  exclude_columns=["approval_workflow_stage"],
414
417
  table_title="Responses",
418
+ enable_related_link=False,
415
419
  ),
416
420
  ],
417
421
  )
@@ -757,6 +761,7 @@ class ConfigContextUIViewSet(NautobotUIViewSet):
757
761
  "locations",
758
762
  "roles",
759
763
  "device_types",
764
+ "device_families",
760
765
  "platforms",
761
766
  "cluster_groups",
762
767
  "clusters",
@@ -967,6 +972,7 @@ class ContactUIViewSet(NautobotUIViewSet):
967
972
  table_filter="contact",
968
973
  table_title="Contact For",
969
974
  add_button_route=None,
975
+ enable_related_link=False,
970
976
  ),
971
977
  ),
972
978
  )
@@ -1756,6 +1762,7 @@ class GraphQLQueryUIViewSet(
1756
1762
  ObjectDestroyViewMixin,
1757
1763
  ObjectBulkDestroyViewMixin,
1758
1764
  ObjectChangeLogViewMixin,
1765
+ ObjectDataComplianceViewMixin,
1759
1766
  ObjectNotesViewMixin,
1760
1767
  ):
1761
1768
  filterset_form_class = forms.GraphQLQueryFilterForm
@@ -2042,55 +2049,66 @@ class JobRunView(ObjectPermissionRequiredMixin, View):
2042
2049
  ignore_singleton_lock = job_form.cleaned_data.pop("_ignore_singleton_lock", False)
2043
2050
  schedule_type = schedule_form.cleaned_data["_schedule_type"]
2044
2051
 
2045
- scheduled_job = ScheduledJob.create_schedule(
2046
- job_model,
2047
- request.user,
2048
- name=schedule_form.cleaned_data.get("_schedule_name"),
2049
- start_time=schedule_form.cleaned_data.get("_schedule_start_time"),
2050
- interval=schedule_type,
2051
- crontab=schedule_form.cleaned_data.get("_recurrence_custom_time"),
2052
- job_queue=job_queue,
2053
- profile=profile,
2054
- ignore_singleton_lock=ignore_singleton_lock,
2055
- validated_save=False,
2056
- **job_class.serialize_data(job_form.cleaned_data),
2057
- )
2058
- scheduled_job_has_approval_workflow = scheduled_job.has_approval_workflow_definition()
2059
- is_scheduled = schedule_type in JobExecutionType.SCHEDULE_CHOICES
2060
- if job_model.has_sensitive_variables and scheduled_job_has_approval_workflow:
2061
- messages.error(
2062
- request,
2063
- "Unable to run or schedule job: "
2064
- "This job is flagged as possibly having sensitive variables but also has an applicable approval workflow definition."
2065
- "Modify or remove the approval workflow definition or modify the job to set `has_sensitive_variables` to False.",
2052
+ with transaction.atomic():
2053
+ scheduled_job = ScheduledJob.create_schedule(
2054
+ job_model,
2055
+ request.user,
2056
+ name=schedule_form.cleaned_data.get("_schedule_name"),
2057
+ start_time=schedule_form.cleaned_data.get("_schedule_start_time"),
2058
+ interval=schedule_type,
2059
+ crontab=schedule_form.cleaned_data.get("_recurrence_custom_time"),
2060
+ job_queue=job_queue,
2061
+ profile=profile,
2062
+ ignore_singleton_lock=ignore_singleton_lock,
2063
+ **job_class.serialize_data(job_form.cleaned_data),
2066
2064
  )
2067
- else:
2068
- if dryrun and not is_scheduled:
2069
- # Enqueue job for immediate execution when dryrun and (no schedule, no has_sensitive_variables)
2065
+ scheduled_job_has_approval_workflow = scheduled_job.has_approval_workflow_definition()
2066
+ is_scheduled = schedule_type in JobExecutionType.SCHEDULE_CHOICES
2067
+ if job_model.has_sensitive_variables and scheduled_job_has_approval_workflow:
2068
+ messages.error(
2069
+ request,
2070
+ "Unable to run or schedule job: "
2071
+ "This job is flagged as possibly having sensitive variables but also has an applicable approval workflow definition."
2072
+ "Modify or remove the approval workflow definition or modify the job to set `has_sensitive_variables` to False.",
2073
+ )
2074
+ scheduled_job.delete()
2075
+ scheduled_job = None
2076
+ else:
2077
+ if dryrun and not is_scheduled:
2078
+ # Enqueue job for immediate execution when dryrun and (no schedule, no has_sensitive_variables)
2079
+ scheduled_job.delete()
2080
+ scheduled_job = None
2081
+ return self._handle_immediate_execution(
2082
+ request,
2083
+ job_model,
2084
+ job_class,
2085
+ job_form,
2086
+ profile,
2087
+ ignore_singleton_lock,
2088
+ job_queue,
2089
+ return_url,
2090
+ )
2091
+ # Step 1: Check if approval is required
2092
+ if scheduled_job_has_approval_workflow:
2093
+ return self._handle_approval_workflow_response(request, scheduled_job, return_url)
2094
+
2095
+ # Step 3: If approval is not required
2096
+ if is_scheduled:
2097
+ return self._handle_scheduled_job_response(request, scheduled_job, return_url)
2098
+
2099
+ # Step 4: Immediate execution (no schedule, no approval)
2100
+ scheduled_job.delete()
2101
+ scheduled_job = None
2070
2102
  return self._handle_immediate_execution(
2071
- request, job_model, job_class, job_form, profile, ignore_singleton_lock, job_queue, return_url
2103
+ request,
2104
+ job_model,
2105
+ job_class,
2106
+ job_form,
2107
+ profile,
2108
+ ignore_singleton_lock,
2109
+ job_queue,
2110
+ return_url,
2072
2111
  )
2073
- # Step 1: Check if approval is required
2074
- if scheduled_job_has_approval_workflow:
2075
- scheduled_job.validated_save()
2076
- return self._handle_approval_workflow_response(request, scheduled_job, return_url)
2077
-
2078
- # Step 3: If approval is not required
2079
- if is_scheduled:
2080
- scheduled_job.validated_save()
2081
- return self._handle_scheduled_job_response(request, scheduled_job, return_url)
2082
-
2083
- # Step 4: Immediate execution (no schedule, no approval)
2084
- return self._handle_immediate_execution(
2085
- request,
2086
- job_model,
2087
- job_class,
2088
- job_form,
2089
- profile,
2090
- ignore_singleton_lock,
2091
- job_queue,
2092
- return_url,
2093
- )
2094
2112
 
2095
2113
  if return_url:
2096
2114
  return redirect(return_url)
@@ -2139,13 +2157,17 @@ class JobView(generic.ObjectView):
2139
2157
  section=SectionChoices.LEFT_HALF,
2140
2158
  label="Job",
2141
2159
  fields=["grouping", "name", "description", "enabled"],
2160
+ value_transforms={
2161
+ "description": [helpers.render_markdown],
2162
+ },
2142
2163
  ),
2143
2164
  object_detail.ObjectsTablePanel(
2144
2165
  weight=100,
2145
2166
  section=SectionChoices.FULL_WIDTH,
2146
2167
  table_class=tables.JobResultTable,
2147
- table_title="JobResults",
2148
- table_filter=["job_model"],
2168
+ table_title="Job Results",
2169
+ table_filter="job_model",
2170
+ exclude_columns=["name", "job_model"],
2149
2171
  ),
2150
2172
  jobs_ui.JobObjectFieldsPanel(
2151
2173
  weight=100,
@@ -2501,7 +2523,7 @@ class SavedViewUIViewSet(
2501
2523
 
2502
2524
 
2503
2525
  class ScheduledJobListView(generic.ObjectListView):
2504
- queryset = ScheduledJob.objects.enabled()
2526
+ queryset = ScheduledJob.objects.all()
2505
2527
  table = tables.ScheduledJobTable
2506
2528
  filterset = filters.ScheduledJobFilterSet
2507
2529
  filterset_form = forms.ScheduledJobFilterForm
@@ -2923,7 +2945,12 @@ class ObjectMetadataUIViewSet(
2923
2945
 
2924
2946
 
2925
2947
  class NoteUIViewSet(
2926
- ObjectChangeLogViewMixin, ObjectDestroyViewMixin, ObjectDetailViewMixin, ObjectEditViewMixin, ObjectListViewMixin
2948
+ ObjectDestroyViewMixin,
2949
+ ObjectDetailViewMixin,
2950
+ ObjectEditViewMixin,
2951
+ ObjectListViewMixin,
2952
+ ObjectChangeLogViewMixin,
2953
+ ObjectDataComplianceViewMixin,
2927
2954
  ):
2928
2955
  filterset_class = filters.NoteFilterSet
2929
2956
  filterset_form_class = forms.NoteFilterForm
@@ -3250,6 +3277,7 @@ class SecretUIViewSet(
3250
3277
  ObjectBulkDestroyViewMixin,
3251
3278
  # no ObjectBulkUpdateViewMixin here yet
3252
3279
  ObjectChangeLogViewMixin,
3280
+ ObjectDataComplianceViewMixin,
3253
3281
  ObjectNotesViewMixin,
3254
3282
  ):
3255
3283
  queryset = Secret.objects.all()
@@ -3331,7 +3359,8 @@ class SecretsGroupUIViewSet(NautobotUIViewSet):
3331
3359
  object_detail.ObjectsTablePanel(
3332
3360
  table_class=tables.SecretsGroupAssociationTable,
3333
3361
  table_filter="secrets_group",
3334
- related_field_name="secrets_group",
3362
+ related_field_name="secrets_groups",
3363
+ related_list_url_name="extras:secret_list",
3335
3364
  table_title="Secrets",
3336
3365
  section=SectionChoices.LEFT_HALF,
3337
3366
  weight=200,
@@ -3437,6 +3466,7 @@ class TagUIViewSet(NautobotUIViewSet):
3437
3466
  select_related_fields=["content_type"],
3438
3467
  prefetch_related_fields=["content_object"],
3439
3468
  include_paginator=True,
3469
+ enable_related_link=False,
3440
3470
  ),
3441
3471
  ),
3442
3472
  )
@@ -3493,6 +3523,7 @@ class TeamUIViewSet(NautobotUIViewSet):
3493
3523
  table_filter="team",
3494
3524
  table_title="Contact For",
3495
3525
  add_button_route=None,
3526
+ enable_related_link=False,
3496
3527
  ),
3497
3528
  )
3498
3529
  )
@@ -3525,7 +3556,7 @@ class WebhookUIViewSet(NautobotUIViewSet):
3525
3556
  section=SectionChoices.LEFT_HALF,
3526
3557
  weight=100,
3527
3558
  fields=("http_method", "http_content_type", "payload_url", "additional_headers"),
3528
- value_transforms={"additional_headers": [helpers.pre_tag]},
3559
+ value_transforms={"additional_headers": [partial(helpers.pre_tag, format_empty_value=False)]},
3529
3560
  ),
3530
3561
  object_detail.ObjectFieldsPanel(
3531
3562
  label="Security",
nautobot/ipam/filters.py CHANGED
@@ -233,6 +233,13 @@ class PrefixFilterSet(
233
233
  method="search_contains",
234
234
  label="Prefixes which contain this prefix or IP",
235
235
  )
236
+ ancestors = NaturalKeyOrPKMultipleChoiceFilter(
237
+ queryset=Prefix.objects.all(),
238
+ prefers_id=True,
239
+ to_field_name="network",
240
+ method="filter_ancestors",
241
+ label="Prefixes which are ancestors of this prefix (ID or host string)",
242
+ )
236
243
  vrfs = NaturalKeyOrPKMultipleChoiceFilter(
237
244
  queryset=VRF.objects.all(),
238
245
  to_field_name="rd",
@@ -295,6 +302,10 @@ class PrefixFilterSet(
295
302
  to_field_name="pk",
296
303
  label="VPN Tunnel Endpoint ID",
297
304
  )
305
+ vpn_tunnel_endpoints_name_contains = django_filters.CharFilter(
306
+ method="filter_vpntunnelendpoint_name_contains",
307
+ label="VPN Tunnel Endpoint Name Contains",
308
+ )
298
309
 
299
310
  class Meta:
300
311
  model = Prefix
@@ -345,6 +356,13 @@ class PrefixFilterSet(
345
356
  prefixes_queryset |= queryset.filter(query)
346
357
  return prefixes_queryset
347
358
 
359
+ def filter_ancestors(self, queryset, name, value):
360
+ if not value:
361
+ return queryset
362
+ prefixes = Prefix.objects.filter(pk__in=[v.id for v in value])
363
+ ancestor_ids = [ancestor.id for prefix in prefixes for ancestor in prefix.ancestors()]
364
+ return queryset.filter(pk__in=ancestor_ids)
365
+
348
366
  def generate_query_filter_present_in_vrf(self, value):
349
367
  if isinstance(value, (str, uuid.UUID)):
350
368
  value = VRF.objects.get(pk=value)
@@ -358,6 +376,9 @@ class PrefixFilterSet(
358
376
  params = self.generate_query_filter_present_in_vrf(value)
359
377
  return queryset.filter(params).distinct()
360
378
 
379
+ def filter_vpntunnelendpoint_name_contains(self, queryset, name, value):
380
+ return queryset.filter(vpn_tunnel_endpoints__name__contains=value)
381
+
361
382
 
362
383
  class PrefixLocationAssignmentFilterSet(NautobotFilterSet):
363
384
  q = SearchFilter(
@@ -468,6 +489,11 @@ class IPAddressFilterSet(
468
489
  label="Has NAT Inside",
469
490
  )
470
491
  ip_version = django_filters.NumberFilter()
492
+ services = NaturalKeyOrPKMultipleChoiceFilter(
493
+ queryset=Service.objects.all(),
494
+ to_field_name="name",
495
+ label="Services (name or ID)",
496
+ )
471
497
 
472
498
  class Meta:
473
499
  model = IPAddress
nautobot/ipam/tables.py CHANGED
@@ -379,6 +379,11 @@ class PrefixTable(StatusTableMixin, RoleTableMixin, BaseTable):
379
379
  cloud_networks_count = LinkedCountColumn(
380
380
  viewname="cloud:cloudnetwork_list", url_params={"prefixes": "pk"}, verbose_name="Cloud Networks"
381
381
  )
382
+ tunnel_endpoints_count = LinkedCountColumn(
383
+ viewname="vpn:vpntunnelendpoint_list",
384
+ url_params={"protected_prefixes": "pk"},
385
+ verbose_name="VPN Tunnel Endpoints",
386
+ )
382
387
  actions = ButtonsColumn(Prefix)
383
388
 
384
389
  class Meta(BaseTable.Meta):
@@ -394,6 +399,7 @@ class PrefixTable(StatusTableMixin, RoleTableMixin, BaseTable):
394
399
  "tenant",
395
400
  "location_count",
396
401
  "cloud_networks_count",
402
+ "tunnel_endpoints_count",
397
403
  "vlan",
398
404
  "role",
399
405
  "rir",
@@ -1,4 +1,4 @@
1
- {% extends 'ipam/namespace_retrieve.html' %}
1
+ {% extends 'generic/object_retrieve.html' %}
2
2
 
3
3
  {% block title %}{{ block.super }} - IP Addresses{% endblock %}
4
4
 
@@ -1,4 +1,4 @@
1
- {% extends 'ipam/namespace_retrieve.html' %}
1
+ {% extends 'generic/object_retrieve.html' %}
2
2
 
3
3
  {% block title %}{{ block.super }} - Prefixes{% endblock %}
4
4
 
@@ -1,4 +1,4 @@
1
- {% extends 'ipam/namespace_retrieve.html' %}
1
+ {% extends 'generic/object_retrieve.html' %}
2
2
 
3
3
  {% block title %}{{ block.super }} - VRFs{% endblock %}
4
4
 
@@ -187,6 +187,7 @@ class PrefixTestCase(FilterTestCases.FilterTestCase, FilterTestCases.TenancyFilt
187
187
  ["status", "status__id"],
188
188
  ["status", "status__name"],
189
189
  ["type"],
190
+ ["vpn_tunnel_endpoints", "vpn_tunnel_endpoints__id"],
190
191
  )
191
192
 
192
193
  def test_filters_generic(self):
@@ -230,6 +231,17 @@ class PrefixTestCase(FilterTestCases.FilterTestCase, FilterTestCases.TenancyFilt
230
231
  all_prefixes = self.queryset.all()
231
232
  self.assertQuerysetEqualAndNotEmpty(self.filterset(params, self.queryset).qs, all_prefixes)
232
233
 
234
+ def test_ancestors(self):
235
+ prefixes = (
236
+ Prefix.objects.create(prefix="10.0.0.0/8", status=Status.objects.get_for_model(Prefix).first()),
237
+ Prefix.objects.create(prefix="10.0.0.0/16", status=Status.objects.get_for_model(Prefix).first()),
238
+ Prefix.objects.create(prefix="10.0.0.0/24", status=Status.objects.get_for_model(Prefix).first()),
239
+ )
240
+ params = {"ancestors": [str(prefixes[2].pk)]}
241
+ filterset = self.filterset(params, self.queryset)
242
+ ancestors = [ancestor.id for ancestor in prefixes[2].ancestors()]
243
+ self.assertQuerysetEqualAndNotEmpty(filterset.qs, self.queryset.filter(id__in=ancestors))
244
+
233
245
 
234
246
  class PrefixLocationAssignmentTestCase(FilterTestCases.FilterTestCase):
235
247
  queryset = PrefixLocationAssignment.objects.all()
@@ -518,7 +530,11 @@ class IPAddressTestCase(FilterTestCases.FilterTestCase, FilterTestCases.TenancyF
518
530
  queryset = IPAddress.objects.all()
519
531
  filterset = IPAddressFilterSet
520
532
  tenancy_related_name = "ip_addresses"
521
- generic_filter_tests = (["nat_inside", "nat_inside__id"],)
533
+ generic_filter_tests = (
534
+ ["nat_inside", "nat_inside__id"],
535
+ ["services", "services__id"],
536
+ ["services", "services__name"],
537
+ )
522
538
 
523
539
  @classmethod
524
540
  def setUpTestData(cls):
@@ -710,6 +726,15 @@ class IPAddressTestCase(FilterTestCases.FilterTestCase, FilterTestCases.TenancyF
710
726
  nat_inside=ip1,
711
727
  )
712
728
 
729
+ services = (
730
+ Service.objects.create(name="Service 1", protocol="TCP", ports=[80]),
731
+ Service.objects.create(name="Service 2", protocol="UDP", ports=[53]),
732
+ Service.objects.create(name="Service 3", protocol="TCP", ports=[443]),
733
+ )
734
+ services[0].ip_addresses.add(ip0)
735
+ services[1].ip_addresses.add(ip1)
736
+ services[2].ip_addresses.add(ip2)
737
+
713
738
  def test_search(self):
714
739
  ipv4_octets = self.ipv4_address.host.split(".")
715
740
  ipv6_hextets = self.ipv6_address.host.split(":")
@@ -34,7 +34,7 @@ class IPAddressToInterfaceTest(TestCase):
34
34
 
35
35
  @classmethod
36
36
  def setUpTestData(cls):
37
- cls.namespace = Namespace.objects.first()
37
+ cls.namespace = Namespace.objects.create(name="IPAM Models Test Namespace")
38
38
  cls.status = Status.objects.get(name="Active")
39
39
  cls.prefix = Prefix.objects.create(prefix="192.0.2.0/24", status=cls.status, namespace=cls.namespace)
40
40
  cls.test_device = Device.objects.create(